import * as plugins from './plugins.js'; export const controllerIssuerIdentityDocumentId = 'controller-issuer-v1'; export const controllerIssuerAnchorVersion = 1 as const; const base64UrlPattern = /^[A-Za-z0-9_-]+$/; export interface IControllerIssuerAnchor { version: typeof controllerIssuerAnchorVersion; issuerId: string; createdAt: number; anchor: string; } export interface IControllerIssuerIdentityDocument { id: typeof controllerIssuerIdentityDocumentId; issuerId: string; anchorHash: string; createdAt: Date; state: 'initializing' | 'active'; pendingAnchor?: string; } export interface IInitializingControllerIssuerIdentityDocument extends IControllerIssuerIdentityDocument { state: 'initializing'; pendingAnchor: string; } const isPlainObject = (valueArg: unknown): valueArg is Record => ( typeof valueArg === 'object' && valueArg !== null && !Array.isArray(valueArg) && ( Object.getPrototypeOf(valueArg) === Object.prototype || Object.getPrototypeOf(valueArg) === null ) ); const hasExactKeys = ( valueArg: Record, requiredKeysArg: readonly string[], ): boolean => { const keys = Object.keys(valueArg); return keys.length === requiredKeysArg.length && requiredKeysArg.every((key) => Object.hasOwn(valueArg, key)); }; const isBase64UrlBytes = (valueArg: unknown, byteLengthArg: number): valueArg is string => { if ( typeof valueArg !== 'string' || !base64UrlPattern.test(valueArg) || Buffer.from(valueArg, 'base64url').byteLength !== byteLengthArg ) return false; return Buffer.from(valueArg, 'base64url').toString('base64url') === valueArg; }; export const controllerIssuerAnchorBytes = ( anchorArg: IControllerIssuerAnchor, ): Buffer => Buffer.from(`${JSON.stringify(anchorArg)}\n`, 'utf8'); export const controllerIssuerAnchorHash = (anchorBytesArg: Uint8Array): string => plugins.crypto .createHash('sha256') .update(anchorBytesArg) .digest('base64url'); export const parseControllerIssuerAnchor = ( bytesArg: Uint8Array, ): IControllerIssuerAnchor => { let parsed: unknown; try { parsed = JSON.parse(Buffer.from(bytesArg).toString('utf8')); } catch (errorArg) { throw new Error('The controller issuer anchor is malformed.', { cause: errorArg }); } if ( !isPlainObject(parsed) || !hasExactKeys(parsed, ['version', 'issuerId', 'createdAt', 'anchor']) || parsed.version !== controllerIssuerAnchorVersion || !isBase64UrlBytes(parsed.issuerId, 32) || !Number.isSafeInteger(parsed.createdAt) || (parsed.createdAt as number) < 0 || !isBase64UrlBytes(parsed.anchor, 32) ) { throw new Error('The controller issuer anchor is malformed.'); } const anchor: IControllerIssuerAnchor = { version: controllerIssuerAnchorVersion, issuerId: parsed.issuerId, createdAt: parsed.createdAt as number, anchor: parsed.anchor, }; if (!Buffer.from(bytesArg).equals(controllerIssuerAnchorBytes(anchor))) { throw new Error('The controller issuer anchor is not canonically encoded.'); } return anchor; }; export const assertControllerIssuerIdentityDocument = ( valueArg: unknown, ): asserts valueArg is IControllerIssuerIdentityDocument => { if (!isPlainObject(valueArg)) { throw new Error('Invalid controller issuer identity document.'); } const requiredKeys = valueArg.state === 'initializing' ? ['id', 'issuerId', 'anchorHash', 'createdAt', 'state', 'pendingAnchor'] : ['id', 'issuerId', 'anchorHash', 'createdAt', 'state']; if ( !hasExactKeys(valueArg, requiredKeys) || valueArg.id !== controllerIssuerIdentityDocumentId || !isBase64UrlBytes(valueArg.issuerId, 32) || !isBase64UrlBytes(valueArg.anchorHash, 32) || !(valueArg.createdAt instanceof Date) || !Number.isFinite(valueArg.createdAt.getTime()) || valueArg.createdAt.getTime() < 0 || (valueArg.state !== 'initializing' && valueArg.state !== 'active') ) { throw new Error('Invalid controller issuer identity document.'); } if (valueArg.state === 'initializing') { if (!isBase64UrlBytes(valueArg.pendingAnchor, 32)) { throw new Error('Invalid initializing controller issuer identity document.'); } const anchorBytes = controllerIssuerAnchorBytes({ version: controllerIssuerAnchorVersion, issuerId: valueArg.issuerId, createdAt: valueArg.createdAt.getTime(), anchor: valueArg.pendingAnchor, }); if (controllerIssuerAnchorHash(anchorBytes) !== valueArg.anchorHash) { throw new Error('Invalid initializing controller issuer anchor hash.'); } } }; export const isInitializingControllerIssuerIdentityDocument = ( valueArg: IControllerIssuerIdentityDocument, ): valueArg is IInitializingControllerIssuerIdentityDocument => valueArg.state === 'initializing'; @plugins.smartdata.managed({ collectionName: 'agl_controller_issuer_identities' }) @plugins.smartdata.exactPersistence({ assertDocument: assertControllerIssuerIdentityDocument, }) export class ControllerIssuerIdentityModel extends plugins.smartdata.SmartDataDbDoc< ControllerIssuerIdentityModel, IControllerIssuerIdentityDocument > { declare static exact: plugins.smartdata.TExact; @plugins.smartdata.unI() public id!: typeof controllerIssuerIdentityDocumentId; @plugins.smartdata.svDb() public issuerId!: string; @plugins.smartdata.svDb() public anchorHash!: string; @plugins.smartdata.svDb() public createdAt!: Date; @plugins.smartdata.svDb() public state!: IControllerIssuerIdentityDocument['state']; @plugins.smartdata.svDb() public pendingAnchor?: string; }