@tinycloud/node-sdk 3.9.0-beta.3 → 3.9.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/credential-pending-store.cjs +45 -9
- package/dist/credential-pending-store.cjs.map +1 -1
- package/dist/credential-pending-store.d.cts +2 -1
- package/dist/credential-pending-store.d.ts +2 -1
- package/dist/credential-pending-store.js +54 -17
- package/dist/credential-pending-store.js.map +1 -1
- package/dist/credentials-core.cjs +140 -56
- package/dist/credentials-core.cjs.map +1 -1
- package/dist/credentials-core.d.cts +21 -1
- package/dist/credentials-core.d.ts +21 -1
- package/dist/credentials-core.js +138 -54
- package/dist/credentials-core.js.map +1 -1
- package/dist/credentials.cjs +306 -117
- package/dist/credentials.cjs.map +1 -1
- package/dist/credentials.d.cts +33 -1
- package/dist/credentials.d.ts +33 -1
- package/dist/credentials.js +294 -107
- package/dist/credentials.js.map +1 -1
- package/dist/{email-proof-D0r_WShV.d.cts → email-proof-B4DRizIL.d.cts} +23 -5
- package/dist/{email-proof-D0r_WShV.d.ts → email-proof-B4DRizIL.d.ts} +23 -5
- package/dist/share-receive.cjs +294 -115
- package/dist/share-receive.cjs.map +1 -1
- package/dist/share-receive.d.cts +2 -1
- package/dist/share-receive.d.ts +2 -1
- package/dist/share-receive.js +278 -98
- package/dist/share-receive.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/credential-pending-store.ts","../src/credentials/email-proof.ts","../src/credentials/interpreter.ts","../src/credentials/transport.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport {\n EMAIL_PROOF_PENDING_MAX_MS,\n pendingDeadlineMs,\n restoreEmailProofPending,\n type EmailProofPending,\n type EmailProofPendingStore,\n} from './credentials/email-proof';\n\n/**\n * File-backed pending store for the email-proof flow (`@tinycloud/node-sdk/credential-pending-store`).\n *\n * One file per request, named `pending-<sha256(requestId)>.json`. No file name contains a mailbox\n * address. A record holds a bearer verifier and optionally a base64 secret, so the store is owner-only:\n * a 0700 directory, 0600 files, and symlink, hard-link, and owner checks on every read. A record may\n * not expire more than 15 minutes after its `createdAt`; records expire on read.\n * Only a symlinked final `home` is refused. Ancestors may be symlinks; the resolved `home` must be owned by the current user.\n *\n * Unsafe files throw and are left in place. Malformed or over-long files are deleted and throw.\n * One unsafe file therefore makes `findLive` throw for every address until the file is removed.\n */\n\nexport type PendingStoreErrorCode = 'credential-permissions' | 'credential-store-invalid';\n\nexport class PendingStoreError extends Error {\n readonly code: PendingStoreErrorCode;\n\n constructor(code: PendingStoreErrorCode, message: string) {\n super(message);\n this.name = 'PendingStoreError';\n this.code = code;\n }\n}\n\nexport interface FileEmailProofPendingStoreOptions {\n /** Absolute profile directory. Pending files live under `<home>/credentials`. */\n readonly home: string;\n}\n\nexport interface PutEmailProofPendingOptions {\n /** Optional caller secret, stored base64 in the owner-only file. Read it back with `storedSecret`. Never serialized. */\n readonly secret?: Uint8Array;\n}\n\nexport interface FileEmailProofPendingStore extends EmailProofPendingStore {\n get(requestId: string, now: Date): Promise<EmailProofPending | undefined>;\n put(pending: EmailProofPending, options?: PutEmailProofPendingOptions): Promise<void>;\n}\n\n/** The secret stored with a pending record, or undefined. Returns a copy; the record keeps its own. */\nexport function storedSecret(pending: EmailProofPending): Uint8Array | undefined {\n const secret = secrets.get(pending);\n return secret === undefined ? undefined : new Uint8Array(secret);\n}\n\nconst DIR_NAME = 'credentials';\nconst PENDING_FILE_PATTERN = /^pending-[0-9a-f]{64}\\.json$/;\n/** Temp files from `writeOwnerOnly`: `<pending file>.<pid>.<12 hex>.tmp`. */\nconst PENDING_TEMP_PATTERN = /^pending-[0-9a-f]{64}\\.json\\.\\d+\\.[0-9a-f]{12}\\.tmp$/;\nconst BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\nconst FILE_TEXT_FIELDS = [\n 'profile',\n 'address',\n 'requestId',\n 'verifier',\n 'holderDid',\n 'descriptorDigest',\n 'requirementDigest',\n 'openerOrigin',\n 'createdAt',\n 'expiresAt',\n] as const;\n\ninterface FileRecord {\n readonly type: 'TinyCloudEmailProofPending';\n readonly version: 1;\n readonly profile: string;\n readonly address: string;\n readonly requestId: string;\n readonly verifier: string;\n readonly holderDid: string;\n readonly descriptorDigest: string;\n readonly requirementDigest: string;\n readonly openerOrigin: string;\n readonly createdAt: string;\n readonly expiresAt: string;\n readonly secret?: string;\n}\n\ninterface Entry {\n readonly pending: EmailProofPending;\n readonly expired: boolean;\n}\n\nconst secrets = new WeakMap<EmailProofPending, Uint8Array>();\n\nfunction sha256Hex(text: string): string {\n return createHash('sha256').update(text).digest('hex');\n}\n\nfunction credentialsDir(home: string): string {\n return path.join(home, DIR_NAME);\n}\n\nfunction pendingFile(home: string, requestId: string): string {\n return path.join(credentialsDir(home), `pending-${sha256Hex(requestId)}.json`);\n}\n\nfunction assertAbsoluteHome(home: unknown): asserts home is string {\n if (typeof home !== 'string' || home.length === 0 || !path.isAbsolute(home)) {\n throw new PendingStoreError('credential-store-invalid', 'Credential store home must be a non-empty absolute path.');\n }\n}\n\nfunction lstatOrUndefined(file: string): fs.Stats | undefined {\n try {\n return fs.lstatSync(file);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n}\n\nfunction assertOwnedByCurrentUser(st: fs.Stats, label: string): void {\n if (typeof process.getuid === 'function' && st.uid !== process.getuid()) {\n throw new PendingStoreError('credential-permissions', `${label} is not owned by the current user.`);\n }\n}\n\nfunction assertOwnerOnly(st: fs.Stats, label: string): void {\n const mode = st.mode & 0o777;\n if ((mode & 0o077) !== 0) {\n throw new PendingStoreError('credential-permissions', `${label} must be owner-only; found mode ${mode.toString(8)}.`);\n }\n // Owner check: a file owned by another user is untrusted even at mode 0600.\n assertOwnedByCurrentUser(st, label);\n}\n\n/**\n * Refuses a symlinked `home`. Ancestors may be symlinks (macOS `/var` is `/private/var`), so the resolved\n * `home` must be owned by the current user instead. A missing `home` is fine: the caller creates it.\n */\nfunction assertHomeSafe(home: string): void {\n const st = lstatOrUndefined(home);\n if (st?.isSymbolicLink()) {\n throw new PendingStoreError('credential-permissions', `${home} is a symlink; refusing to store credentials through it.`);\n }\n if (st) assertOwnedByCurrentUser(fs.statSync(fs.realpathSync(home)), home);\n}\n\n/**\n * Creates each missing directory on `target` with mode 0700 and pins it, so a restrictive umask cannot\n * leave the store unwritable. Existing directories are not touched.\n */\nfunction mkdirOwnerOnly(target: string): void {\n const missing: string[] = [];\n let current = target;\n while (lstatOrUndefined(current) === undefined) {\n missing.unshift(current);\n const parent = path.dirname(current);\n if (parent === current) break;\n current = parent;\n }\n for (const dir of missing) {\n try {\n fs.mkdirSync(dir, { mode: 0o700 });\n } catch (error) {\n // A pre-existing directory is not ours to chmod; ensureSecureDir checks it instead.\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n continue;\n }\n fs.chmodSync(dir, 0o700);\n }\n}\n\n/** Creates the store directory 0700 if absent. A pre-existing directory must already be owner-only; it is never chmodded. */\nexport function ensureSecureDir(home: string): void {\n assertHomeSafe(home);\n const dir = credentialsDir(home);\n mkdirOwnerOnly(home);\n mkdirOwnerOnly(dir);\n const st = fs.lstatSync(dir);\n if (!st.isDirectory()) {\n throw new PendingStoreError('credential-permissions', `${dir} must be a real directory, not a symlink or file.`);\n }\n // A loose pre-existing directory is refused, never chmodded: it may not be ours. assertOwnerOnly also checks the owner.\n assertOwnerOnly(st, dir);\n}\n\n/** Returns false when the store directory does not exist. Re-checks symlink, owner, and mode on every call. */\nexport function checkSecureDir(home: string): boolean {\n assertHomeSafe(home);\n const dir = credentialsDir(home);\n const st = lstatOrUndefined(dir);\n if (!st) return false;\n if (!st.isDirectory()) {\n throw new PendingStoreError('credential-permissions', `${dir} must be a real directory, not a symlink or file.`);\n }\n assertOwnerOnly(st, dir);\n return true;\n}\n\n/**\n * Atomic owner-only write: O_EXCL|O_NOFOLLOW temp file at 0600, fsync, then rename.\n * Refuses to replace a destination that is not a regular file.\n */\nfunction writeOwnerOnly(home: string, file: string, record: FileRecord): void {\n ensureSecureDir(home);\n const existing = lstatOrUndefined(file);\n if (existing && !existing.isFile()) {\n throw new PendingStoreError('credential-permissions', `${path.basename(file)} exists and is not a regular file; refusing to replace it.`);\n }\n const temp = `${file}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;\n let fd: number | undefined;\n // O_EXCL: never reuse an existing temp name. O_NOFOLLOW: a symlink planted at the temp name fails the open.\n try {\n fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);\n fs.writeFileSync(fd, JSON.stringify(record, null, 2) + '\\n');\n fs.fchmodSync(fd, 0o600);\n fs.fsyncSync(fd);\n fs.closeSync(fd);\n fd = undefined;\n fs.renameSync(temp, file);\n } finally {\n if (fd !== undefined) fs.closeSync(fd);\n try {\n fs.unlinkSync(temp);\n } catch {\n /* renamed or absent */\n }\n }\n}\n\n/**\n * Reads an owner-only JSON file. Re-checks the directory and the file on every read, refuses\n * symlinks and hard links, and confirms the opened descriptor is the inode that was checked.\n */\nfunction readOwnerOnly(home: string, file: string): unknown {\n if (!checkSecureDir(home)) return undefined;\n const label = path.basename(file);\n const before = lstatOrUndefined(file);\n if (!before) return undefined;\n if (!before.isFile()) {\n throw new PendingStoreError('credential-permissions', `${label} must be a regular file; symlinks are refused.`);\n }\n // lstat and owner check run before the open; the fstat below compares the opened inode.\n assertOwnerOnly(before, label);\n // O_NOFOLLOW: a symlink swapped in after the lstat fails the open instead of being read.\n const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);\n let text: string;\n try {\n const opened = fs.fstatSync(fd);\n if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) {\n throw new PendingStoreError('credential-permissions', `${label} changed while it was being opened.`);\n }\n if (opened.nlink !== 1) {\n throw new PendingStoreError('credential-permissions', `${label} has multiple hard links; refusing to read it.`);\n }\n assertOwnerOnly(opened, label);\n text = fs.readFileSync(fd, 'utf8');\n } finally {\n fs.closeSync(fd);\n }\n try {\n return JSON.parse(text) as unknown;\n } catch {\n removeFile(file);\n throw new PendingStoreError('credential-store-invalid', `Invalid credential store file ${label}.`);\n }\n}\n\nfunction removeFile(file: string): void {\n fs.rmSync(file, { force: true });\n}\n\n/** A temp file older than the pending window was left by a killed writer. It may hold a verifier, so it is deleted. */\nfunction sweepAbandonedTemp(file: string, now: Date): void {\n const st = lstatOrUndefined(file);\n if (st && now.getTime() - st.mtimeMs > EMAIL_PROOF_PENDING_MAX_MS) removeFile(file);\n}\n\nfunction exceedsWindow(record: { readonly createdAt: string; readonly expiresAt: string }): boolean {\n return Date.parse(record.expiresAt) > Date.parse(record.createdAt) + EMAIL_PROOF_PENDING_MAX_MS;\n}\n\nfunction isFileRecord(value: unknown): value is FileRecord {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const record = value as Record<string, unknown>;\n if (record.type !== 'TinyCloudEmailProofPending' || record.version !== 1) return false;\n if (!FILE_TEXT_FIELDS.every((field) => typeof record[field] === 'string')) return false;\n if (record.secret !== undefined && (typeof record.secret !== 'string' || !BASE64.test(record.secret))) return false;\n return Number.isFinite(Date.parse(record.createdAt as string)) && Number.isFinite(Date.parse(record.expiresAt as string));\n}\n\n/** Builds the pending object from named fields only. Unknown keys in the file (including `__proto__`) are ignored. */\nfunction restoreFromFile(record: FileRecord): EmailProofPending {\n return restoreEmailProofPending({\n type: record.type,\n version: record.version,\n profile: record.profile as EmailProofPending['profile'],\n address: record.address,\n requestId: record.requestId,\n verifier: record.verifier,\n holderDid: record.holderDid,\n descriptorDigest: record.descriptorDigest,\n requirementDigest: record.requirementDigest,\n openerOrigin: record.openerOrigin,\n createdAt: record.createdAt,\n expiresAt: record.expiresAt,\n });\n}\n\n/** Reads one pending file. Deletes expired files, and malformed or over-long files before throwing. */\nfunction readEntry(home: string, file: string, now: Date): Entry | undefined {\n const value = readOwnerOnly(home, file);\n if (value === undefined) return undefined;\n if (!isFileRecord(value) || pendingFile(home, value.requestId) !== file || exceedsWindow(value)) {\n removeFile(file);\n throw new PendingStoreError('credential-store-invalid', 'Pending verification file is invalid and was deleted.');\n }\n const pending = restoreFromFile(value);\n if (value.secret !== undefined) secrets.set(pending, Buffer.from(value.secret, 'base64'));\n const expired = pendingDeadlineMs(pending) <= now.getTime();\n if (expired) removeFile(file);\n return { pending, expired };\n}\n\nfunction liveEntries(home: string, now: Date): Entry[] {\n if (!checkSecureDir(home)) return [];\n const dir = credentialsDir(home);\n const entries: Entry[] = [];\n for (const name of fs.readdirSync(dir)) {\n const file = path.join(dir, name);\n if (PENDING_TEMP_PATTERN.test(name)) {\n sweepAbandonedTemp(file, now);\n continue;\n }\n if (!PENDING_FILE_PATTERN.test(name)) continue;\n const entry = readEntry(home, file, now);\n if (entry) entries.push(entry);\n }\n return entries;\n}\n\nfunction toFileRecord(pending: EmailProofPending, secret: Uint8Array | undefined): FileRecord {\n return {\n type: pending.type,\n version: pending.version,\n profile: pending.profile,\n address: pending.address,\n requestId: pending.requestId,\n verifier: pending.verifier,\n holderDid: pending.holderDid,\n descriptorDigest: pending.descriptorDigest,\n requirementDigest: pending.requirementDigest,\n openerOrigin: pending.openerOrigin,\n createdAt: pending.createdAt,\n expiresAt: pending.expiresAt,\n ...(secret === undefined ? {} : { secret: Buffer.from(secret).toString('base64') }),\n };\n}\n\n/** Converts raw permission failures (for example EACCES under a restrictive umask) into a stable store error. */\nfunction wrapFsErrors<T>(run: () => T): T {\n try {\n return run();\n } catch (error) {\n if (error instanceof PendingStoreError) throw error;\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'EACCES' || code === 'EPERM') {\n throw new PendingStoreError('credential-permissions', 'Credential store is not accessible to this process; check the profile directory permissions.');\n }\n throw error;\n }\n}\n\n/**\n * File-backed `EmailProofPendingStore`, keyed by request id: one file per request, named by a hash of the\n * id. `put` never touches another request's file. `findLive` returns the newest unexpired record for an\n * address, so a second process can complete a request it did not create.\n */\nexport function createFileEmailProofPendingStore(options: FileEmailProofPendingStoreOptions): FileEmailProofPendingStore {\n const home: unknown = options?.home;\n assertAbsoluteHome(home);\n return {\n async get(requestId, now) {\n return wrapFsErrors(() => {\n const entry = readEntry(home, pendingFile(home, requestId), now);\n return entry === undefined || entry.expired ? undefined : entry.pending;\n });\n },\n async put(pending, putOptions) {\n return wrapFsErrors(() => {\n if (exceedsWindow(pending)) {\n throw new PendingStoreError('credential-store-invalid', 'Pending record expires beyond the 15-minute window and was rejected.');\n }\n writeOwnerOnly(home, pendingFile(home, pending.requestId), toFileRecord(pending, putOptions?.secret));\n });\n },\n async delete(requestId) {\n return wrapFsErrors(() => {\n if (!checkSecureDir(home)) return;\n removeFile(pendingFile(home, requestId));\n });\n },\n async findLive(address, now) {\n return wrapFsErrors(() => {\n let newest: EmailProofPending | undefined;\n for (const entry of liveEntries(home, now)) {\n if (entry.expired || entry.pending.address !== address) continue;\n if (!newest || Date.parse(entry.pending.createdAt) > Date.parse(newest.createdAt)) newest = entry.pending;\n }\n return newest;\n });\n },\n };\n}\n","import {\n canonicalDigest,\n createEmailCredentialRequirement,\n credentialError,\n credentialRequirementDigest,\n CredentialError,\n descriptorSatisfiesRequirement,\n encodeBase64Url,\n sha256Base64Url,\n validateCredentialFlowDescriptor,\n verifyIssuedCredential,\n type CredentialFlowDescriptor,\n type CredentialIssuerMetadata,\n type CredentialRequirement,\n type IssuedCredentialEnvelope,\n type IssuerVerificationKey,\n type StoredCredentialRecord,\n type VerifiedCredential,\n} from \"@tinycloud/sdk-core\";\nimport { interpretCredentialFlow } from \"./interpreter\";\nimport { OpenCredentialsHttpTransport } from \"./transport\";\nimport type { CredentialSigningAdapter, PrimitiveStepHandler } from \"./types\";\n\n/** Production pins. Credentials verify only against these values. Tests pin them literally. */\nexport const EMAIL_PROOF_PINS = Object.freeze({\n witnessOrigin: \"https://witness.credentials.org\",\n issuerDid: \"did:web:issuer.credentials.org\",\n issuerKid: \"did:web:issuer.credentials.org#controller\",\n issuerX: \"eEJI4xEobto4HtQ7Pg9R1vBOwcpfGRTlXDG5QfqLnGQ\",\n descriptorDigest: \"1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0\",\n} as const);\n\nexport const EMAIL_PROOF_CREDENTIAL_TYPE = { id: \"opencredentials.email/v1\", version: 1 } as const;\n/** A pending request is never trusted beyond this window, measured from creation. */\nexport const EMAIL_PROOF_PENDING_MAX_MS = 15 * 60_000;\n\nconst DISCOVERY_URL = `${EMAIL_PROOF_PINS.witnessOrigin}/.well-known/opencredentials`;\nexport const EMAIL_PROOF_ISSUER_KEYS: readonly IssuerVerificationKey[] = [\n {\n kid: EMAIL_PROOF_PINS.issuerKid,\n alg: \"EdDSA\",\n jwk: { kty: \"OKP\", crv: \"Ed25519\", x: EMAIL_PROOF_PINS.issuerX },\n validFrom: \"2020-01-01T00:00:00Z\",\n validUntil: \"2100-01-01T00:00:00Z\",\n },\n];\n\nexport type EmailProofProfileId = \"tinycloud.email-proof/v1\" | \"tinycloud.email-held/v1\";\nexport const DEFAULT_EMAIL_PROOF_PROFILE: EmailProofProfileId = \"tinycloud.email-proof/v1\";\n/** Each profile is pinned to its descriptor digest and to the mailbox step the witness must offer. */\nconst PROFILE_PINS: Readonly<Record<EmailProofProfileId, { readonly descriptorDigest: string; readonly requiredStep: \"mailbox_otp\"; readonly enabled: boolean }>> = {\n \"tinycloud.email-proof/v1\": { descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest, requiredStep: \"mailbox_otp\", enabled: true },\n // Held profile: disabled until the held wire path ships. Requests refuse with PROFILE_UNAVAILABLE before any network call.\n \"tinycloud.email-held/v1\": { descriptorDigest: \"dc3_DuvzipXwYkTfjKqTaK1c3VS0YSPnaBsYSQriC0w\", requiredStep: \"mailbox_otp\", enabled: false },\n};\n\nconst OPENER_ORIGIN = \"https://localhost\";\nconst STEP_TIMEOUT_MS = 60_000;\nconst HOLDER_DID_PREFIX = \"did:key:\";\nconst HOLDER_DID_PATTERN = /^did:key:z6Mk[^#]+$/;\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst INSPECT = Symbol.for(\"nodejs.util.inspect.custom\");\n\n/** Which part of the two-step flow failed. */\nexport type EmailProofStep = \"request\" | \"code\" | \"credential\" | \"session\";\n\n/**\n * Stable error codes. `recoverable` = the same pending request may be retried\n * (with another code, or after the network returns) before it expires.\n *\n * | code | recoverable | reason |\n * |-------------------------|-------------|--------|\n * | ADDRESS_INVALID | false | input is malformed; the same address cannot succeed |\n * | HOLDER_INVALID | false | holder key is not an Ed25519 did:key with a matching kid |\n * | CODE_INVALID | true | local format check (8 digits); no network; pending kept |\n * | RATE_LIMITED | witness | witness `recoverable` flag decides (defaults to true); retryAfterMs set |\n * | ISSUER_UNAVAILABLE | true | transient issuer outage (503); witness flag is honoured |\n * | NETWORK | true | transport failure or step timeout; pending kept |\n * | PROFILE_UNAVAILABLE | false | catalog or descriptor does not match the pinned profile |\n * | CODE_REJECTED | true | wrong code; witness flag decides whether another attempt is allowed |\n * | CODE_ATTEMPTS_EXHAUSTED | false | witness proof-attempt budget spent; pending is dead |\n * | SIGNATURE_REJECTED | true | holder binding rejected; same pending may be retried |\n * | CREDENTIAL_INVALID | false | issued credential failed local verification |\n * | SESSION_EXPIRED | false | pending window or server expiry has passed |\n * | SESSION_INVALID | false | pending does not match holder, descriptor, or requirement |\n * | CANCELED | false | caller aborted; pending is deleted |\n * | FAILED | false | unclassified failure; pending is deleted |\n */\nexport type EmailProofErrorCode =\n | \"ADDRESS_INVALID\"\n | \"HOLDER_INVALID\"\n | \"CODE_INVALID\"\n | \"RATE_LIMITED\"\n | \"ISSUER_UNAVAILABLE\"\n | \"NETWORK\"\n | \"PROFILE_UNAVAILABLE\"\n | \"CODE_REJECTED\"\n | \"CODE_ATTEMPTS_EXHAUSTED\"\n | \"SIGNATURE_REJECTED\"\n | \"CREDENTIAL_INVALID\"\n | \"SESSION_EXPIRED\"\n | \"SESSION_INVALID\"\n | \"CANCELED\"\n | \"FAILED\";\n\nconst RECOVERABILITY: Readonly<Record<EmailProofErrorCode, boolean>> = {\n ADDRESS_INVALID: false,\n HOLDER_INVALID: false,\n CODE_INVALID: true,\n RATE_LIMITED: true,\n ISSUER_UNAVAILABLE: true,\n NETWORK: true,\n PROFILE_UNAVAILABLE: false,\n CODE_REJECTED: true,\n CODE_ATTEMPTS_EXHAUSTED: false,\n SIGNATURE_REJECTED: true,\n CREDENTIAL_INVALID: false,\n SESSION_EXPIRED: false,\n SESSION_INVALID: false,\n CANCELED: false,\n FAILED: false,\n};\n\nconst MESSAGES: Record<Exclude<EmailProofErrorCode, \"RATE_LIMITED\">, string> = {\n ADDRESS_INVALID: \"Email address is invalid.\",\n HOLDER_INVALID: \"Holder must be a did:key Ed25519 identifier.\",\n CODE_INVALID: \"Verification code must be 8 digits.\",\n ISSUER_UNAVAILABLE: \"Email verification is unavailable right now.\",\n NETWORK: \"Could not reach the email verification service.\",\n PROFILE_UNAVAILABLE: \"The witness does not offer the email verification profile.\",\n CODE_REJECTED: \"Verification code was not accepted.\",\n CODE_ATTEMPTS_EXHAUSTED: \"Too many wrong codes. Request a new code.\",\n SIGNATURE_REJECTED: \"Holder signature was rejected.\",\n CREDENTIAL_INVALID: \"Issued credential failed verification.\",\n SESSION_EXPIRED: \"Email verification request expired. Request a new code.\",\n SESSION_INVALID: \"Email verification request is no longer valid. Request a new code.\",\n CANCELED: \"Email verification was canceled.\",\n FAILED: \"Email verification failed.\",\n};\n\nfunction rateLimitMessage(retryAfterMs: number | undefined): string {\n const head = \"Too many codes were requested for this address.\";\n if (retryAfterMs === undefined || !Number.isFinite(retryAfterMs) || retryAfterMs <= 0) return `${head} Try again in about an hour.`;\n const minutes = Math.max(1, Math.ceil(retryAfterMs / 60_000));\n return `${head} Try again in ${minutes} ${minutes === 1 ? \"minute\" : \"minutes\"}.`;\n}\n\n/** Stable, redaction-safe error. The message never includes the code, verifier, or key material. */\nexport class EmailProofError extends Error {\n readonly name = \"EmailProofError\";\n readonly step: EmailProofStep;\n readonly code: EmailProofErrorCode;\n readonly retryAfterMs: number | undefined;\n readonly recoverable: boolean;\n\n constructor(step: EmailProofStep, code: EmailProofErrorCode, retryAfterMs?: number, recoverable: boolean = RECOVERABILITY[code]) {\n super(code === \"RATE_LIMITED\" ? rateLimitMessage(retryAfterMs) : MESSAGES[code]);\n this.step = step;\n this.code = code;\n this.retryAfterMs = retryAfterMs;\n this.recoverable = recoverable;\n }\n}\n\nexport type HolderAlgorithm = \"EdDSA\" | \"EIP191\";\n\n/**\n * Holder key adapter. `kid` names the key the witness verifies against. `completeEmailProof` rejects with\n * SESSION_INVALID unless `did === pending.holderDid`.\n * `sign` MUST return a raw signature over exactly the bytes it is given. For `EdDSA` the signer applies no\n * prefix. For `EIP191` the adapter performs `personal_sign` over those bytes, and the wallet applies the\n * `\\x19Ethereum Signed Message:\\n<len>` prefix. The prefix is applied in that one place and nowhere else.\n */\nexport interface HolderSigner {\n readonly did: string;\n readonly alg: HolderAlgorithm;\n readonly kid: string;\n readonly sign: (bytes: Uint8Array) => Promise<Uint8Array>;\n}\n\n/**\n * Ed25519 did:key holder. Accepts `did:key:z6Mk…` or `did:key:z6Mk…#<same multibase>`\n * (the session-manager form) and normalises to the bare DID. `kid` is derived from the bare DID.\n */\nexport function createEd25519HolderSigner(did: string, sign: (bytes: Uint8Array) => Promise<Uint8Array>): HolderSigner {\n const fragment = did.indexOf(\"#\");\n const bare = fragment === -1 ? did : did.slice(0, fragment);\n if (fragment !== -1 && did.slice(fragment + 1) !== bare.slice(HOLDER_DID_PREFIX.length)) throw new EmailProofError(\"request\", \"HOLDER_INVALID\");\n if (!HOLDER_DID_PATTERN.test(bare)) throw new EmailProofError(\"request\", \"HOLDER_INVALID\");\n return { did: bare, alg: \"EdDSA\", kid: holderKid(bare), sign };\n}\n\n/** Options shared by the public steps. Issuer keys are never caller-supplied. */\nexport interface EmailProofOptions {\n readonly fetch?: typeof fetch;\n readonly signal?: AbortSignal;\n readonly now?: () => Date;\n}\n\nexport interface EmailProofRequestInput extends EmailProofOptions {\n readonly address: string;\n readonly holder: HolderSigner;\n readonly profile?: EmailProofProfileId;\n}\n\n/**\n * Durable state between `requestEmailProofCode` and `completeEmailProof`.\n * `verifier` is a bearer secret. It is a non-enumerable property, so spread\n * omits it; `toJSON` and `util.inspect` redact it. Persist it only with owner-only storage.\n */\nexport interface EmailProofPending {\n readonly type: \"TinyCloudEmailProofPending\";\n readonly version: 1;\n readonly profile: EmailProofProfileId;\n readonly address: string;\n readonly requestId: string;\n readonly verifier: string;\n readonly holderDid: string;\n readonly descriptorDigest: string;\n readonly requirementDigest: string;\n readonly openerOrigin: string;\n readonly createdAt: string;\n readonly expiresAt: string;\n}\n\n/** Holds pending requests keyed by requestId. Implementations must never return an expired request from `findLive`. */\nexport interface EmailProofPendingStore {\n /** Newest unexpired request for `address`, or undefined. */\n findLive(address: string, now: Date): Promise<EmailProofPending | undefined>;\n put(pending: EmailProofPending): Promise<void>;\n delete(requestId: string): Promise<void>;\n /** Pending request by id, or undefined if absent or past its deadline. */\n get(requestId: string, now: Date): Promise<EmailProofPending | undefined>;\n}\n\n/**\n * Rebuilds a pending request from stored fields. The result is redacted exactly like\n * one produced by `requestEmailProofCode`. Stores call this on every read.\n */\nexport function restoreEmailProofPending(fields: EmailProofPending): EmailProofPending {\n const shown = {\n type: fields.type,\n version: fields.version,\n profile: fields.profile,\n address: fields.address,\n requestId: fields.requestId,\n holderDid: fields.holderDid,\n descriptorDigest: fields.descriptorDigest,\n requirementDigest: fields.requirementDigest,\n openerOrigin: fields.openerOrigin,\n createdAt: fields.createdAt,\n expiresAt: fields.expiresAt,\n };\n const pending = { ...shown } as EmailProofPending;\n hide(pending, \"verifier\", fields.verifier);\n hide(pending, \"toJSON\", () => ({ ...shown }));\n hide(pending, INSPECT, () => ({ ...shown, verifier: \"[redacted]\" }));\n return Object.freeze(pending);\n}\n\nfunction hide(target: object, key: string | symbol, value: unknown): void {\n Object.defineProperty(target, key, { value, enumerable: false, writable: false, configurable: false });\n}\n\n/** Latest instant at which a pending request may still be completed. */\nexport function pendingDeadlineMs(pending: EmailProofPending): number {\n return Math.min(Date.parse(pending.expiresAt), Date.parse(pending.createdAt) + EMAIL_PROOF_PENDING_MAX_MS);\n}\n\n/** Process-local pending store for browsers and tests. Nothing survives a reload. */\nexport function createMemoryEmailProofPendingStore(): EmailProofPendingStore {\n const byRequest = new Map<string, EmailProofPending>();\n const prune = (now: Date) => {\n for (const [requestId, pending] of byRequest) if (pendingDeadlineMs(pending) <= now.getTime()) byRequest.delete(requestId);\n };\n return {\n async findLive(address, now) {\n prune(now);\n let newest: EmailProofPending | undefined;\n for (const pending of byRequest.values()) {\n if (pending.address === address && (!newest || Date.parse(pending.createdAt) > Date.parse(newest.createdAt))) newest = pending;\n }\n return newest;\n },\n async put(pending) { byRequest.set(pending.requestId, pending); },\n async delete(requestId) { byRequest.delete(requestId); },\n async get(requestId, now) { prune(now); return byRequest.get(requestId); },\n };\n}\n\nexport interface EmailProofResult {\n readonly envelope: IssuedCredentialEnvelope;\n readonly verified: VerifiedCredential;\n readonly credentialId: string;\n readonly issuedAt: string;\n readonly expiresAt: string;\n /** The descriptor the verifier checked against. Stored alongside the credential for offline re-verification. */\n readonly descriptor: CredentialFlowDescriptor;\n}\n\nexport interface EmailProofAcquireInput extends EmailProofRequestInput {\n /** Supplies the 8-digit code. Omit to only request one; the pending request is returned for a later call. */\n readonly getCode?: (context: { readonly pending: EmailProofPending }) => Promise<string>;\n /** Resumes an unexpired pending request for this address, and keeps one across recoverable failures. */\n readonly pendingStore?: EmailProofPendingStore;\n}\n\nexport type EmailProofAcquisition =\n | { readonly status: \"issued\"; readonly result: EmailProofResult; /** Non-fatal, e.g. the pending record could not be deleted after success. */ readonly warnings: readonly string[] }\n | { readonly status: \"code-requested\"; readonly pending: EmailProofPending };\n\n/** Thrown from the OTP handler to stop step 1 after the witness has sent the code. */\nclass CodeRequested extends Error {}\n\nconst pauseForCode: PrimitiveStepHandler = async () => {\n throw new CodeRequested(\"code requested\");\n};\n\n/**\n * Step 1: start an email-proof request. The witness sends an 8-digit code to\n * `address`. Returns the pending state to persist until the owner supplies the code.\n */\nexport async function requestEmailProofCode(input: EmailProofRequestInput): Promise<EmailProofPending> {\n const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;\n profilePin(profile, \"request\");\n const address = normalizeAddress(input.address);\n assertHolder(input.holder, \"request\");\n const now = input.now ?? (() => new Date());\n const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);\n const requirement = emailRequirement(address, profile);\n const requirementDigest = await credentialRequirementDigest(requirement);\n const descriptor = await witnessDescriptor(profile, requirement, { fetch: input.fetch, signal }, \"request\");\n const descriptorDigest = await canonicalDigest(descriptor);\n const verifier = randomVerifier();\n const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });\n\n const createdAtMs = now().getTime();\n let created: Awaited<ReturnType<OpenCredentialsHttpTransport[\"create\"]>>;\n try {\n created = await transport.create({\n descriptor,\n descriptorDigest,\n requirement,\n requirementDigest,\n holderDid: input.holder.did,\n openerOrigin: OPENER_ORIGIN,\n completionVerifierChallenge: await sha256Base64Url(verifier),\n signal,\n });\n } catch (error) {\n throw mapError(error, \"request\", signal);\n }\n const serverExpiry = Date.parse(created.expiresAt);\n if (!Number.isFinite(serverExpiry)) throw new EmailProofError(\"request\", \"FAILED\");\n const pending = restoreEmailProofPending({\n type: \"TinyCloudEmailProofPending\",\n version: 1,\n profile,\n address,\n requestId: created.requestId,\n verifier,\n holderDid: input.holder.did,\n descriptorDigest,\n requirementDigest,\n openerOrigin: OPENER_ORIGIN,\n createdAt: new Date(createdAtMs).toISOString(),\n expiresAt: new Date(Math.min(serverExpiry, createdAtMs + EMAIL_PROOF_PENDING_MAX_MS)).toISOString(),\n });\n\n try {\n await interpretCredentialFlow({\n descriptor,\n requirement,\n requestId: created.requestId,\n verifier,\n holderDid: input.holder.did,\n descriptorDigest,\n requirementDigest,\n openerOrigin: OPENER_ORIGIN,\n transport,\n signing: holderSigning(input.holder),\n handlers: { mailbox_otp: pauseForCode },\n signal,\n });\n } catch (error) {\n if (error instanceof CodeRequested) return pending;\n throw mapError(error, \"request\", signal);\n }\n throw new EmailProofError(\"request\", \"FAILED\");\n}\n\n/**\n * Step 2: submit the owner's 8-digit code, sign the holder binding, issue, and\n * verify. The caller must delete `pending` after this call, success or failure.\n */\nexport async function completeEmailProof(input: EmailProofOptions & { readonly pending: EmailProofPending; readonly code: string; readonly holder: HolderSigner }): Promise<EmailProofResult> {\n return completeWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);\n}\n\n/**\n * Internal test seam: completes with caller-supplied issuer keys. Not re-exported\n * from the package barrel. Production code uses `completeEmailProof`.\n */\nexport async function completeEmailProofWithIssuerKeys(\n input: EmailProofOptions & { readonly pending: EmailProofPending; readonly code: string; readonly holder: HolderSigner },\n issuerKeys: readonly IssuerVerificationKey[],\n): Promise<EmailProofResult> {\n return completeWithIssuerKeys(input, issuerKeys);\n}\n\nasync function completeWithIssuerKeys(\n input: EmailProofOptions & { readonly pending: EmailProofPending; readonly code: string; readonly holder: HolderSigner },\n issuerKeys: readonly IssuerVerificationKey[],\n): Promise<EmailProofResult> {\n const { pending } = input;\n const now = input.now ?? (() => new Date());\n if (!isPending(pending)) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n if (pendingDeadlineMs(pending) <= now().getTime()) throw new EmailProofError(\"session\", \"SESSION_EXPIRED\");\n if (!/^\\d{8}$/.test(input.code)) throw new EmailProofError(\"code\", \"CODE_INVALID\");\n assertHolder(input.holder, \"code\");\n if (input.holder.did !== pending.holderDid) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);\n\n const requirement = emailRequirement(pending.address, pending.profile);\n if (await credentialRequirementDigest(requirement) !== pending.requirementDigest) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n const descriptor = await witnessDescriptor(pending.profile, requirement, { fetch: input.fetch, signal }, \"session\");\n const descriptorDigest = await canonicalDigest(descriptor);\n if (descriptorDigest !== pending.descriptorDigest) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });\n\n try {\n // The code email was sent in step 1. A challenge-required state here would make\n // the interpreter resend it, so refuse instead.\n const state = await transport.state(pending.requestId, pending.verifier, signal);\n if (state.nextStep?.constraints.challengeRequired === true) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n await interpretCredentialFlow({\n descriptor,\n requirement,\n requestId: pending.requestId,\n verifier: pending.verifier,\n holderDid: pending.holderDid,\n descriptorDigest,\n requirementDigest: pending.requirementDigest,\n openerOrigin: pending.openerOrigin,\n transport,\n signing: holderSigning(input.holder),\n handlers: { mailbox_otp: async () => ({ otp: input.code }) },\n signal,\n });\n } catch (error) {\n throw mapError(error, \"code\", signal);\n }\n\n try {\n const envelope = await transport.result(pending.requestId, pending.verifier, signal);\n const verified = await verifyIssuedCredential({\n envelope,\n descriptor,\n descriptorDigest,\n requirement,\n holderDid: pending.holderDid,\n issuerMetadata: pinnedIssuerMetadata(issuerKeys),\n now: now(),\n checkStatus: (status, statusSignal) => transport.checkStatus(status, statusSignal),\n signal,\n });\n return { envelope, verified, credentialId: verified.credentialId, issuedAt: verified.issuedAt, expiresAt: verified.expiresAt, descriptor };\n } catch (error) {\n throw mapError(error, \"credential\", signal);\n }\n}\n\n/**\n * Request a code and, when `getCode` is given, complete the flow in one call.\n * With a `pendingStore`, an unexpired request for the address is resumed instead of\n * requesting a new code. A non-recoverable failure deletes the pending request.\n *\n * `store.put` throwing after the code was mailed: the code is in the owner's inbox but\n * not persisted, so the error propagates and the owner must request a new code.\n * `store.delete` failing after success: the credential is returned with a warning.\n * `store.delete` failing on a non-recoverable failure: the original error propagates.\n */\nexport async function acquireEmailProof(input: EmailProofAcquireInput): Promise<EmailProofAcquisition> {\n return acquireWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);\n}\n\n/** Internal test seam, same as `completeEmailProofWithIssuerKeys`. Not re-exported from the barrel. */\nexport async function acquireEmailProofWithIssuerKeys(input: EmailProofAcquireInput, issuerKeys: readonly IssuerVerificationKey[]): Promise<EmailProofAcquisition> {\n return acquireWithIssuerKeys(input, issuerKeys);\n}\n\nasync function acquireWithIssuerKeys(input: EmailProofAcquireInput, issuerKeys: readonly IssuerVerificationKey[]): Promise<EmailProofAcquisition> {\n const address = normalizeAddress(input.address);\n const now = input.now ?? (() => new Date());\n const store = input.pendingStore;\n const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;\n const live = store ? await store.findLive(address, now()) : undefined;\n // The store returns the newest live record per address across profiles. Resume only one of this profile.\n let pending = live?.profile === profile ? live : undefined;\n if (!pending) {\n pending = await requestEmailProofCode({ ...input, address });\n await store?.put(pending);\n }\n const shown = restoreEmailProofPending(pending);\n if (!input.getCode) return { status: \"code-requested\", pending: shown };\n\n const code = await input.getCode({ pending: shown });\n try {\n const result = await completeWithIssuerKeys({ ...input, pending, code }, issuerKeys);\n const warnings: string[] = [];\n if (store) {\n try { await store.delete(pending.requestId); } catch { warnings.push(\"The pending request could not be deleted after success; it expires at its deadline.\"); }\n }\n return { status: \"issued\", result, warnings };\n } catch (error) {\n if (!(error instanceof EmailProofError && error.recoverable)) await store?.delete(pending.requestId).catch(() => undefined);\n throw error;\n }\n}\n\nfunction normalizeAddress(address: string): string {\n const value = address.trim();\n if (value.length > 254 || !EMAIL_PATTERN.test(value)) throw new EmailProofError(\"request\", \"ADDRESS_INVALID\");\n return value;\n}\n\nfunction holderKid(did: string): string {\n return `${did}#${did.slice(HOLDER_DID_PREFIX.length)}`;\n}\n\nfunction assertHolder(holder: HolderSigner, step: EmailProofStep): void {\n if (!HOLDER_DID_PATTERN.test(holder.did) || holder.alg !== \"EdDSA\" || holder.kid !== holderKid(holder.did)) throw new EmailProofError(step, \"HOLDER_INVALID\");\n}\n\nfunction profilePin(profile: string, step: EmailProofStep): (typeof PROFILE_PINS)[EmailProofProfileId] {\n const entry = Object.prototype.hasOwnProperty.call(PROFILE_PINS, profile) ? PROFILE_PINS[profile as EmailProofProfileId] : undefined;\n if (!entry || !entry.enabled) throw new EmailProofError(step, \"PROFILE_UNAVAILABLE\");\n return entry;\n}\n\nfunction emailRequirement(address: string, profile: EmailProofProfileId): CredentialRequirement {\n return createEmailCredentialRequirement({ email: address, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE });\n}\n\n/** Requirement for an email credential issued to `address` under the default profile. Storage and `show` key on its digest. */\nexport function emailProofRequirement(address: string): CredentialRequirement {\n return emailRequirement(normalizeAddress(address), DEFAULT_EMAIL_PROOF_PROFILE);\n}\n\n/** Discovers the catalog and returns the descriptor only if it matches the pinned issuer, origin, step, and digest. */\nasync function witnessDescriptor(\n profile: EmailProofProfileId,\n requirement: CredentialRequirement,\n options: { readonly fetch?: typeof fetch; readonly signal: AbortSignal },\n step: EmailProofStep,\n): Promise<CredentialFlowDescriptor> {\n const pin = profilePin(profile, step);\n let descriptor: CredentialFlowDescriptor;\n try {\n descriptor = await discoverCatalogDescriptor(requirement, DISCOVERY_URL, { fetch: options.fetch, signal: options.signal });\n } catch (error) {\n throw discoveryFailure(error, options.signal, step);\n }\n if (descriptor.issuer.origin !== EMAIL_PROOF_PINS.witnessOrigin || descriptor.issuer.did !== EMAIL_PROOF_PINS.issuerDid || !descriptor.steps.some((candidate) => candidate.type === pin.requiredStep)) {\n throw new EmailProofError(step, \"PROFILE_UNAVAILABLE\");\n }\n if (await canonicalDigest(descriptor) !== pin.descriptorDigest) throw new EmailProofError(step, \"PROFILE_UNAVAILABLE\");\n return descriptor;\n}\n\n/**\n * Fetch an OpenCredentials catalog and return the first enabled, ready profile whose\n * descriptor satisfies the requirement and matches its catalog digest.\n */\nasync function discoverCatalogDescriptor(\n requirement: CredentialRequirement,\n discoveryUrl: string,\n options: { readonly fetch?: typeof fetch; readonly signal?: AbortSignal } = {},\n): Promise<CredentialFlowDescriptor> {\n let url: URL; try { url = new URL(discoveryUrl); } catch { throw new CredentialError(\"UNSUPPORTED_PROFILE\", \"Discovery URL is invalid\"); }\n if (url.protocol !== \"https:\" || ![\"/.well-known/opencredentials\", \"/v1/credential-types\"].includes(url.pathname) || url.search || url.hash) throw new CredentialError(\"UNSUPPORTED_PROFILE\", \"Discovery URL is not an allowed catalog endpoint\");\n const init: RequestInit & { readonly cache: \"no-store\" } = { credentials: \"omit\", cache: \"no-store\", redirect: \"error\", referrerPolicy: \"no-referrer\", signal: options.signal };\n let response: Response;\n try { response = await (options.fetch ?? globalThis.fetch.bind(globalThis))(url, init); } catch (cause) { throw new CredentialError(\"OFFLINE\", \"Credential discovery is unavailable\", { cause }); }\n if (!response.ok) throw new CredentialError(response.status === 503 ? \"ISSUER_UNREADY\" : \"OFFLINE\", \"Credential discovery is unavailable\");\n const catalog = await response.json() as Record<string, unknown>;\n if (catalog.type !== \"tinycloud.credentials/catalog/v1\" || catalog.protocol !== \"tinycloud.credentials/acquisition/v1\" || catalog.catalogVersion !== 1 || !Array.isArray(catalog.profiles)) throw new CredentialError(\"DESCRIPTOR_INVALID\", \"Credential catalog is invalid\");\n for (const entryValue of catalog.profiles) {\n if (typeof entryValue !== \"object\" || entryValue === null || Array.isArray(entryValue)) continue;\n const entry = entryValue as Record<string, unknown>;\n if (entry.supported !== true || entry.enabled !== true || (entry.readiness !== \"ready\" && entry.readiness !== \"degraded\")) continue;\n try {\n const candidate = validateCredentialFlowDescriptor(entry.descriptor);\n if (descriptorSatisfiesRequirement(candidate, requirement) && entry.descriptorDigest === await canonicalDigest(candidate)) return candidate;\n } catch { /* malformed catalog entry: skip */ }\n }\n throw new CredentialError(\"UNSUPPORTED_PROFILE\", \"No available credential profile satisfies the requirement\");\n}\n\n/** A discovery abort by the caller is CANCELED. A step timeout stays NETWORK (recoverable). */\nfunction discoveryFailure(error: unknown, signal: AbortSignal, step: EmailProofStep): EmailProofError {\n if (signal.aborted && (signal.reason as { name?: string } | undefined)?.name !== \"TimeoutError\") return new EmailProofError(step, \"CANCELED\");\n return mapError(error, step, signal);\n}\n\nfunction pinnedIssuerMetadata(keys: readonly IssuerVerificationKey[]): CredentialIssuerMetadata {\n return { type: \"OpenCredentialsIssuerMetadata\", version: 1, origin: EMAIL_PROOF_PINS.witnessOrigin, issuerDid: EMAIL_PROOF_PINS.issuerDid, keys, cache: { maxAgeSeconds: 3600, etag: \"pinned\" } };\n}\n\n/** A stored email-proof credential plus the descriptor it was issued under. Read back from disk, so untrusted until verified. */\nexport type StoredEmailProofRecord = StoredCredentialRecord & {\n readonly subjectDid: string;\n readonly descriptor: CredentialFlowDescriptor;\n};\n\n/** Input to `verifyStoredEmailProof`. */\nexport type StoredEmailProofInput = {\n readonly record: StoredEmailProofRecord;\n readonly requirement: CredentialRequirement;\n readonly holderDid: string;\n readonly now: Date;\n readonly signal?: AbortSignal;\n};\n\n/**\n * Offline re-verification of a stored email-proof credential against the pinned issuer key.\n * Rebuilds the envelope from the record, runs the issuance verifier, then cross-checks the\n * record fields the signature does not cover. Every failure throws CredentialError.\n */\nexport async function verifyStoredEmailProof(input: StoredEmailProofInput): Promise<VerifiedCredential> {\n return verifyStoredWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);\n}\n\n/** Internal test seam, same as `acquireEmailProofWithIssuerKeys`. Not re-exported from the barrel. */\nexport async function verifyStoredWithIssuerKeys(input: StoredEmailProofInput, issuerKeys: readonly IssuerVerificationKey[]): Promise<VerifiedCredential> {\n const { record, requirement, holderDid } = input;\n const descriptor = record.descriptor;\n if (record.descriptorDigest !== EMAIL_PROOF_PINS.descriptorDigest || await canonicalDigest(descriptor) !== EMAIL_PROOF_PINS.descriptorDigest) {\n throw new CredentialError(\"VERIFICATION_FAILED\", \"stored descriptor does not match the pinned descriptor digest\");\n }\n const envelope: IssuedCredentialEnvelope = {\n type: \"OpenCredentialsIssuedCredential\",\n version: 1,\n protocol: descriptor.protocol,\n profile: record.profile,\n credentialType: record.credentialType,\n schema: record.schema,\n format: \"vc+sd-jwt\",\n issuerDid: record.issuerDid,\n issuerKid: record.issuerKid,\n subjectDid: record.subjectDid,\n holderDid: record.holderDid,\n claims: record.claims,\n claimsDigest: record.claimsDigest,\n descriptorDigest: record.descriptorDigest,\n credentialId: record.credentialId,\n issuedAt: record.issuedAt,\n notBefore: record.notBefore,\n expiresAt: record.expiresAt,\n status: record.status,\n credential: record.credential,\n };\n const verified = await verifyIssuedCredential({\n envelope,\n descriptor,\n descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest,\n requirement,\n holderDid,\n issuerMetadata: pinnedIssuerMetadata(issuerKeys),\n now: input.now,\n checkStatus: async (status) => status.method === \"none\" && status.freshnessSeconds === descriptor.status.freshnessSeconds,\n signal: input.signal,\n });\n // Fields outside the signed envelope: each must agree with what the verifier just proved.\n if (record.holderDid !== holderDid || record.ownerDid !== holderDid) throw new CredentialError(\"HOLDER_MISMATCH\", \"stored record belongs to another holder\");\n if (record.credentialDigest !== verified.credentialDigest) throw new CredentialError(\"VERIFICATION_FAILED\", \"stored credential digest does not match the verified credential\");\n if (record.requirementDigest !== await credentialRequirementDigest(requirement)) throw new CredentialError(\"VERIFICATION_FAILED\", \"stored requirement digest does not match the requirement\");\n if (record.recordId !== await canonicalDigest({ credentialDigest: verified.credentialDigest, holderDid: record.ownerDid })) throw new CredentialError(\"VERIFICATION_FAILED\", \"stored record id does not match its credential\");\n return verified;\n}\n\n/** Holder signing: the signer receives the raw binding bytes and returns a raw Ed25519 signature. The kid goes on the wire. */\nfunction holderSigning(holder: HolderSigner): CredentialSigningAdapter {\n return { autoSign: async (_binding, bytes) => holder.sign(bytes), holderKey: { alg: \"EdDSA\", kid: holder.kid } };\n}\n\nfunction randomVerifier(): string {\n return encodeBase64Url(crypto.getRandomValues(new Uint8Array(32)));\n}\n\nfunction isPending(value: unknown): value is EmailProofPending {\n if (typeof value !== \"object\" || value === null) return false;\n const pending = value as EmailProofPending;\n return pending.type === \"TinyCloudEmailProofPending\"\n && pending.version === 1\n && pending.openerOrigin === OPENER_ORIGIN\n && [pending.profile, pending.address, pending.requestId, pending.verifier, pending.holderDid, pending.descriptorDigest, pending.requirementDigest, pending.openerOrigin, pending.createdAt, pending.expiresAt].every((field) => typeof field === \"string\")\n && Number.isFinite(Date.parse(pending.createdAt))\n && Number.isFinite(Date.parse(pending.expiresAt));\n}\n\n/** Maps any failure to a stable code. Caller aborts map to CANCELED; transient network failures stay recoverable. */\nfunction mapError(value: unknown, step: EmailProofStep, signal?: AbortSignal): EmailProofError {\n if (value instanceof EmailProofError) return value;\n if (value instanceof DOMException && value.name === \"TimeoutError\") return new EmailProofError(step, \"NETWORK\");\n const error = credentialError(value);\n const details = error.details;\n switch (error.code) {\n case \"ISSUER_UNREADY\":\n return details.state === \"rate_limited\"\n ? new EmailProofError(step, \"RATE_LIMITED\", details.retryAfterMs, error.recoverable)\n : new EmailProofError(step, \"ISSUER_UNAVAILABLE\", undefined, error.recoverable);\n case \"OFFLINE\":\n if (signal?.aborted && (signal.reason as { name?: string } | undefined)?.name !== \"TimeoutError\") return new EmailProofError(step, \"CANCELED\");\n return new EmailProofError(step, \"NETWORK\");\n case \"UNSUPPORTED_PROFILE\":\n case \"DESCRIPTOR_INVALID\":\n return new EmailProofError(step, \"PROFILE_UNAVAILABLE\");\n case \"PROOF_REJECTED\":\n // The transport emits PROOF_REJECTED only for a server recoverable:true response.\n return new EmailProofError(\"code\", \"CODE_REJECTED\", undefined, true);\n case \"VERIFICATION_FAILED\":\n if (details.state === \"proof_attempts_exhausted\") return new EmailProofError(\"code\", \"CODE_ATTEMPTS_EXHAUSTED\");\n if (step === \"credential\") return new EmailProofError(\"credential\", \"CREDENTIAL_INVALID\");\n // Unclassified rejection at the code step: not recoverable, so the pending is deleted.\n if (step === \"code\") return new EmailProofError(\"code\", \"CODE_REJECTED\", undefined, false);\n return new EmailProofError(step, \"FAILED\");\n case \"HOLDER_MISMATCH\":\n return new EmailProofError(\"credential\", \"CREDENTIAL_INVALID\");\n case \"REQUEST_EXPIRED\":\n return new EmailProofError(\"session\", \"SESSION_EXPIRED\");\n case \"REQUEST_SUBSTITUTED\":\n return new EmailProofError(\"session\", \"SESSION_INVALID\");\n case \"SIGNATURE_REJECTED\":\n return new EmailProofError(\"credential\", \"SIGNATURE_REJECTED\", undefined, error.recoverable);\n case \"CANCELED\":\n return new EmailProofError(step, \"CANCELED\");\n default:\n return new EmailProofError(step, \"FAILED\");\n }\n}\n","import {\n CredentialError,\n encodeBase64Url,\n holderBindingSigningBytes,\n validateCredentialHolderBinding,\n type CredentialFlowDescriptor,\n type CredentialProgressEvent,\n type CredentialRequirement,\n} from \"@tinycloud/sdk-core\";\nimport type { CredentialAcquisitionTransport, CredentialProofSubject, CredentialSigningAdapter, InlineCredentialProofHandler, InlineCredentialProofRequest, PrimitiveStepHandler } from \"./types\";\n\nconst delay = (ms: number, signal?: AbortSignal) => new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\"abort\", () => { clearTimeout(timer); reject(new DOMException(\"Aborted\", \"AbortError\")); }, { once: true });\n});\n\n/** The mailbox named by an email requirement, which the issuer delivers the code to. */\nexport function mailboxSubject(requirement: CredentialRequirement): CredentialProofSubject | undefined {\n const email = requirement.claims.email;\n return typeof email === \"string\" && email.length > 0 ? Object.freeze({ kind: \"email\" as const, value: email }) : undefined;\n}\n\n/** Finite step interpreter. Dispatch is exclusively by registered primitive and version. */\nexport async function interpretCredentialFlow(input: {\n readonly descriptor: CredentialFlowDescriptor;\n readonly requirement: CredentialRequirement;\n readonly requestId: string;\n readonly verifier: string;\n readonly holderDid: string;\n readonly descriptorDigest: string;\n readonly requirementDigest: string;\n readonly openerOrigin: string;\n readonly transport: CredentialAcquisitionTransport;\n readonly signing: CredentialSigningAdapter;\n /**\n * Fallback for a host-owned surface. It receives every declared proof step,\n * including primitives added by a later protocol version.\n */\n readonly proofHandler?: InlineCredentialProofHandler;\n readonly handlers?: Partial<Record<\"collect_input\" | \"mailbox_otp\", PrimitiveStepHandler>>;\n readonly signal?: AbortSignal;\n readonly onProgress?: (event: CredentialProgressEvent) => void;\n readonly onWait?: () => Promise<void>;\n}): Promise<void> {\n const steps = new Map(input.descriptor.steps.map((step) => [step.type, step]));\n const completed = new Set<string>(); let lastTransition = \"\";\n for (;;) {\n input.signal?.throwIfAborted();\n const state = await input.transport.state(input.requestId, input.verifier, input.signal);\n if (state.requestId !== input.requestId) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential request identity changed\");\n if (state.state === \"expired\") throw new CredentialError(\"REQUEST_EXPIRED\", \"Credential request expired\", { state: state.state, correlationId: state.correlationId });\n if (state.state === \"canceled\") throw new CredentialError(\"CANCELED\", \"Credential request was canceled\", { state: state.state, correlationId: state.correlationId });\n if (state.state === \"issuer_unready\") throw new CredentialError(\"ISSUER_UNREADY\", \"Credential issuer is not ready\", { retryAfterMs: state.retryAfterMs, state: state.state, correlationId: state.correlationId });\n if (state.state === \"complete\") return;\n if (state.state === \"ready_to_issue\" || state.state === \"issued\") {\n if (state.state === \"ready_to_issue\" && !completed.has(\"issue\")) { await input.transport.issue(input.requestId, input.verifier, input.signal); completed.add(\"issue\"); }\n else await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal));\n continue;\n }\n const next = state.nextStep;\n if (!next) { await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal)); continue; }\n const declared = steps.get(next.type);\n if (next.id !== next.type || !declared || declared.type !== next.type || declared.version !== next.version) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Server selected an undeclared credential step\");\n if (completed.has(next.id) || state.transitionId === lastTransition) { await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal)); continue; }\n lastTransition = state.transitionId;\n if (next.type === \"holder_signature\") {\n input.onProgress?.({ state: \"signing\", stepId: next.id, correlationId: state.correlationId });\n const binding = validateCredentialHolderBinding(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));\n if (binding.requestId !== input.requestId || binding.descriptorDigest !== input.descriptorDigest || binding.requirementDigest !== input.requirementDigest || binding.holderDid !== input.holderDid || binding.issuer !== input.descriptor.issuer.did || binding.issuerKid !== input.descriptor.issuer.kid || binding.profile !== input.descriptor.profile || binding.openerOrigin !== input.openerOrigin || binding.completionOrigin !== input.openerOrigin || binding.audience !== \"tinycloud://credentials\") throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding was substituted\");\n const bytes = holderBindingSigningBytes(binding);\n let signature = await input.signing.autoSign?.(binding, bytes, input.signal);\n if (signature === undefined) {\n if (!input.signing.requestApproval) throw new CredentialError(\"SIGNATURE_REJECTED\", \"OpenKey approval is required\");\n try { signature = await input.signing.requestApproval(binding, bytes, input.signal); } catch (cause) { if (cause instanceof CredentialError) throw cause; throw new CredentialError(\"SIGNATURE_REJECTED\", \"OpenKey approval was rejected\", { cause }); }\n }\n if (!(signature instanceof Uint8Array) || signature.length === 0) throw new CredentialError(\"SIGNATURE_REJECTED\", \"OpenKey signature is invalid\");\n await input.transport.submitHolderSignature(input.requestId, input.verifier, encodeBase64Url(signature), input.signal, input.signing.holderKey);\n } else {\n input.onProgress?.({ state: next.type === \"collect_input\" ? \"collecting\" : \"proving\", stepId: next.id, correlationId: state.correlationId });\n const handler = input.handlers?.[next.type];\n const inlineHandler = input.proofHandler;\n if (!handler && !inlineHandler) { await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal)); continue; }\n if (next.constraints.challengeRequired === true) await input.transport.beginStep(input.requestId, input.verifier, next.type, input.signal);\n if (handler) {\n const proof = await handler({ descriptor: input.descriptor, requirement: input.requirement, stepId: next.id, constraints: next.constraints, signal: input.signal });\n await input.transport.submitStep(input.requestId, input.verifier, next.id, proof, input.signal);\n } else {\n let feedback: InlineCredentialProofRequest[\"feedback\"];\n // A mistyped code is re-entered against the same challenge; the issuer\n // counts every attempt and reports when the bounded budget is spent.\n for (let attempt = 1; ; attempt += 1) {\n const proof = await inlineHandler!({\n stepId: next.id,\n constraints: next.constraints,\n display: {\n title: input.descriptor.display.title,\n description: input.descriptor.display.description,\n consent: input.descriptor.display.consent,\n progressLabel: input.descriptor.accessibility.progressLabel,\n errorLiveRegion: input.descriptor.accessibility.errorLiveRegion,\n },\n inputs: input.descriptor.inputs.map(({ id, label, schema }) => ({ id, label, schema })),\n ...(feedback === undefined ? {} : { feedback }),\n signal: input.signal,\n });\n try {\n await input.transport.submitStep(input.requestId, input.verifier, next.id, proof, input.signal);\n break;\n } catch (cause) {\n if (!(cause instanceof CredentialError) || cause.code !== \"PROOF_REJECTED\") throw cause;\n const attemptsRemaining = input.descriptor.lifecycle.maxProofAttempts - attempt;\n // Once this view stops re-prompting, the rejection is final: a\n // host must not silently start a new acquisition on its behalf.\n if (next.type !== \"mailbox_otp\" || attemptsRemaining <= 0) throw new CredentialError(\"VERIFICATION_FAILED\", \"The proof was not accepted\", { state: attemptsRemaining <= 0 ? \"proof_attempts_exhausted\" : \"proof_rejected\", cause });\n feedback = { kind: \"rejected\", attemptsRemaining };\n }\n }\n }\n }\n completed.add(next.id);\n }\n}\n","import {\n CredentialError, canonicalDigest, credentialEndpointPath, decodeBase64Url, encodeBase64Url,\n holderBindingCanonicalBytes, sha256Base64Url, validateCredentialHolderBinding,\n type CredentialFlowDescriptor, type CredentialHolderBinding, type CredentialIssuerMetadata,\n type CredentialRequirement, type IssuedCredentialEnvelope,\n} from \"@tinycloud/sdk-core\";\nimport type { CredentialAcquisitionTransport, CredentialRequestState, HolderSignatureKey, PrimitiveStepResult } from \"./types\";\n\nfunction object(value: unknown, label: string): Record<string, unknown> { if (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new CredentialError(\"VERIFICATION_FAILED\", `${label} is invalid`); return value as Record<string, unknown>; }\nfunction opaque(value: unknown, label: string): string { if (typeof value !== \"string\" || !/^[A-Za-z0-9_-]{16,128}$/.test(value)) throw new CredentialError(\"REQUEST_SUBSTITUTED\", `${label} is invalid`); return value; }\nfunction timestamp(value: unknown, label: string): string { if (typeof value !== \"string\" || !Number.isFinite(Date.parse(value))) throw new CredentialError(\"VERIFICATION_FAILED\", `${label} is invalid`); return new Date(value).toISOString(); }\nfunction jwtPayload(credential: string): Record<string, unknown> { const part = credential.split(\"~\", 1)[0]?.split(\".\")[1]; if (!part) throw new CredentialError(\"VERIFICATION_FAILED\", \"Issued SD-JWT is invalid\"); try { return object(JSON.parse(new TextDecoder().decode(decodeBase64Url(part))), \"SD-JWT payload\"); } catch (cause) { if (cause instanceof CredentialError) throw cause; throw new CredentialError(\"VERIFICATION_FAILED\", \"Issued SD-JWT is invalid\", { cause }); } }\nconst SERVER_CODES = new Set([\"REQUEST_EXPIRED\", \"ISSUER_UNREADY\", \"UNSUPPORTED_PROFILE\", \"UNSUPPORTED_VERSION\", \"SIGNATURE_REJECTED\"] as const);\nfunction serverError(value: unknown, response: Response, honorServerRecoverable: boolean): CredentialError | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n const body = value as Record<string, unknown>;\n const keys = Object.keys(body).sort();\n if (keys.join(\",\") !== \"code,correlationId,recoverable,state,type\" || body.type !== \"tinycloud.credentials/error/v1\" || typeof body.code !== \"string\" || typeof body.recoverable !== \"boolean\" || typeof body.state !== \"string\" || typeof body.correlationId !== \"string\") return undefined;\n // A rejected proof is retryable only while the issuer still has attempts\n // left for this challenge; once exhausted the acquisition cannot finish.\n if (body.code === \"PROOF_REJECTED\") return body.recoverable\n ? new CredentialError(\"PROOF_REJECTED\", \"The proof was not accepted\", { state: body.state, correlationId: body.correlationId })\n : new CredentialError(\"VERIFICATION_FAILED\", \"The proof attempt limit was reached\", { state: \"proof_attempts_exhausted\", correlationId: body.correlationId });\n if (body.code === \"RATE_LIMITED\") return new CredentialError(\"ISSUER_UNREADY\", \"Too many verification codes were requested\", { state: \"rate_limited\", correlationId: body.correlationId, retryAfterMs: Number(response.headers.get(\"retry-after\")) * 1000 || undefined, recoverable: honorServerRecoverable ? body.recoverable : undefined });\n if (body.code === \"CHALLENGE_EXPIRED\") return new CredentialError(\"REQUEST_EXPIRED\", \"The credential challenge expired\", { state: body.state, correlationId: body.correlationId, recoverable: honorServerRecoverable ? body.recoverable : undefined });\n if (!SERVER_CODES.has(body.code as any)) return undefined;\n const code = body.code as \"REQUEST_EXPIRED\" | \"ISSUER_UNREADY\" | \"UNSUPPORTED_PROFILE\" | \"UNSUPPORTED_VERSION\" | \"SIGNATURE_REJECTED\";\n return new CredentialError(code, `OpenCredentials rejected the request: ${code}`, {\n state: body.state,\n correlationId: body.correlationId,\n retryAfterMs: Number(response.headers.get(\"retry-after\")) * 1000 || undefined,\n recoverable: honorServerRecoverable ? body.recoverable : undefined,\n });\n}\n\nexport class OpenCredentialsHttpTransport implements CredentialAcquisitionTransport {\n private readonly nonces = new Map<string, string>();\n private readonly bindings = new Map<string, CredentialHolderBinding>();\n constructor(private readonly descriptor: CredentialFlowDescriptor, private readonly fetchFn: typeof fetch = globalThis.fetch.bind(globalThis), private readonly options: { readonly honorServerRecoverable?: boolean } = {}) {}\n private url(endpoint: Parameters<typeof credentialEndpointPath>[0], id?: string): string { return new URL(credentialEndpointPath(endpoint, id), this.descriptor.issuer.origin).href; }\n private async request(endpoint: Parameters<typeof credentialEndpointPath>[0], input: { readonly id?: string; readonly method?: \"GET\" | \"POST\"; readonly verifier?: string; readonly body?: unknown; readonly signal?: AbortSignal }): Promise<unknown> {\n let response: Response;\n try { const init: RequestInit & { readonly cache: \"no-store\" } = { method: input.method ?? \"GET\", credentials: \"omit\", cache: \"no-store\", redirect: \"error\", referrerPolicy: \"no-referrer\", signal: input.signal, headers: { accept: \"application/json\", ...(input.body === undefined ? {} : { \"content-type\": \"application/json\" }), ...(input.verifier === undefined ? {} : { authorization: `Bearer ${input.verifier}` }) }, ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }) }; response = await this.fetchFn(this.url(endpoint, input.id), init); }\n catch (cause) { if (cause instanceof DOMException && cause.name === \"AbortError\") throw new CredentialError(\"CANCELED\", \"Credential acquisition was canceled\", { cause }); throw new CredentialError(\"OFFLINE\", \"OpenCredentials is unavailable\", { cause }); }\n if (!response.ok) {\n let parsed: unknown;\n try { parsed = await response.json(); } catch { parsed = undefined; }\n const typed = serverError(parsed, response, this.options.honorServerRecoverable === true);\n if (typed) throw typed;\n if (response.status === 410) throw new CredentialError(\"REQUEST_EXPIRED\", \"Credential request expired\");\n if (response.status === 409) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential request was replayed or substituted\");\n if (response.status === 503) throw new CredentialError(\"ISSUER_UNREADY\", \"Credential issuer is not ready\", { retryAfterMs: Number(response.headers.get(\"retry-after\")) * 1000 || undefined });\n throw new CredentialError(\"VERIFICATION_FAILED\", \"OpenCredentials rejected the request\");\n }\n if (response.status === 204) return undefined; try { return await response.json(); } catch (cause) { throw new CredentialError(\"VERIFICATION_FAILED\", \"OpenCredentials response is invalid\", { cause }); }\n }\n async create(input: { descriptor: CredentialFlowDescriptor; descriptorDigest: string; requirement: CredentialRequirement; requirementDigest: string; holderDid: string; openerOrigin: string; completionVerifierChallenge: string; inputs?: Readonly<Record<string, string>>; signal?: AbortSignal }) {\n const body = object(await this.request(\"request\", { method: \"POST\", body: { protocol: input.descriptor.protocol, profile: input.descriptor.profile, profileVersion: input.descriptor.profileVersion, descriptorDigest: input.descriptorDigest, requirementDigest: input.requirementDigest, holderDid: input.holderDid, inputs: input.inputs ?? input.requirement.claims, audience: \"tinycloud://credentials\", openerOrigin: input.openerOrigin, completionOrigin: input.openerOrigin, completionContext: \"sdk-acquisition\", completionVerifierChallenge: input.completionVerifierChallenge }, signal: input.signal }), \"create response\");\n if (body.type !== \"tinycloud.credentials/acquisition-request/v1\" || body.protocol !== input.descriptor.protocol || body.next !== \"challenge\" || body.endpoint !== \"challenge\") throw new CredentialError(\"VERIFICATION_FAILED\", \"create response is invalid\");\n const requestId = opaque(body.requestId, \"requestId\"); return { requestId, locator: requestId, expiresAt: timestamp(body.expiresAt, \"expiresAt\"), correlationId: requestId };\n }\n private proofStep(): \"mailbox_otp\" | \"collect_input\" { return this.descriptor.steps.some((step) => step.type === \"mailbox_otp\") ? \"mailbox_otp\" : \"collect_input\"; }\n async state(id: string, verifier: string, signal?: AbortSignal): Promise<CredentialRequestState> {\n const raw = object(await this.request(\"state\", { id, verifier, signal }), \"state response\");\n if (raw.type !== \"tinycloud.credentials/acquisition-state/v1\" || raw.requestId !== id || raw.profile !== this.descriptor.profile || raw.profileVersion !== 1) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential state response is substituted\");\n const base = { type: \"OpenCredentialsAcquisitionState\" as const, version: 1 as const, requestId: id, transitionId: `${String(raw.state)}:${String(raw.resultPreviouslyRead ?? false)}`, correlationId: id };\n if (raw.state === \"challenge_required\") { const type = this.proofStep(); return { ...base, state: \"pending\", nextStep: { id: type, type, version: 1, constraints: { challengeRequired: true } } }; }\n if (raw.state === \"proof_required\") { const type = this.proofStep(); this.nonces.set(id, opaque(raw.challengeNonce, \"challengeNonce\")); return { ...base, state: \"pending\", nextStep: { id: type, type, version: 1, constraints: {} } }; }\n if (raw.state === \"holder_binding_required\" || raw.state === \"holder_signature_required\") return { ...base, state: \"pending\", nextStep: { id: \"holder_signature\", type: \"holder_signature\", version: 1, constraints: {} } };\n if (raw.state === \"ready_to_issue\") return { ...base, state: \"ready_to_issue\" };\n if (raw.state === \"issued\") return { ...base, state: \"complete\" };\n throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential state is unsupported\");\n }\n async beginStep(id: string, verifier: string, stepId: \"collect_input\" | \"mailbox_otp\", signal?: AbortSignal): Promise<void> { const step = this.proofStep(); if (stepId !== step) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential challenge step is substituted\"); const challenge = object(await this.request(\"challenge\", { id, verifier, method: \"POST\", body: { step, stepVersion: 1 }, signal }), \"challenge response\"); if (challenge.type !== \"tinycloud.credentials/challenge/v1\" || challenge.step !== step || challenge.stepVersion !== 1) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential challenge is substituted\"); this.nonces.set(id, opaque(challenge.challengeNonce, \"challengeNonce\")); }\n async submitStep(id: string, verifier: string, stepId: string, proof: PrimitiveStepResult, signal?: AbortSignal): Promise<void> { const step = this.proofStep(); if (stepId !== step) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential proof step is substituted\"); const nonce = this.nonces.get(id); if (!nonce) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential challenge is missing\"); await this.request(\"proof\", { id, verifier, method: \"POST\", body: { step, stepVersion: 1, challengeNonce: nonce, proof }, signal }); }\n async holderBinding(id: string, verifier: string, signal?: AbortSignal): Promise<CredentialHolderBinding> { const raw = object(await this.request(\"holder_binding\", { id, verifier, signal }), \"holder binding response\"); if (raw.type !== \"tinycloud.credentials/holder-binding-request/v1\" || raw.signingDomain !== \"tinycloud.credentials/holder-binding/v1\" || raw.alg !== \"EdDSA\" || typeof raw.canonical !== \"string\") throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding response is invalid\"); const binding = validateCredentialHolderBinding(raw.binding); if (encodeBase64Url(holderBindingCanonicalBytes(binding)) !== raw.canonical) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding canonical bytes were substituted\"); this.bindings.set(id, binding); return binding; }\n async submitHolderSignature(id: string, verifier: string, signature: string, signal?: AbortSignal, key?: HolderSignatureKey): Promise<void> { const binding = this.bindings.get(id); if (!binding) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding was not retrieved\"); const kid = key?.kid ?? `${binding.holderDid}#${binding.holderDid.replace(\"did:key:\", \"\")}`; await this.request(\"holder_signature\", { id, verifier, method: \"POST\", body: { alg: key?.alg ?? \"EdDSA\", kid, signature }, signal }); }\n async issue(id: string, verifier: string, signal?: AbortSignal): Promise<void> { await this.request(\"issue\", { id, verifier, method: \"POST\", signal }); }\n async result(id: string, verifier: string, signal?: AbortSignal): Promise<IssuedCredentialEnvelope> {\n const raw = object(await this.request(\"result\", { id, verifier, signal }), \"result response\"); if (raw.type !== \"tinycloud.credentials/result/v1\" || raw.protocol !== this.descriptor.protocol || raw.profile !== this.descriptor.profile || raw.profileVersion !== 1 || raw.format !== \"vc+sd-jwt\" || raw.descriptorDigest !== await canonicalDigest(this.descriptor) || raw.issuer !== this.descriptor.issuer.did || raw.issuerKid !== this.descriptor.issuer.kid || typeof raw.credential !== \"string\" || await sha256Base64Url(raw.credential) !== raw.credentialDigest) throw new CredentialError(\"VERIFICATION_FAILED\", \"Issued result metadata is invalid\");\n const holder = object(raw.holderBinding, \"result holder binding\"); if (raw.subject !== holder.did || holder.signingDomain !== \"tinycloud.credentials/holder-binding/v1\") throw new CredentialError(\"HOLDER_MISMATCH\", \"Issued holder binding is invalid\");\n const claimsRaw = object(raw.claims, \"result claims\"); const claims: Record<string, string> = {}; for (const [name, value] of Object.entries(claimsRaw)) { if (typeof value !== \"string\") throw new CredentialError(\"VERIFICATION_FAILED\", \"Issued claims are invalid\"); claims[name] = value; }\n const validity = object(raw.validity, \"result validity\"); const payload = jwtPayload(raw.credential); if (typeof payload.jti !== \"string\") throw new CredentialError(\"VERIFICATION_FAILED\", \"Credential id is invalid\"); const issuedAt = timestamp(validity.issuedAt, \"issuedAt\"); const expiresAt = timestamp(validity.expiresAt, \"expiresAt\");\n const status = object(raw.status, \"result status\"); if (status.type !== \"none\" || status.freshnessSeconds !== this.descriptor.status.freshnessSeconds) throw new CredentialError(\"VERIFICATION_FAILED\", \"Credential status is invalid\");\n return { type: \"OpenCredentialsIssuedCredential\", version: 1, protocol: this.descriptor.protocol, profile: { id: this.descriptor.profile, version: 1 }, credentialType: { id: this.descriptor.format.vct, version: 1 }, schema: this.descriptor.format.vct, format: \"vc+sd-jwt\", issuerDid: raw.issuer as string, issuerKid: raw.issuerKid as string, subjectDid: raw.subject as string, holderDid: raw.subject as string, claims, claimsDigest: await canonicalDigest(claims), descriptorDigest: raw.descriptorDigest as string, credentialId: payload.jti, issuedAt, notBefore: issuedAt, expiresAt, status: { method: \"none\", freshnessSeconds: status.freshnessSeconds as number }, credential: raw.credential };\n }\n async issuerMetadata(signal?: AbortSignal): Promise<CredentialIssuerMetadata> { return await this.request(\"issuer_metadata\", { signal }) as CredentialIssuerMetadata; }\n async checkStatus(status: IssuedCredentialEnvelope[\"status\"], _signal?: AbortSignal): Promise<boolean> { return status.method === \"none\" && status.freshnessSeconds === this.descriptor.status.freshnessSeconds; }\n}\n"],"mappings":"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","names":["import_sdk_core","import_sdk_core"]}
|
|
1
|
+
{"version":3,"sources":["../src/credential-pending-store.ts","../src/credentials/email-proof.ts","../src/credentials/interpreter.ts","../src/credentials/held-binding.ts","../src/credentials/pins.ts","../src/credentials/transport.ts"],"sourcesContent":["import { createHash, randomBytes } from 'node:crypto';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport {\n EMAIL_PROOF_PENDING_MAX_MS,\n pendingDeadlineMs,\n restoreEmailProofPending,\n type EmailProofPending,\n type EmailProofPendingStore,\n} from './credentials/email-proof';\n\n/**\n * File-backed pending store for the email-proof flow (`@tinycloud/node-sdk/credential-pending-store`).\n *\n * One file per request, named `pending-<sha256(requestId)>.json`. No file name contains a mailbox\n * address. A record holds a bearer verifier and optionally a base64 secret, so the store is owner-only:\n * a 0700 directory, 0600 files, and symlink, hard-link, and owner checks on every read. A record may\n * not expire more than 15 minutes after its `createdAt`; records expire on read.\n * Only a symlinked final `home` is refused. Ancestors may be symlinks; the resolved `home` must be owned by the current user.\n *\n * Unsafe files throw and are left in place. Malformed or over-long files are deleted and throw.\n * One unsafe file therefore makes `findLive` throw for every address until the file is removed.\n */\n\nexport type PendingStoreErrorCode = 'credential-permissions' | 'credential-store-invalid';\n\nexport class PendingStoreError extends Error {\n readonly code: PendingStoreErrorCode;\n\n constructor(code: PendingStoreErrorCode, message: string) {\n super(message);\n this.name = 'PendingStoreError';\n this.code = code;\n }\n}\n\nexport interface FileEmailProofPendingStoreOptions {\n /** Absolute profile directory. Pending files live under `<home>/credentials`. */\n readonly home: string;\n}\n\nexport interface PutEmailProofPendingOptions {\n /** Optional caller secret, stored base64 in the owner-only file. Read it back with `storedSecret`. Never serialized. */\n readonly secret?: Uint8Array;\n}\n\nexport interface FileEmailProofPendingStore extends EmailProofPendingStore {\n get(requestId: string, now: Date): Promise<EmailProofPending | undefined>;\n put(pending: EmailProofPending, options?: PutEmailProofPendingOptions): Promise<void>;\n}\n\n/** The secret stored with a pending record, or undefined. Returns a copy; the record keeps its own. */\nexport function storedSecret(pending: EmailProofPending): Uint8Array | undefined {\n const secret = secrets.get(pending);\n return secret === undefined ? undefined : new Uint8Array(secret);\n}\n\nconst DIR_NAME = 'credentials';\nconst PENDING_FILE_PATTERN = /^pending-[0-9a-f]{64}\\.json$/;\n/** Temp files from `writeOwnerOnly`: `<pending file>.<pid>.<12 hex>.tmp`. */\nconst PENDING_TEMP_PATTERN = /^pending-[0-9a-f]{64}\\.json\\.\\d+\\.[0-9a-f]{12}\\.tmp$/;\nconst BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;\nconst FILE_TEXT_FIELDS = [\n 'profile',\n 'address',\n 'requestId',\n 'verifier',\n 'holderDid',\n 'descriptorDigest',\n 'requirementDigest',\n 'openerOrigin',\n 'createdAt',\n 'expiresAt',\n] as const;\n\nconst HELD_PROFILE_ID = 'tinycloud.email-held/v1';\n\ninterface FileRecord {\n readonly type: 'TinyCloudEmailProofPending';\n readonly version: 1;\n readonly profile: string;\n readonly address: string;\n readonly requestId: string;\n readonly verifier: string;\n readonly holderDid: string;\n readonly descriptorDigest: string;\n readonly requirementDigest: string;\n readonly openerOrigin: string;\n readonly createdAt: string;\n readonly expiresAt: string;\n /** Held profile only (`tinycloud.email-held/v1`): `expiresAt` of the create response. Required for a held record, absent from every other. */\n readonly serverExpiresAt?: string;\n readonly secret?: string;\n}\n\ninterface Entry {\n readonly pending: EmailProofPending;\n readonly expired: boolean;\n}\n\nconst secrets = new WeakMap<EmailProofPending, Uint8Array>();\n\nfunction sha256Hex(text: string): string {\n return createHash('sha256').update(text).digest('hex');\n}\n\nfunction credentialsDir(home: string): string {\n return path.join(home, DIR_NAME);\n}\n\nfunction pendingFile(home: string, requestId: string): string {\n return path.join(credentialsDir(home), `pending-${sha256Hex(requestId)}.json`);\n}\n\nfunction assertAbsoluteHome(home: unknown): asserts home is string {\n if (typeof home !== 'string' || home.length === 0 || !path.isAbsolute(home)) {\n throw new PendingStoreError('credential-store-invalid', 'Credential store home must be a non-empty absolute path.');\n }\n}\n\nfunction lstatOrUndefined(file: string): fs.Stats | undefined {\n try {\n return fs.lstatSync(file);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw error;\n }\n}\n\nfunction assertOwnedByCurrentUser(st: fs.Stats, label: string): void {\n if (typeof process.getuid === 'function' && st.uid !== process.getuid()) {\n throw new PendingStoreError('credential-permissions', `${label} is not owned by the current user.`);\n }\n}\n\nfunction assertOwnerOnly(st: fs.Stats, label: string): void {\n const mode = st.mode & 0o777;\n if ((mode & 0o077) !== 0) {\n throw new PendingStoreError('credential-permissions', `${label} must be owner-only; found mode ${mode.toString(8)}.`);\n }\n // Owner check: a file owned by another user is untrusted even at mode 0600.\n assertOwnedByCurrentUser(st, label);\n}\n\n/**\n * Refuses a symlinked `home`. Ancestors may be symlinks (macOS `/var` is `/private/var`), so the resolved\n * `home` must be owned by the current user instead. A missing `home` is fine: the caller creates it.\n */\nfunction assertHomeSafe(home: string): void {\n const st = lstatOrUndefined(home);\n if (st?.isSymbolicLink()) {\n throw new PendingStoreError('credential-permissions', `${home} is a symlink; refusing to store credentials through it.`);\n }\n if (st) assertOwnedByCurrentUser(fs.statSync(fs.realpathSync(home)), home);\n}\n\n/**\n * Creates each missing directory on `target` with mode 0700 and pins it, so a restrictive umask cannot\n * leave the store unwritable. Existing directories are not touched.\n */\nfunction mkdirOwnerOnly(target: string): void {\n const missing: string[] = [];\n let current = target;\n while (lstatOrUndefined(current) === undefined) {\n missing.unshift(current);\n const parent = path.dirname(current);\n if (parent === current) break;\n current = parent;\n }\n for (const dir of missing) {\n try {\n fs.mkdirSync(dir, { mode: 0o700 });\n } catch (error) {\n // A pre-existing directory is not ours to chmod; ensureSecureDir checks it instead.\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n continue;\n }\n fs.chmodSync(dir, 0o700);\n }\n}\n\n/** Creates the store directory 0700 if absent. A pre-existing directory must already be owner-only; it is never chmodded. */\nexport function ensureSecureDir(home: string): void {\n assertHomeSafe(home);\n const dir = credentialsDir(home);\n mkdirOwnerOnly(home);\n mkdirOwnerOnly(dir);\n const st = fs.lstatSync(dir);\n if (!st.isDirectory()) {\n throw new PendingStoreError('credential-permissions', `${dir} must be a real directory, not a symlink or file.`);\n }\n // A loose pre-existing directory is refused, never chmodded: it may not be ours. assertOwnerOnly also checks the owner.\n assertOwnerOnly(st, dir);\n}\n\n/** Returns false when the store directory does not exist. Re-checks symlink, owner, and mode on every call. */\nexport function checkSecureDir(home: string): boolean {\n assertHomeSafe(home);\n const dir = credentialsDir(home);\n const st = lstatOrUndefined(dir);\n if (!st) return false;\n if (!st.isDirectory()) {\n throw new PendingStoreError('credential-permissions', `${dir} must be a real directory, not a symlink or file.`);\n }\n assertOwnerOnly(st, dir);\n return true;\n}\n\n/**\n * Atomic owner-only write: O_EXCL|O_NOFOLLOW temp file at 0600, fsync, then rename.\n * Refuses to replace a destination that is not a regular file.\n */\nfunction writeOwnerOnly(home: string, file: string, record: FileRecord): void {\n ensureSecureDir(home);\n const existing = lstatOrUndefined(file);\n if (existing && !existing.isFile()) {\n throw new PendingStoreError('credential-permissions', `${path.basename(file)} exists and is not a regular file; refusing to replace it.`);\n }\n const temp = `${file}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;\n let fd: number | undefined;\n // O_EXCL: never reuse an existing temp name. O_NOFOLLOW: a symlink planted at the temp name fails the open.\n try {\n fd = fs.openSync(temp, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);\n fs.writeFileSync(fd, JSON.stringify(record, null, 2) + '\\n');\n fs.fchmodSync(fd, 0o600);\n fs.fsyncSync(fd);\n fs.closeSync(fd);\n fd = undefined;\n fs.renameSync(temp, file);\n } finally {\n if (fd !== undefined) fs.closeSync(fd);\n try {\n fs.unlinkSync(temp);\n } catch {\n /* renamed or absent */\n }\n }\n}\n\n/**\n * Reads an owner-only JSON file. Re-checks the directory and the file on every read, refuses\n * symlinks and hard links, and confirms the opened descriptor is the inode that was checked.\n */\nfunction readOwnerOnly(home: string, file: string): unknown {\n if (!checkSecureDir(home)) return undefined;\n const label = path.basename(file);\n const before = lstatOrUndefined(file);\n if (!before) return undefined;\n if (!before.isFile()) {\n throw new PendingStoreError('credential-permissions', `${label} must be a regular file; symlinks are refused.`);\n }\n // lstat and owner check run before the open; the fstat below compares the opened inode.\n assertOwnerOnly(before, label);\n // O_NOFOLLOW: a symlink swapped in after the lstat fails the open instead of being read.\n const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);\n let text: string;\n try {\n const opened = fs.fstatSync(fd);\n if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) {\n throw new PendingStoreError('credential-permissions', `${label} changed while it was being opened.`);\n }\n if (opened.nlink !== 1) {\n throw new PendingStoreError('credential-permissions', `${label} has multiple hard links; refusing to read it.`);\n }\n assertOwnerOnly(opened, label);\n text = fs.readFileSync(fd, 'utf8');\n } finally {\n fs.closeSync(fd);\n }\n try {\n return JSON.parse(text) as unknown;\n } catch {\n removeFile(file);\n throw new PendingStoreError('credential-store-invalid', `Invalid credential store file ${label}.`);\n }\n}\n\nfunction removeFile(file: string): void {\n fs.rmSync(file, { force: true });\n}\n\n/** A temp file older than the pending window was left by a killed writer. It may hold a verifier, so it is deleted. */\nfunction sweepAbandonedTemp(file: string, now: Date): void {\n const st = lstatOrUndefined(file);\n if (st && now.getTime() - st.mtimeMs > EMAIL_PROOF_PENDING_MAX_MS) removeFile(file);\n}\n\nfunction exceedsWindow(record: { readonly createdAt: string; readonly expiresAt: string }): boolean {\n return Date.parse(record.expiresAt) > Date.parse(record.createdAt) + EMAIL_PROOF_PENDING_MAX_MS;\n}\n\nfunction isFileRecord(value: unknown): value is FileRecord {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;\n const record = value as Record<string, unknown>;\n if (record.type !== 'TinyCloudEmailProofPending' || record.version !== 1) return false;\n if (!FILE_TEXT_FIELDS.every((field) => typeof record[field] === 'string')) return false;\n if (record.secret !== undefined && (typeof record.secret !== 'string' || !BASE64.test(record.secret))) return false;\n // serverExpiresAt: present and parseable for the held profile, absent for every other profile. Neither shape loads as the other.\n if (record.profile === HELD_PROFILE_ID) {\n if (typeof record.serverExpiresAt !== 'string' || !Number.isFinite(Date.parse(record.serverExpiresAt))) return false;\n } else if (record.serverExpiresAt !== undefined) {\n return false;\n }\n return Number.isFinite(Date.parse(record.createdAt as string)) && Number.isFinite(Date.parse(record.expiresAt as string));\n}\n\n/** Builds the pending object from named fields only. Unknown keys in the file (including `__proto__`) are ignored. */\nfunction restoreFromFile(record: FileRecord): EmailProofPending {\n return restoreEmailProofPending({\n type: record.type,\n version: record.version,\n profile: record.profile as EmailProofPending['profile'],\n address: record.address,\n requestId: record.requestId,\n verifier: record.verifier,\n holderDid: record.holderDid,\n descriptorDigest: record.descriptorDigest,\n requirementDigest: record.requirementDigest,\n openerOrigin: record.openerOrigin,\n createdAt: record.createdAt,\n expiresAt: record.expiresAt,\n ...(record.serverExpiresAt === undefined ? {} : { serverExpiresAt: record.serverExpiresAt }),\n });\n}\n\n/** Reads one pending file. Deletes expired files, and malformed or over-long files before throwing. */\nfunction readEntry(home: string, file: string, now: Date): Entry | undefined {\n const value = readOwnerOnly(home, file);\n if (value === undefined) return undefined;\n if (!isFileRecord(value) || pendingFile(home, value.requestId) !== file || exceedsWindow(value)) {\n removeFile(file);\n throw new PendingStoreError('credential-store-invalid', 'Pending verification file is invalid and was deleted.');\n }\n const pending = restoreFromFile(value);\n if (value.secret !== undefined) secrets.set(pending, Buffer.from(value.secret, 'base64'));\n const expired = pendingDeadlineMs(pending) <= now.getTime();\n if (expired) removeFile(file);\n return { pending, expired };\n}\n\nfunction liveEntries(home: string, now: Date): Entry[] {\n if (!checkSecureDir(home)) return [];\n const dir = credentialsDir(home);\n const entries: Entry[] = [];\n for (const name of fs.readdirSync(dir)) {\n const file = path.join(dir, name);\n if (PENDING_TEMP_PATTERN.test(name)) {\n sweepAbandonedTemp(file, now);\n continue;\n }\n if (!PENDING_FILE_PATTERN.test(name)) continue;\n const entry = readEntry(home, file, now);\n if (entry) entries.push(entry);\n }\n return entries;\n}\n\nfunction toFileRecord(pending: EmailProofPending, secret: Uint8Array | undefined): FileRecord {\n return {\n type: pending.type,\n version: pending.version,\n profile: pending.profile,\n address: pending.address,\n requestId: pending.requestId,\n verifier: pending.verifier,\n holderDid: pending.holderDid,\n descriptorDigest: pending.descriptorDigest,\n requirementDigest: pending.requirementDigest,\n openerOrigin: pending.openerOrigin,\n createdAt: pending.createdAt,\n expiresAt: pending.expiresAt,\n ...(pending.serverExpiresAt === undefined ? {} : { serverExpiresAt: pending.serverExpiresAt }),\n ...(secret === undefined ? {} : { secret: Buffer.from(secret).toString('base64') }),\n };\n}\n\n/** Converts raw permission failures (for example EACCES under a restrictive umask) into a stable store error. */\nfunction wrapFsErrors<T>(run: () => T): T {\n try {\n return run();\n } catch (error) {\n if (error instanceof PendingStoreError) throw error;\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'EACCES' || code === 'EPERM') {\n throw new PendingStoreError('credential-permissions', 'Credential store is not accessible to this process; check the profile directory permissions.');\n }\n throw error;\n }\n}\n\n/**\n * File-backed `EmailProofPendingStore`, keyed by request id: one file per request, named by a hash of the\n * id. `put` never touches another request's file. `findLive` returns the newest unexpired record for an\n * address, so a second process can complete a request it did not create.\n */\nexport function createFileEmailProofPendingStore(options: FileEmailProofPendingStoreOptions): FileEmailProofPendingStore {\n const home: unknown = options?.home;\n assertAbsoluteHome(home);\n return {\n async get(requestId, now) {\n return wrapFsErrors(() => {\n const entry = readEntry(home, pendingFile(home, requestId), now);\n return entry === undefined || entry.expired ? undefined : entry.pending;\n });\n },\n async put(pending, putOptions) {\n return wrapFsErrors(() => {\n if (exceedsWindow(pending)) {\n throw new PendingStoreError('credential-store-invalid', 'Pending record expires beyond the 15-minute window and was rejected.');\n }\n const record = toFileRecord(pending, putOptions?.secret);\n if (!isFileRecord(record)) {\n throw new PendingStoreError('credential-store-invalid', 'Pending record is not a valid held or ticket record and was rejected.');\n }\n writeOwnerOnly(home, pendingFile(home, pending.requestId), record);\n });\n },\n async delete(requestId) {\n return wrapFsErrors(() => {\n if (!checkSecureDir(home)) return;\n removeFile(pendingFile(home, requestId));\n });\n },\n async findLive(address, now) {\n return wrapFsErrors(() => {\n let newest: EmailProofPending | undefined;\n for (const entry of liveEntries(home, now)) {\n if (entry.expired || entry.pending.address !== address) continue;\n if (!newest || Date.parse(entry.pending.createdAt) > Date.parse(newest.createdAt)) newest = entry.pending;\n }\n return newest;\n });\n },\n };\n}\n","import {\n canonicalDigest,\n createEmailCredentialRequirement,\n credentialError,\n credentialRequirementDigest,\n CredentialError,\n descriptorSatisfiesRequirement,\n encodeBase64Url,\n sha256Base64Url,\n validateCredentialFlowDescriptor,\n validateCredentialRequirement,\n verifyIssuedCredential,\n type CredentialFlowDescriptor,\n type CredentialIssuerMetadata,\n type CredentialRequirement,\n type IssuedCredentialEnvelope,\n type IssuerVerificationKey,\n type StoredCredentialRecord,\n type VerifiedCredential,\n} from \"@tinycloud/sdk-core\";\nimport { parseIssuerSet, parsePkhDid, toChecksumAddress, verifyHeldEmailCredential, type VerifiedHeldEmailCredential } from \"@tinycloud/sdk-core/held-credentials\";\nimport { interpretCredentialFlow } from \"./interpreter\";\nimport { OpenCredentialsHttpTransport } from \"./transport\";\nimport type { CredentialSigningAdapter, PrimitiveStepHandler } from \"./types\";\nimport { EMAIL_PROOF_PINS, HELD_ACCOUNT_BINDING_PINS } from \"./pins\";\n\nexport { EMAIL_PROOF_PINS, HELD_ACCOUNT_BINDING_PINS };\n\nexport const EMAIL_PROOF_CREDENTIAL_TYPE = { id: \"opencredentials.email/v1\", version: 1 } as const;\n/** A pending request is never trusted beyond this window, measured from creation. */\nexport const EMAIL_PROOF_PENDING_MAX_MS = 15 * 60_000;\n\nconst DISCOVERY_URL = `${EMAIL_PROOF_PINS.witnessOrigin}/.well-known/opencredentials`;\nexport const EMAIL_PROOF_ISSUER_KEYS: readonly IssuerVerificationKey[] = [\n {\n kid: EMAIL_PROOF_PINS.issuerKid,\n alg: \"EdDSA\",\n jwk: { kty: \"OKP\", crv: \"Ed25519\", x: EMAIL_PROOF_PINS.issuerX },\n validFrom: \"2020-01-01T00:00:00Z\",\n validUntil: \"2100-01-01T00:00:00Z\",\n },\n];\n\nexport type EmailProofProfileId = \"tinycloud.email-proof/v1\" | \"tinycloud.email-held/v1\";\nexport const DEFAULT_EMAIL_PROOF_PROFILE: EmailProofProfileId = \"tinycloud.email-proof/v1\";\n/** Each profile is pinned to its descriptor digest and to the mailbox step the witness must offer. */\nconst PROFILE_PINS: Readonly<Record<EmailProofProfileId, { readonly descriptorDigest: string; readonly requiredStep: \"mailbox_otp\"; readonly enabled: boolean }>> = {\n \"tinycloud.email-proof/v1\": { descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest, requiredStep: \"mailbox_otp\", enabled: true },\n // Held profile: disabled by default. A call enables it only with the per-call opt-in `allowHeldProfile: true` (see `profilePin`).\n // Without it, requests refuse with PROFILE_UNAVAILABLE before any network call.\n \"tinycloud.email-held/v1\": { descriptorDigest: HELD_ACCOUNT_BINDING_PINS.descriptorDigest, requiredStep: \"mailbox_otp\", enabled: false },\n};\n\nconst OPENER_ORIGIN = HELD_ACCOUNT_BINDING_PINS.openerOrigin;\nconst HELD_PROFILE = \"tinycloud.email-held/v1\" as const;\nconst HELD_KID_FRAGMENT = \"blockchainAccountId\";\n/** The held requirement's freshness (SPEC): 365 days. */\nconst HELD_MAX_AGE_SECONDS = 31_536_000;\n/** The witness's request window. */\nconst HELD_REQUEST_TTL_MS = 600_000;\n/** How far the client clock may be behind the witness's: in the pre-flight, the post-issue check and the reuse read. */\nconst CLOCK_SKEW_BOUND_MS = 300_000;\nconst STEP_TIMEOUT_MS = 60_000;\nconst HOLDER_DID_PREFIX = \"did:key:\";\nconst HOLDER_DID_PATTERN = /^did:key:z6Mk[^#]+$/;\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst INSPECT = Symbol.for(\"nodejs.util.inspect.custom\");\n\n/** Which part of the two-step flow failed. */\nexport type EmailProofStep = \"request\" | \"code\" | \"credential\" | \"session\";\n\n/**\n * Stable error codes. `recoverable` = the same pending request may be retried\n * (with another code, or after the network returns) before it expires.\n *\n * | code | recoverable | reason |\n * |-------------------------|-------------|--------|\n * | ADDRESS_INVALID | false | input is malformed; the same address cannot succeed |\n * | HOLDER_INVALID | false | holder key is not an Ed25519 did:key with a matching kid |\n * | CODE_INVALID | true | local format check (8 digits); no network; pending kept |\n * | RATE_LIMITED | witness | witness `recoverable` flag decides (defaults to true); retryAfterMs set |\n * | ISSUER_UNAVAILABLE | true | transient issuer outage (503); witness flag is honoured |\n * | NETWORK | true | transport failure or step timeout; pending kept |\n * | PROFILE_UNAVAILABLE | false | catalog or descriptor does not match the pinned profile, or (local, no witness contacted) the profile is not enabled |\n * | CODE_REJECTED | true | wrong code; witness flag decides whether another attempt is allowed |\n * | CODE_ATTEMPTS_EXHAUSTED | false | witness proof-attempt budget spent; pending is dead |\n * | SIGNATURE_REJECTED | true | holder binding rejected; same pending may be retried |\n * | CREDENTIAL_INVALID | false | issued credential failed local verification |\n * | SESSION_EXPIRED | false | pending window or server expiry has passed |\n * | SESSION_INVALID | false | pending does not match holder, descriptor, or requirement |\n * | CLIENT_CLOCK_SKEW | false | device clock more than 5 min behind the witness; no mail sent; fix the clock and start again |\n * | CANCELED | false | caller aborted; pending is deleted |\n * | FAILED | false | unclassified failure; pending is deleted |\n */\nexport type EmailProofErrorCode =\n | \"ADDRESS_INVALID\"\n | \"HOLDER_INVALID\"\n | \"CODE_INVALID\"\n | \"RATE_LIMITED\"\n | \"ISSUER_UNAVAILABLE\"\n | \"NETWORK\"\n | \"PROFILE_UNAVAILABLE\"\n | \"CODE_REJECTED\"\n | \"CODE_ATTEMPTS_EXHAUSTED\"\n | \"SIGNATURE_REJECTED\"\n | \"CREDENTIAL_INVALID\"\n | \"SESSION_EXPIRED\"\n | \"SESSION_INVALID\"\n | \"CLIENT_CLOCK_SKEW\"\n | \"CANCELED\"\n | \"FAILED\";\n\nconst RECOVERABILITY: Readonly<Record<EmailProofErrorCode, boolean>> = {\n ADDRESS_INVALID: false,\n HOLDER_INVALID: false,\n CODE_INVALID: true,\n RATE_LIMITED: true,\n ISSUER_UNAVAILABLE: true,\n NETWORK: true,\n PROFILE_UNAVAILABLE: false,\n CODE_REJECTED: true,\n CODE_ATTEMPTS_EXHAUSTED: false,\n SIGNATURE_REJECTED: true,\n CREDENTIAL_INVALID: false,\n SESSION_EXPIRED: false,\n SESSION_INVALID: false,\n CLIENT_CLOCK_SKEW: false,\n CANCELED: false,\n FAILED: false,\n};\n\nconst MESSAGES: Record<Exclude<EmailProofErrorCode, \"RATE_LIMITED\">, string> = {\n ADDRESS_INVALID: \"Email address is invalid.\",\n HOLDER_INVALID: \"Holder must be a did:key Ed25519 identifier.\",\n CODE_INVALID: \"Verification code must be 8 digits.\",\n ISSUER_UNAVAILABLE: \"Email verification is unavailable right now.\",\n NETWORK: \"Could not reach the email verification service.\",\n PROFILE_UNAVAILABLE: \"The witness does not offer the email verification profile.\",\n CODE_REJECTED: \"Verification code was not accepted.\",\n CODE_ATTEMPTS_EXHAUSTED: \"Too many wrong codes. Request a new code.\",\n SIGNATURE_REJECTED: \"Holder signature was rejected.\",\n CREDENTIAL_INVALID: \"Issued credential failed verification.\",\n SESSION_EXPIRED: \"Email verification request expired. Request a new code.\",\n SESSION_INVALID: \"Email verification request is no longer valid. Request a new code.\",\n CLIENT_CLOCK_SKEW: \"Your device clock is more than 5 minutes behind. Correct it and try again.\",\n CANCELED: \"Email verification was canceled.\",\n FAILED: \"Email verification failed.\",\n};\n\n/** The local refusal of a profile this SDK build does not enable (the held profile without the per-call opt-in). No witness is contacted. */\nconst LOCAL_PROFILE_UNAVAILABLE = \"This SDK build doesn't enable the email verification profile.\";\n\nfunction rateLimitMessage(retryAfterMs: number | undefined): string {\n const head = \"Too many codes were requested for this address.\";\n if (retryAfterMs === undefined || !Number.isFinite(retryAfterMs) || retryAfterMs <= 0) return `${head} Try again in about an hour.`;\n const minutes = Math.max(1, Math.ceil(retryAfterMs / 60_000));\n return `${head} Try again in ${minutes} ${minutes === 1 ? \"minute\" : \"minutes\"}.`;\n}\n\n/** Stable, redaction-safe error. The message never includes the code, verifier, or key material. */\nexport class EmailProofError extends Error {\n readonly name = \"EmailProofError\";\n readonly step: EmailProofStep;\n readonly code: EmailProofErrorCode;\n readonly retryAfterMs: number | undefined;\n readonly recoverable: boolean;\n\n /** `message` overrides the code's text for one refusal whose cause the shared text would misdescribe. It never carries request data. */\n constructor(step: EmailProofStep, code: EmailProofErrorCode, retryAfterMs?: number, recoverable: boolean = RECOVERABILITY[code], message?: string) {\n super(message ?? (code === \"RATE_LIMITED\" ? rateLimitMessage(retryAfterMs) : MESSAGES[code]));\n this.step = step;\n this.code = code;\n this.retryAfterMs = retryAfterMs;\n this.recoverable = recoverable;\n }\n}\n\nexport type HolderAlgorithm = \"EdDSA\" | \"EIP191\";\n\n/**\n * Holder key adapter. `kid` names the key the witness verifies against. `completeEmailProof` rejects with\n * SESSION_INVALID unless `did === pending.holderDid`.\n * `sign` MUST return a raw signature over exactly the bytes it is given. For `EdDSA` the signer applies no\n * prefix. For `EIP191` the adapter performs `personal_sign` over those bytes, and the wallet applies the\n * `\\x19Ethereum Signed Message:\\n<len>` prefix. The prefix is applied in that one place and nowhere else.\n */\nexport interface HolderSigner {\n readonly did: string;\n readonly alg: HolderAlgorithm;\n readonly kid: string;\n readonly sign: (bytes: Uint8Array) => Promise<Uint8Array>;\n}\n\n/**\n * Ed25519 did:key holder. Accepts `did:key:z6Mk…` or `did:key:z6Mk…#<same multibase>`\n * (the session-manager form) and normalises to the bare DID. `kid` is derived from the bare DID.\n */\nexport function createEd25519HolderSigner(did: string, sign: (bytes: Uint8Array) => Promise<Uint8Array>): HolderSigner {\n const fragment = did.indexOf(\"#\");\n const bare = fragment === -1 ? did : did.slice(0, fragment);\n if (fragment !== -1 && did.slice(fragment + 1) !== bare.slice(HOLDER_DID_PREFIX.length)) throw new EmailProofError(\"request\", \"HOLDER_INVALID\");\n if (!HOLDER_DID_PATTERN.test(bare)) throw new EmailProofError(\"request\", \"HOLDER_INVALID\");\n return { did: bare, alg: \"EdDSA\", kid: holderKid(bare), sign };\n}\n\n/** Options shared by the public steps. Issuer keys are never caller-supplied. */\nexport interface EmailProofOptions {\n /**\n * Per-call opt-in for the held profile `tinycloud.email-held/v1`. Only the literal `true` counts, it enables the held\n * profile for this call alone (never stored in a pending, never read from the environment), and it is ignored for any other\n * profile. Without it a held request is refused PROFILE_UNAVAILABLE before any network call. Nothing else can be supplied with\n * it: the origin, issuer key and descriptor digest are compiled pins. The default 60 s step signal spans the wallet prompt in\n * `completeEmailProof` for an account holder: pass a `signal` bounded by the request `expiresAt` if the user may take longer.\n */\n readonly allowHeldProfile?: true;\n readonly fetch?: typeof fetch;\n readonly signal?: AbortSignal;\n readonly now?: () => Date;\n}\n\nexport interface EmailProofRequestInput extends EmailProofOptions {\n readonly address: string;\n readonly holder: HolderSigner;\n readonly profile?: EmailProofProfileId;\n}\n\n/**\n * Durable state between `requestEmailProofCode` and `completeEmailProof`.\n * `verifier` is a bearer secret. It is a non-enumerable property, so spread\n * omits it; `toJSON` and `util.inspect` redact it. Persist it only with owner-only storage.\n */\nexport interface EmailProofPending {\n readonly type: \"TinyCloudEmailProofPending\";\n readonly version: 1;\n readonly profile: EmailProofProfileId;\n readonly address: string;\n readonly requestId: string;\n readonly verifier: string;\n readonly holderDid: string;\n readonly descriptorDigest: string;\n readonly requirementDigest: string;\n readonly openerOrigin: string;\n readonly createdAt: string;\n readonly expiresAt: string;\n /** Held only: `expiresAt` of the create response (the witness's own deadline). The signed binding must carry it. */\n readonly serverExpiresAt?: string;\n}\n\n/** Holds pending requests keyed by requestId. Implementations must never return an expired request from `findLive`. */\nexport interface EmailProofPendingStore {\n /** Newest unexpired request for `address`, or undefined. */\n findLive(address: string, now: Date): Promise<EmailProofPending | undefined>;\n put(pending: EmailProofPending): Promise<void>;\n delete(requestId: string): Promise<void>;\n /** Pending request by id, or undefined if absent or past its deadline. */\n get(requestId: string, now: Date): Promise<EmailProofPending | undefined>;\n}\n\n/**\n * Rebuilds a pending request from stored fields. The result is redacted exactly like\n * one produced by `requestEmailProofCode`. Stores call this on every read.\n */\nexport function restoreEmailProofPending(fields: EmailProofPending): EmailProofPending {\n const shown = {\n type: fields.type,\n version: fields.version,\n profile: fields.profile,\n address: fields.address,\n requestId: fields.requestId,\n holderDid: fields.holderDid,\n descriptorDigest: fields.descriptorDigest,\n requirementDigest: fields.requirementDigest,\n openerOrigin: fields.openerOrigin,\n createdAt: fields.createdAt,\n expiresAt: fields.expiresAt,\n ...(fields.serverExpiresAt === undefined ? {} : { serverExpiresAt: fields.serverExpiresAt }),\n };\n const pending = { ...shown } as EmailProofPending;\n hide(pending, \"verifier\", fields.verifier);\n hide(pending, \"toJSON\", () => ({ ...shown }));\n hide(pending, INSPECT, () => ({ ...shown, verifier: \"[redacted]\" }));\n return Object.freeze(pending);\n}\n\nfunction hide(target: object, key: string | symbol, value: unknown): void {\n Object.defineProperty(target, key, { value, enumerable: false, writable: false, configurable: false });\n}\n\n/** Latest instant at which a pending request may still be completed. */\nexport function pendingDeadlineMs(pending: EmailProofPending): number {\n return Math.min(Date.parse(pending.expiresAt), Date.parse(pending.createdAt) + EMAIL_PROOF_PENDING_MAX_MS);\n}\n\n/** Process-local pending store for browsers and tests. Nothing survives a reload. */\nexport function createMemoryEmailProofPendingStore(): EmailProofPendingStore {\n const byRequest = new Map<string, EmailProofPending>();\n const prune = (now: Date) => {\n for (const [requestId, pending] of byRequest) if (pendingDeadlineMs(pending) <= now.getTime()) byRequest.delete(requestId);\n };\n return {\n async findLive(address, now) {\n prune(now);\n let newest: EmailProofPending | undefined;\n for (const pending of byRequest.values()) {\n if (pending.address === address && (!newest || Date.parse(pending.createdAt) > Date.parse(newest.createdAt))) newest = pending;\n }\n return newest;\n },\n async put(pending) { byRequest.set(pending.requestId, pending); },\n async delete(requestId) { byRequest.delete(requestId); },\n async get(requestId, now) { prune(now); return byRequest.get(requestId); },\n };\n}\n\nexport interface EmailProofResult {\n readonly envelope: IssuedCredentialEnvelope;\n /** The ticket verifier's result, or for the held profile (`envelope.profile.id`) the held verifier's. */\n readonly verified: VerifiedCredential | VerifiedHeldEmailCredential;\n readonly credentialId: string;\n readonly issuedAt: string;\n readonly expiresAt: string;\n /** The descriptor the verifier checked against. Stored alongside the credential for offline re-verification. */\n readonly descriptor: CredentialFlowDescriptor;\n}\n\n/** True for a ticket (did:key) result, whose `verified` is the ticket verifier's output. The held profile's result carries the held verifier's. */\nexport function isTicketEmailProofResult(result: EmailProofResult): result is EmailProofResult & { readonly verified: VerifiedCredential } {\n return result.envelope.profile.id !== HELD_PROFILE;\n}\n\nexport interface EmailProofAcquireInput extends EmailProofRequestInput {\n /** Supplies the 8-digit code. Omit to only request one; the pending request is returned for a later call. */\n readonly getCode?: (context: { readonly pending: EmailProofPending }) => Promise<string>;\n /** Resumes an unexpired pending request for this address, and keeps one across recoverable failures. */\n readonly pendingStore?: EmailProofPendingStore;\n}\n\nexport type EmailProofAcquisition =\n | { readonly status: \"issued\"; readonly result: EmailProofResult; /** Non-fatal, e.g. the pending record could not be deleted after success. */ readonly warnings: readonly string[] }\n | { readonly status: \"code-requested\"; readonly pending: EmailProofPending };\n\n/** Thrown from the OTP handler to stop step 1 after the witness has sent the code. */\nclass CodeRequested extends Error {}\n\nconst pauseForCode: PrimitiveStepHandler = async () => {\n throw new CodeRequested(\"code requested\");\n};\n\n/**\n * Step 1: start an email-proof request. The witness sends an 8-digit code to\n * `address`. Returns the pending state to persist until the owner supplies the code.\n */\nexport async function requestEmailProofCode(input: EmailProofRequestInput): Promise<EmailProofPending> {\n const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;\n profilePin(profile, \"request\", input.allowHeldProfile);\n const held = profile === HELD_PROFILE;\n // Before any network call: the holder, then the address (held order, C6.4); the ticket keeps its address-then-holder order.\n if (held) assertHolder(input.holder, \"request\", profile);\n const address = normalizeAddress(input.address, profile);\n if (!held) assertHolder(input.holder, \"request\", profile);\n const now = input.now ?? (() => new Date());\n const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);\n const requirement = emailRequirement(address, profile);\n const requirementDigest = await credentialRequirementDigest(requirement);\n const descriptor = await witnessDescriptor(profile, requirement, { fetch: input.fetch, signal }, \"request\", input.allowHeldProfile);\n const descriptorDigest = await canonicalDigest(descriptor);\n const verifier = randomVerifier();\n const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true, ...(held ? { allowAccountSubject: true as const } : {}) });\n\n const createdAtMs = now().getTime();\n let created: Awaited<ReturnType<OpenCredentialsHttpTransport[\"create\"]>>;\n try {\n created = await transport.create({\n descriptor,\n descriptorDigest,\n requirement,\n requirementDigest,\n holderDid: input.holder.did,\n openerOrigin: OPENER_ORIGIN,\n completionVerifierChallenge: await sha256Base64Url(verifier),\n signal,\n });\n } catch (error) {\n throw mapError(error, \"request\", signal);\n }\n const serverExpiry = Date.parse(created.expiresAt);\n if (!Number.isFinite(serverExpiry)) throw new EmailProofError(\"request\", \"FAILED\");\n // Held: the create response carries the witness's clock (`expiresAt` is creation + 600 s). A client more than 5 minutes behind it is\n // refused here, after R1 (no mail, no send counted) and before R3, so no code is mailed, no pending is stored and no wallet is asked.\n const serverNowMs = serverExpiry - HELD_REQUEST_TTL_MS;\n if (held && serverNowMs - now().getTime() > CLOCK_SKEW_BOUND_MS) throw new EmailProofError(\"request\", \"CLIENT_CLOCK_SKEW\");\n const pending = restoreEmailProofPending({\n type: \"TinyCloudEmailProofPending\",\n version: 1,\n profile,\n address,\n requestId: created.requestId,\n verifier,\n holderDid: input.holder.did,\n descriptorDigest,\n requirementDigest,\n openerOrigin: OPENER_ORIGIN,\n createdAt: new Date(createdAtMs).toISOString(),\n // Held: the deadline is measured on the client clock (a fast client must not lose window), the witness enforces its own 600 s.\n expiresAt: new Date(held ? createdAtMs + Math.min(serverExpiry - serverNowMs, EMAIL_PROOF_PENDING_MAX_MS) : Math.min(serverExpiry, createdAtMs + EMAIL_PROOF_PENDING_MAX_MS)).toISOString(),\n ...(held ? { serverExpiresAt: created.expiresAt } : {}),\n });\n\n try {\n await interpretCredentialFlow({\n descriptor,\n ...(held ? { allowAccountSubject: true as const } : {}),\n requirement,\n requestId: created.requestId,\n verifier,\n holderDid: input.holder.did,\n descriptorDigest,\n requirementDigest,\n openerOrigin: OPENER_ORIGIN,\n ...(held ? { expiresAt: created.expiresAt } : {}),\n transport,\n signing: holderSigning(input.holder, held),\n handlers: { mailbox_otp: pauseForCode },\n signal,\n });\n } catch (error) {\n if (error instanceof CodeRequested) return pending;\n throw mapError(error, \"request\", signal);\n }\n throw new EmailProofError(\"request\", \"FAILED\");\n}\n\n/**\n * Step 2: submit the owner's 8-digit code, sign the holder binding, issue, and\n * verify. The caller must delete `pending` after this call, success or failure.\n */\nexport async function completeEmailProof(input: EmailProofOptions & { readonly pending: EmailProofPending; readonly code: string; readonly holder: HolderSigner }): Promise<EmailProofResult> {\n return completeWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);\n}\n\n/**\n * Internal test seam: completes with caller-supplied issuer keys. Not re-exported\n * from the package barrel. Production code uses `completeEmailProof`.\n */\nexport async function completeEmailProofWithIssuerKeys(\n input: EmailProofOptions & { readonly pending: EmailProofPending; readonly code: string; readonly holder: HolderSigner },\n issuerKeys: readonly IssuerVerificationKey[],\n): Promise<EmailProofResult> {\n return completeWithIssuerKeys(input, issuerKeys);\n}\n\nasync function completeWithIssuerKeys(\n input: EmailProofOptions & { readonly pending: EmailProofPending; readonly code: string; readonly holder: HolderSigner },\n issuerKeys: readonly IssuerVerificationKey[],\n): Promise<EmailProofResult> {\n const { pending } = input;\n const now = input.now ?? (() => new Date());\n if (!isPending(pending)) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n // The opt-in is per call and never stored: a held pending is completed only by a caller that opts in again.\n profilePin(pending.profile, \"session\", input.allowHeldProfile);\n const held = pending.profile === HELD_PROFILE;\n if (held && !Number.isFinite(Date.parse(pending.serverExpiresAt ?? \"\"))) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n if (pendingDeadlineMs(pending) <= now().getTime()) throw new EmailProofError(\"session\", \"SESSION_EXPIRED\");\n if (!/^\\d{8}$/.test(input.code)) throw new EmailProofError(\"code\", \"CODE_INVALID\");\n assertHolder(input.holder, \"code\", pending.profile);\n if (input.holder.did !== pending.holderDid) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);\n\n const requirement = emailRequirement(pending.address, pending.profile);\n if (await credentialRequirementDigest(requirement) !== pending.requirementDigest) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n const descriptor = await witnessDescriptor(pending.profile, requirement, { fetch: input.fetch, signal }, \"session\", input.allowHeldProfile);\n const descriptorDigest = await canonicalDigest(descriptor);\n if (descriptorDigest !== pending.descriptorDigest) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true, ...(held ? { allowAccountSubject: true as const } : {}) });\n\n try {\n // The code email was sent in step 1. A challenge-required state here would make\n // the interpreter resend it, so refuse instead.\n const state = await transport.state(pending.requestId, pending.verifier, signal);\n if (state.nextStep?.constraints.challengeRequired === true) throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n await interpretCredentialFlow({\n descriptor,\n ...(held ? { allowAccountSubject: true as const } : {}),\n requirement,\n requestId: pending.requestId,\n verifier: pending.verifier,\n holderDid: pending.holderDid,\n descriptorDigest,\n requirementDigest: pending.requirementDigest,\n openerOrigin: pending.openerOrigin,\n ...(held ? { expiresAt: pending.serverExpiresAt } : {}),\n transport,\n signing: holderSigning(input.holder, held),\n handlers: { mailbox_otp: async () => ({ otp: input.code }) },\n signal,\n });\n } catch (error) {\n throw mapError(error, \"code\", signal);\n }\n\n try {\n const envelope = await transport.result(pending.requestId, pending.verifier, signal);\n if (held) {\n const verified = await verifyHeldResult({ envelope, pending, descriptor, descriptorDigest, transport, issuerKeys, now });\n return { envelope, verified, credentialId: verified.credentialId, issuedAt: verified.issuedAt.toISOString(), expiresAt: verified.expiresAt.toISOString(), descriptor };\n }\n const verified = await verifyIssuedCredential({\n envelope,\n descriptor,\n descriptorDigest,\n requirement,\n holderDid: pending.holderDid,\n issuerMetadata: pinnedIssuerMetadata(issuerKeys),\n now: now(),\n checkStatus: (status, statusSignal) => transport.checkStatus(status, statusSignal),\n signal,\n });\n return { envelope, verified, credentialId: verified.credentialId, issuedAt: verified.issuedAt, expiresAt: verified.expiresAt, descriptor };\n } catch (error) {\n throw mapError(error, \"credential\", signal);\n }\n}\n\n/**\n * Request a code and, when `getCode` is given, complete the flow in one call.\n * With a `pendingStore`, an unexpired request for the address is resumed instead of\n * requesting a new code. A non-recoverable failure deletes the pending request.\n *\n * `store.put` throwing after the code was mailed: the code is in the owner's inbox but\n * not persisted, so the error propagates and the owner must request a new code.\n * `store.delete` failing after success: the credential is returned with a warning.\n * `store.delete` failing on a non-recoverable failure: the original error propagates.\n */\nexport async function acquireEmailProof(input: EmailProofAcquireInput): Promise<EmailProofAcquisition> {\n return acquireWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);\n}\n\n/** Internal test seam, same as `completeEmailProofWithIssuerKeys`. Not re-exported from the barrel. */\nexport async function acquireEmailProofWithIssuerKeys(input: EmailProofAcquireInput, issuerKeys: readonly IssuerVerificationKey[]): Promise<EmailProofAcquisition> {\n return acquireWithIssuerKeys(input, issuerKeys);\n}\n\nasync function acquireWithIssuerKeys(input: EmailProofAcquireInput, issuerKeys: readonly IssuerVerificationKey[]): Promise<EmailProofAcquisition> {\n const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;\n profilePin(profile, \"request\", input.allowHeldProfile);\n const address = normalizeAddress(input.address, profile);\n const now = input.now ?? (() => new Date());\n const store = input.pendingStore;\n const live = store ? await store.findLive(address, now()) : undefined;\n // The store returns the newest live record per address across profiles. Resume only one of this profile.\n let pending = live?.profile === profile ? live : undefined;\n // A resumed held pending without a valid create-response deadline cannot be completed (the signed binding must carry it): refuse it now,\n // before the user is asked for a code, and drop it.\n if (pending && profile === HELD_PROFILE && !Number.isFinite(Date.parse(pending.serverExpiresAt ?? \"\"))) {\n await store?.delete(pending.requestId).catch(() => undefined);\n throw new EmailProofError(\"session\", \"SESSION_INVALID\");\n }\n if (!pending) {\n pending = await requestEmailProofCode({ ...input, address });\n await store?.put(pending);\n }\n const shown = restoreEmailProofPending(pending);\n if (!input.getCode) return { status: \"code-requested\", pending: shown };\n\n const code = await input.getCode({ pending: shown });\n try {\n const result = await completeWithIssuerKeys({ ...input, pending, code }, issuerKeys);\n const warnings: string[] = [];\n if (store) {\n try { await store.delete(pending.requestId); } catch { warnings.push(\"The pending request could not be deleted after success; it expires at its deadline.\"); }\n }\n return { status: \"issued\", result, warnings };\n } catch (error) {\n if (!(error instanceof EmailProofError && error.recoverable)) await store?.delete(pending.requestId).catch(() => undefined);\n throw error;\n }\n}\n\nfunction normalizeAddress(address: string, profile: EmailProofProfileId): string {\n if (profile === HELD_PROFILE) return normalizeHeldAddress(address);\n const value = address.trim();\n if (value.length > 254 || !EMAIL_PATTERN.test(value)) throw new EmailProofError(\"request\", \"ADDRESS_INVALID\");\n return value;\n}\n\nconst HELD_LOCAL_ATOM = /^[a-z0-9!#$%&'*+\\-/=?^_`{|}~]+(\\.[a-z0-9!#$%&'*+\\-/=?^_`{|}~]+)*$/;\nconst HELD_DOMAIN_LABEL = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;\n\n/**\n * Held address, in the witness's own order: trim, refuse any non-ASCII character (so an uppercase non-ASCII form is never lowercased into\n * something else), ASCII-lowercase, then the witness's rules (C2.5.3). The OTP proves the lowercased mailbox, which is what the\n * credential then discloses. Every refusal is ADDRESS_INVALID, before any network call.\n */\nfunction normalizeHeldAddress(address: string): string {\n const invalid = () => new EmailProofError(\"request\", \"ADDRESS_INVALID\");\n const trimmed = address.trim();\n if (/[^\\x00-\\x7f]/.test(trimmed)) throw invalid();\n const value = trimmed.toLowerCase();\n const parts = value.split(\"@\");\n if (value.length < 3 || value.length > 254 || parts.length !== 2 || /[^\\x21-\\x7e]/.test(value)) throw invalid();\n const [local, domain] = parts as [string, string];\n if (local.length > 64 || !HELD_LOCAL_ATOM.test(local) || local.startsWith(\"+\")) throw invalid();\n if (domain.length > 253 || !domain.includes(\".\") || !domain.split(\".\").every((label) => label.length <= 63 && HELD_DOMAIN_LABEL.test(label))) throw invalid();\n return value;\n}\n\nfunction holderKid(did: string): string {\n return `${did}#${did.slice(HOLDER_DID_PREFIX.length)}`;\n}\n\n/** Held holder: exactly the canonical `did:pkh:eip155:1:<EIP-55>`, alg EIP191, kid `<did>#blockchainAccountId`. Every valid account is accepted. */\nfunction assertHeldHolder(holder: HolderSigner, step: EmailProofStep): void {\n const parsed = parsePkhDid(holder.did);\n const canonical = parsed !== null && parsed.chainId === \"1\" ? `did:pkh:eip155:1:${toChecksumAddress(parsed.address)}` : undefined;\n if (canonical === undefined || holder.did !== canonical || holder.alg !== \"EIP191\" || holder.kid !== `${canonical}#${HELD_KID_FRAGMENT}`) throw new EmailProofError(step, \"HOLDER_INVALID\");\n}\n\nfunction assertHolder(holder: HolderSigner, step: EmailProofStep, profile: EmailProofProfileId): void {\n if (profile === HELD_PROFILE) return assertHeldHolder(holder, step);\n if (!HOLDER_DID_PATTERN.test(holder.did) || holder.alg !== \"EdDSA\" || holder.kid !== holderKid(holder.did)) throw new EmailProofError(step, \"HOLDER_INVALID\");\n}\n\n/** The only reader of the held opt-in. Held is enabled for this call only by the literal `true`; any other value, and any other profile, is unaffected. */\nfunction profilePin(profile: string, step: EmailProofStep, allowHeld?: unknown): (typeof PROFILE_PINS)[EmailProofProfileId] {\n const entry = Object.prototype.hasOwnProperty.call(PROFILE_PINS, profile) ? PROFILE_PINS[profile as EmailProofProfileId] : undefined;\n if (!entry || (!entry.enabled && !(allowHeld === true && profile === HELD_PROFILE))) {\n // Local refusal: no witness has been contacted, so the shared PROFILE_UNAVAILABLE text (\"The witness does not offer ...\") would be untrue here.\n throw new EmailProofError(step, \"PROFILE_UNAVAILABLE\", undefined, undefined, LOCAL_PROFILE_UNAVAILABLE);\n }\n return entry;\n}\n\nfunction emailRequirement(address: string, profile: EmailProofProfileId): CredentialRequirement {\n // Held: the row's freshness is 365 days (digest Q_dnVOBsX_7fRjuzLPtCmDe_SKPiXW0Sxv0zSPb_Jd4 for alice@example.com). The ticket stays 3600.\n if (profile === HELD_PROFILE) return validateCredentialRequirement({ type: \"TinyCloudCredentialRequirement\", version: 1, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE, claims: { email: address }, maxAgeSeconds: HELD_MAX_AGE_SECONDS });\n return createEmailCredentialRequirement({ email: address, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE });\n}\n\n/** Requirement for an email credential issued to `address` under the default profile. Storage and `show` key on its digest. */\nexport function emailProofRequirement(address: string): CredentialRequirement {\n return emailRequirement(normalizeAddress(address, DEFAULT_EMAIL_PROOF_PROFILE), DEFAULT_EMAIL_PROOF_PROFILE);\n}\n\n/** Discovers the catalog and returns the descriptor only if it matches the pinned issuer, origin, step, and digest. */\nasync function witnessDescriptor(\n profile: EmailProofProfileId,\n requirement: CredentialRequirement,\n options: { readonly fetch?: typeof fetch; readonly signal: AbortSignal },\n step: EmailProofStep,\n allowHeld?: unknown,\n): Promise<CredentialFlowDescriptor> {\n const pin = profilePin(profile, step, allowHeld);\n let descriptor: CredentialFlowDescriptor;\n try {\n descriptor = await discoverCatalogDescriptor(requirement, DISCOVERY_URL, { fetch: options.fetch, signal: options.signal });\n } catch (error) {\n throw discoveryFailure(error, options.signal, step);\n }\n if (descriptor.issuer.origin !== EMAIL_PROOF_PINS.witnessOrigin || descriptor.issuer.did !== EMAIL_PROOF_PINS.issuerDid || !descriptor.steps.some((candidate) => candidate.type === pin.requiredStep)) {\n throw new EmailProofError(step, \"PROFILE_UNAVAILABLE\");\n }\n if (await canonicalDigest(descriptor) !== pin.descriptorDigest) throw new EmailProofError(step, \"PROFILE_UNAVAILABLE\");\n return descriptor;\n}\n\n/**\n * Fetch an OpenCredentials catalog and return the first enabled, ready profile whose\n * descriptor satisfies the requirement and matches its catalog digest.\n */\nasync function discoverCatalogDescriptor(\n requirement: CredentialRequirement,\n discoveryUrl: string,\n options: { readonly fetch?: typeof fetch; readonly signal?: AbortSignal } = {},\n): Promise<CredentialFlowDescriptor> {\n let url: URL; try { url = new URL(discoveryUrl); } catch { throw new CredentialError(\"UNSUPPORTED_PROFILE\", \"Discovery URL is invalid\"); }\n if (url.protocol !== \"https:\" || ![\"/.well-known/opencredentials\", \"/v1/credential-types\"].includes(url.pathname) || url.search || url.hash) throw new CredentialError(\"UNSUPPORTED_PROFILE\", \"Discovery URL is not an allowed catalog endpoint\");\n const init: RequestInit & { readonly cache: \"no-store\" } = { credentials: \"omit\", cache: \"no-store\", redirect: \"error\", referrerPolicy: \"no-referrer\", signal: options.signal };\n let response: Response;\n try { response = await (options.fetch ?? globalThis.fetch.bind(globalThis))(url, init); } catch (cause) { throw new CredentialError(\"OFFLINE\", \"Credential discovery is unavailable\", { cause }); }\n if (!response.ok) throw new CredentialError(response.status === 503 ? \"ISSUER_UNREADY\" : \"OFFLINE\", \"Credential discovery is unavailable\");\n const catalog = await response.json() as Record<string, unknown>;\n if (catalog.type !== \"tinycloud.credentials/catalog/v1\" || catalog.protocol !== \"tinycloud.credentials/acquisition/v1\" || catalog.catalogVersion !== 1 || !Array.isArray(catalog.profiles)) throw new CredentialError(\"DESCRIPTOR_INVALID\", \"Credential catalog is invalid\");\n for (const entryValue of catalog.profiles) {\n if (typeof entryValue !== \"object\" || entryValue === null || Array.isArray(entryValue)) continue;\n const entry = entryValue as Record<string, unknown>;\n if (entry.supported !== true || entry.enabled !== true || (entry.readiness !== \"ready\" && entry.readiness !== \"degraded\")) continue;\n try {\n const candidate = validateCredentialFlowDescriptor(entry.descriptor);\n if (descriptorSatisfiesRequirement(candidate, requirement) && entry.descriptorDigest === await canonicalDigest(candidate)) return candidate;\n } catch { /* malformed catalog entry: skip */ }\n }\n throw new CredentialError(\"UNSUPPORTED_PROFILE\", \"No available credential profile satisfies the requirement\");\n}\n\n/** A discovery abort by the caller is CANCELED. A step timeout stays NETWORK (recoverable). */\nfunction discoveryFailure(error: unknown, signal: AbortSignal, step: EmailProofStep): EmailProofError {\n if (signal.aborted && (signal.reason as { name?: string } | undefined)?.name !== \"TimeoutError\") return new EmailProofError(step, \"CANCELED\");\n return mapError(error, step, signal);\n}\n\nfunction pinnedIssuerMetadata(keys: readonly IssuerVerificationKey[]): CredentialIssuerMetadata {\n return { type: \"OpenCredentialsIssuerMetadata\", version: 1, origin: EMAIL_PROOF_PINS.witnessOrigin, issuerDid: EMAIL_PROOF_PINS.issuerDid, keys, cache: { maxAgeSeconds: 3600, etag: \"pinned\" } };\n}\n\n/** A stored email-proof credential plus the descriptor it was issued under. Read back from disk, so untrusted until verified. */\nexport type StoredEmailProofRecord = StoredCredentialRecord & {\n readonly subjectDid: string;\n readonly descriptor: CredentialFlowDescriptor;\n};\n\n/** Input to `verifyStoredEmailProof`. */\nexport type StoredEmailProofInput = {\n readonly record: StoredEmailProofRecord;\n readonly requirement: CredentialRequirement;\n readonly holderDid: string;\n readonly now: Date;\n readonly signal?: AbortSignal;\n};\n\n/**\n * Offline re-verification of a stored email-proof credential against the pinned issuer key.\n * Rebuilds the envelope from the record, runs the issuance verifier, then cross-checks the\n * record fields the signature does not cover. Every failure throws CredentialError.\n */\nexport async function verifyStoredEmailProof(input: StoredEmailProofInput): Promise<VerifiedCredential> {\n return verifyStoredWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);\n}\n\n/** Internal test seam, same as `acquireEmailProofWithIssuerKeys`. Not re-exported from the barrel. */\nexport async function verifyStoredWithIssuerKeys(input: StoredEmailProofInput, issuerKeys: readonly IssuerVerificationKey[]): Promise<VerifiedCredential> {\n const { record, requirement, holderDid } = input;\n const descriptor = record.descriptor;\n if (record.descriptorDigest !== EMAIL_PROOF_PINS.descriptorDigest || await canonicalDigest(descriptor) !== EMAIL_PROOF_PINS.descriptorDigest) {\n throw new CredentialError(\"VERIFICATION_FAILED\", \"stored descriptor does not match the pinned descriptor digest\");\n }\n const envelope: IssuedCredentialEnvelope = {\n type: \"OpenCredentialsIssuedCredential\",\n version: 1,\n protocol: descriptor.protocol,\n profile: record.profile,\n credentialType: record.credentialType,\n schema: record.schema,\n format: \"vc+sd-jwt\",\n issuerDid: record.issuerDid,\n issuerKid: record.issuerKid,\n subjectDid: record.subjectDid,\n holderDid: record.holderDid,\n claims: record.claims,\n claimsDigest: record.claimsDigest,\n descriptorDigest: record.descriptorDigest,\n credentialId: record.credentialId,\n issuedAt: record.issuedAt,\n notBefore: record.notBefore,\n expiresAt: record.expiresAt,\n status: record.status,\n credential: record.credential,\n };\n const verified = await verifyIssuedCredential({\n envelope,\n descriptor,\n descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest,\n requirement,\n holderDid,\n issuerMetadata: pinnedIssuerMetadata(issuerKeys),\n now: input.now,\n checkStatus: async (status) => status.method === \"none\" && status.freshnessSeconds === descriptor.status.freshnessSeconds,\n signal: input.signal,\n });\n // Fields outside the signed envelope: each must agree with what the verifier just proved.\n if (record.holderDid !== holderDid || record.ownerDid !== holderDid) throw new CredentialError(\"HOLDER_MISMATCH\", \"stored record belongs to another holder\");\n if (record.credentialDigest !== verified.credentialDigest) throw new CredentialError(\"VERIFICATION_FAILED\", \"stored credential digest does not match the verified credential\");\n if (record.requirementDigest !== await credentialRequirementDigest(requirement)) throw new CredentialError(\"VERIFICATION_FAILED\", \"stored requirement digest does not match the requirement\");\n if (record.recordId !== await canonicalDigest({ credentialDigest: verified.credentialDigest, holderDid: record.ownerDid })) throw new CredentialError(\"VERIFICATION_FAILED\", \"stored record id does not match its credential\");\n return verified;\n}\n\n/**\n * Held result (S13): verify the SD-JWT with the held verifier against the PINNED issuer, then cross-check the envelope against the\n * credential and the request. The held verifier does not compare the credential `jti` with the signed binding or the disclosed address\n * with the request, so those checks are here. Any failure is VERIFICATION_FAILED (CREDENTIAL_INVALID at step credential).\n */\nasync function verifyHeldResult(input: {\n readonly envelope: IssuedCredentialEnvelope;\n readonly pending: EmailProofPending;\n readonly descriptor: CredentialFlowDescriptor;\n readonly descriptorDigest: string;\n readonly transport: OpenCredentialsHttpTransport;\n readonly issuerKeys: readonly IssuerVerificationKey[];\n readonly now: () => Date;\n}): Promise<VerifiedHeldEmailCredential> {\n const { envelope, pending, descriptor } = input;\n const refuse = (message: string) => new CredentialError(\"VERIFICATION_FAILED\", message);\n // A client more than 5 minutes behind the issuer is refused; otherwise verify at max(client, iat): the held verifier refuses now < nbf = iat.\n const iatMs = Date.parse(envelope.issuedAt);\n const clientMs = input.now().getTime();\n if (!Number.isFinite(iatMs) || iatMs - clientMs > CLOCK_SKEW_BOUND_MS) throw refuse(\"client clock is more than 5 minutes behind the issuer\");\n const key = input.issuerKeys.find((candidate) => candidate.kid === EMAIL_PROOF_PINS.issuerKid);\n if (!key) throw refuse(\"no pinned issuer key\");\n const issuerSet = parseIssuerSet(\n { format: \"tinycloud.credentials-issuers/v1\", version: 1, issuers: [{ issuerDid: EMAIL_PROOF_PINS.issuerDid, kid: EMAIL_PROOF_PINS.issuerKid, alg: \"EdDSA\", publicKey: key.jwk.x, vcts: [EMAIL_PROOF_CREDENTIAL_TYPE.id], profiles: [HELD_PROFILE], notBefore: \"2020-01-01T00:00:00Z\", retiredAt: null, notAfter: null }] },\n { didMethods: { issuer: [\"web\"] } },\n );\n const verified = verifyHeldEmailCredential(envelope.credential, {\n now: new Date(Math.max(clientMs, iatMs)),\n owner: pending.holderDid,\n didMethods: { issuer: [\"web\"] },\n acceptedIssuers: [{ issuerDid: EMAIL_PROOF_PINS.issuerDid, kid: EMAIL_PROOF_PINS.issuerKid }],\n issuerSet,\n });\n const signed = input.transport.signedBinding(pending.requestId);\n const claims = Object.keys(envelope.claims).sort().join();\n const toSecond = (ms: number) => Math.floor(ms / 1000);\n if (\n // (i) the holder, as the one EIP-55 string\n envelope.subjectDid !== pending.holderDid || envelope.holderDid !== pending.holderDid ||\n // (ii) exactly {email, emailDomain}, equal to the credential's, and the requested mailbox\n claims !== \"email,emailDomain\" || envelope.claims.email !== verified.claims.email || envelope.claims.emailDomain !== verified.claims.emailDomain || envelope.claims.email !== pending.address ||\n // (iii) validity to the second\n toSecond(Date.parse(envelope.issuedAt)) !== toSecond(verified.issuedAt.getTime()) || toSecond(Date.parse(envelope.expiresAt)) !== toSecond(verified.expiresAt.getTime()) ||\n // (v) the pins\n envelope.descriptorDigest !== input.descriptorDigest || envelope.profile.id !== HELD_PROFILE || envelope.profile.version !== 1 || envelope.issuerDid !== EMAIL_PROOF_PINS.issuerDid ||\n envelope.issuerKid !== EMAIL_PROOF_PINS.issuerKid || envelope.format !== \"vc+sd-jwt\" || envelope.status.method !== \"none\" || envelope.status.freshnessSeconds !== descriptor.status.freshnessSeconds\n ) throw refuse(\"issued credential does not match the request\");\n // (vi) the credential's jti is the jti the account signed; (viii) its lifetime is the signed lifetime. (iv) and (vii) are checked by the transport.\n if (signed?.type !== descriptor.holderBinding.domain || !(\"credentialLifetimeSeconds\" in signed)) throw refuse(\"signed binding is missing\");\n if (verified.credentialId !== signed.jti || (verified.expiresAt.getTime() - verified.issuedAt.getTime()) / 1000 !== signed.credentialLifetimeSeconds) throw refuse(\"credential does not match the binding the account signed\");\n return verified;\n}\n\n/** Holder signing: the signer receives the raw binding bytes and returns a raw Ed25519 signature. The kid goes on the wire. */\nfunction holderSigning(holder: HolderSigner, held: boolean): CredentialSigningAdapter {\n return {\n autoSign: async (_binding, bytes) => {\n try {\n return await holder.sign(bytes);\n } catch (error) {\n // A2.3 (S14): in the held flow an error from the caller's wallet is a recoverable rejection of the signature, not a wrong code. The\n // signer guard's own refusal (an EmailProofError) and an abort or timeout keep their code.\n const aborted = error instanceof DOMException && (error.name === \"TimeoutError\" || error.name === \"AbortError\");\n if (!held || error instanceof EmailProofError || aborted) throw error;\n throw new EmailProofError(\"credential\", \"SIGNATURE_REJECTED\");\n }\n },\n holderKey: { alg: holder.alg, kid: holder.kid },\n };\n}\n\nfunction randomVerifier(): string {\n return encodeBase64Url(crypto.getRandomValues(new Uint8Array(32)));\n}\n\nfunction isPending(value: unknown): value is EmailProofPending {\n if (typeof value !== \"object\" || value === null) return false;\n const pending = value as EmailProofPending;\n return pending.type === \"TinyCloudEmailProofPending\"\n && pending.version === 1\n && pending.openerOrigin === OPENER_ORIGIN\n && [pending.profile, pending.address, pending.requestId, pending.verifier, pending.holderDid, pending.descriptorDigest, pending.requirementDigest, pending.openerOrigin, pending.createdAt, pending.expiresAt].every((field) => typeof field === \"string\")\n && Number.isFinite(Date.parse(pending.createdAt))\n && Number.isFinite(Date.parse(pending.expiresAt));\n}\n\n/** Maps any failure to a stable code. Caller aborts map to CANCELED; transient network failures stay recoverable. */\nfunction mapError(value: unknown, step: EmailProofStep, signal?: AbortSignal): EmailProofError {\n if (value instanceof EmailProofError) return value;\n if (value instanceof DOMException && value.name === \"TimeoutError\") return new EmailProofError(step, \"NETWORK\");\n const error = credentialError(value);\n const details = error.details;\n switch (error.code) {\n case \"ISSUER_UNREADY\":\n return details.state === \"rate_limited\"\n ? new EmailProofError(step, \"RATE_LIMITED\", details.retryAfterMs, error.recoverable)\n : new EmailProofError(step, \"ISSUER_UNAVAILABLE\", undefined, error.recoverable);\n case \"OFFLINE\":\n if (signal?.aborted && (signal.reason as { name?: string } | undefined)?.name !== \"TimeoutError\") return new EmailProofError(step, \"CANCELED\");\n return new EmailProofError(step, \"NETWORK\");\n case \"UNSUPPORTED_PROFILE\":\n case \"UNSUPPORTED_VERSION\":\n case \"DESCRIPTOR_INVALID\":\n return new EmailProofError(step, \"PROFILE_UNAVAILABLE\");\n case \"PROOF_REJECTED\":\n // The transport emits PROOF_REJECTED only for a server recoverable:true response.\n return new EmailProofError(\"code\", \"CODE_REJECTED\", undefined, true);\n case \"VERIFICATION_FAILED\":\n if (details.state === \"proof_attempts_exhausted\") return new EmailProofError(\"code\", \"CODE_ATTEMPTS_EXHAUSTED\");\n if (step === \"credential\") return new EmailProofError(\"credential\", \"CREDENTIAL_INVALID\");\n // Unclassified rejection at the code step: not recoverable, so the pending is deleted.\n if (step === \"code\") return new EmailProofError(\"code\", \"CODE_REJECTED\", undefined, false);\n return new EmailProofError(step, \"FAILED\");\n case \"HOLDER_MISMATCH\":\n return new EmailProofError(\"credential\", \"CREDENTIAL_INVALID\");\n case \"REQUEST_EXPIRED\":\n return new EmailProofError(\"session\", \"SESSION_EXPIRED\");\n case \"REQUEST_SUBSTITUTED\":\n return new EmailProofError(\"session\", \"SESSION_INVALID\");\n case \"SIGNATURE_REJECTED\":\n return new EmailProofError(\"credential\", \"SIGNATURE_REJECTED\", undefined, error.recoverable);\n case \"CANCELED\":\n return new EmailProofError(step, \"CANCELED\");\n default:\n return new EmailProofError(step, \"FAILED\");\n }\n}\n","import {\n CredentialError,\n encodeBase64Url,\n holderBindingSigningBytes,\n validateCredentialHolderBinding,\n type CredentialFlowDescriptor,\n type CredentialProgressEvent,\n type CredentialRequirement,\n} from \"@tinycloud/sdk-core\";\nimport { assertHeldBinding } from \"./held-binding\";\nimport type { CredentialAcquisitionTransport, CredentialProofSubject, CredentialSigningAdapter, InlineCredentialProofHandler, InlineCredentialProofRequest, PrimitiveStepHandler } from \"./types\";\n\nconst delay = (ms: number, signal?: AbortSignal) => new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\"abort\", () => { clearTimeout(timer); reject(new DOMException(\"Aborted\", \"AbortError\")); }, { once: true });\n});\n\n/** The mailbox named by an email requirement, which the issuer delivers the code to. */\nexport function mailboxSubject(requirement: CredentialRequirement): CredentialProofSubject | undefined {\n const email = requirement.claims.email;\n return typeof email === \"string\" && email.length > 0 ? Object.freeze({ kind: \"email\" as const, value: email }) : undefined;\n}\n\n/** Finite step interpreter. Dispatch is exclusively by registered primitive and version. */\nexport async function interpretCredentialFlow(input: {\n readonly descriptor: CredentialFlowDescriptor;\n readonly requirement: CredentialRequirement;\n readonly requestId: string;\n readonly verifier: string;\n readonly holderDid: string;\n readonly descriptorDigest: string;\n readonly requirementDigest: string;\n readonly openerOrigin: string;\n /** `expiresAt` of the create response. Required for a held (account) descriptor: the signed binding must carry it. */\n readonly expiresAt?: string;\n readonly transport: CredentialAcquisitionTransport;\n readonly signing: CredentialSigningAdapter;\n /**\n * Fallback for a host-owned surface. It receives every declared proof step,\n * including primitives added by a later protocol version.\n */\n readonly proofHandler?: InlineCredentialProofHandler;\n readonly handlers?: Partial<Record<\"collect_input\" | \"mailbox_otp\", PrimitiveStepHandler>>;\n readonly signal?: AbortSignal;\n readonly onProgress?: (event: CredentialProgressEvent) => void;\n readonly onWait?: () => Promise<void>;\n /**\n * Passed only by the email-proof functions, after the per-call held opt-in was accepted. Without it an account-bound (held, EIP191)\n * descriptor is refused before the first transport call (TC-2254 A2.5).\n */\n readonly allowAccountSubject?: true;\n}): Promise<void> {\n const held = input.descriptor.subjectRelationship === \"account_is_subject\";\n if (held && input.allowAccountSubject !== true) throw new CredentialError(\"UNSUPPORTED_PROFILE\", \"The held credential profile requires an explicit opt-in\");\n const steps = new Map(input.descriptor.steps.map((step) => [step.type, step]));\n const completed = new Set<string>(); let lastTransition = \"\";\n /** The identity checks on a holder binding, and for a held request all of S11 (a). One function, called wherever a held binding is read. */\n const checkBinding = async (value: unknown) => {\n const binding = validateCredentialHolderBinding(value);\n if (binding.requestId !== input.requestId || binding.descriptorDigest !== input.descriptorDigest || binding.requirementDigest !== input.requirementDigest || binding.holderDid !== input.holderDid || binding.issuer !== input.descriptor.issuer.did || binding.issuerKid !== input.descriptor.issuer.kid || binding.profile !== input.descriptor.profile || binding.openerOrigin !== input.openerOrigin || binding.completionOrigin !== input.openerOrigin || binding.audience !== \"tinycloud://credentials\") throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding was substituted\");\n const alg = input.descriptor.holderBinding.alg;\n if (binding.type !== input.descriptor.holderBinding.domain || (alg === \"EIP191\" && !input.transport.challengeNonce)) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding was substituted\");\n if (alg === \"EIP191\") await assertHeldBinding(binding, { descriptor: input.descriptor, requirement: input.requirement, requestId: input.requestId, descriptorDigest: input.descriptorDigest, requirementDigest: input.requirementDigest, holderDid: input.holderDid, openerOrigin: input.openerOrigin, challengeNonce: input.transport.challengeNonce!(input.requestId), serverExpiresAt: input.expiresAt });\n return binding;\n };\n /**\n * S11 (e): a held request found past the signature (a retry after a failed notice, or after a lost issue response) has no binding read in\n * this call. Read it and check it in full before R7 or R8, so (vi) and (viii) are judged against the binding of THIS call. No R6, no wallet.\n *\n * Residual risk, accepted (contract addendum A3(1), appended to A2.2): on the retry path the SDK cannot tell that the `jti` in the re-fetched\n * R5 binding is the `jti` the account signed at R6. A2.1(a) keeps no record of that `jti`, and the witness keeps no copy of the signature\n * (`OC/mod.rs:500,2416`). S11(a) therefore checks the re-fetched `jti` for format only (lowercase v4 `urn:uuid`). Checks (vi) and (viii)\n * compare the credential with the binding fetched in the same call.\n *\n * A witness that serves a different `jti` on the retry is detected only at (vi), after R7 has sent the notice, and only if it issues a `jti`\n * different from the one it served. If it issues the `jti` it served, the credential is accepted.\n *\n * This gives a compromised witness nothing it lacks as the pinned issuer. It can already issue a held credential for any account and mailbox\n * without an account signature. The held credential carries no account signature (`held_email.rs:303-317`), and no verifier relies on the\n * `jti` being account-signed (`held_presentation.rs:315-320,367`; `sdk-core/src/held-credentials/credential.ts:159,194`). Possession is proven\n * at presentation by the account's SIWE (`held_presentation.rs:351-359`).\n *\n * An honest witness serves the same binding on every R5 call (`binding_for` is deterministic, `OC/mod.rs:2657-2676`).\n */\n const ensureHeldBinding = async () => {\n if (!held || input.transport.signedBinding?.(input.requestId) !== undefined) return;\n await checkBinding(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));\n };\n for (;;) {\n input.signal?.throwIfAborted();\n const state = await input.transport.state(input.requestId, input.verifier, input.signal);\n if (state.requestId !== input.requestId) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential request identity changed\");\n if (state.state === \"expired\") throw new CredentialError(\"REQUEST_EXPIRED\", \"Credential request expired\", { state: state.state, correlationId: state.correlationId });\n if (state.state === \"canceled\") throw new CredentialError(\"CANCELED\", \"Credential request was canceled\", { state: state.state, correlationId: state.correlationId });\n if (state.state === \"issuer_unready\") throw new CredentialError(\"ISSUER_UNREADY\", \"Credential issuer is not ready\", { retryAfterMs: state.retryAfterMs, state: state.state, correlationId: state.correlationId });\n if (state.state === \"complete\") { await ensureHeldBinding(); return; }\n if (state.state === \"ready_to_issue\" || state.state === \"issued\") {\n await ensureHeldBinding();\n if (state.state === \"ready_to_issue\" && !completed.has(\"issue\")) { await input.transport.issue(input.requestId, input.verifier, input.signal); completed.add(\"issue\"); }\n else await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal));\n continue;\n }\n const next = state.nextStep;\n if (!next) { await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal)); continue; }\n const declared = steps.get(next.type);\n if (next.id !== next.type || !declared || declared.type !== next.type || declared.version !== next.version) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Server selected an undeclared credential step\");\n if (completed.has(next.id) || state.transitionId === lastTransition) { await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal)); continue; }\n lastTransition = state.transitionId;\n if (next.type === \"holder_signature\") {\n input.onProgress?.({ state: \"signing\", stepId: next.id, correlationId: state.correlationId });\n const binding = await checkBinding(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));\n // Held (account, EIP-191) and ticket (did:key, EdDSA) never mix: the descriptor, the binding type and the holder key agree, or nothing is signed.\n const alg = input.descriptor.holderBinding.alg;\n if ((alg === \"EIP191\") !== (input.signing.holderKey?.alg === \"EIP191\")) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding was substituted\");\n const bytes = holderBindingSigningBytes(binding);\n let signature = await input.signing.autoSign?.(binding, bytes, input.signal);\n if (signature === undefined) {\n if (!input.signing.requestApproval) throw new CredentialError(\"SIGNATURE_REJECTED\", \"OpenKey approval is required\");\n try { signature = await input.signing.requestApproval(binding, bytes, input.signal); } catch (cause) { if (cause instanceof CredentialError) throw cause; throw new CredentialError(\"SIGNATURE_REJECTED\", \"OpenKey approval was rejected\", { cause }); }\n }\n if (!(signature instanceof Uint8Array) || signature.length === 0) throw new CredentialError(\"SIGNATURE_REJECTED\", \"OpenKey signature is invalid\");\n await input.transport.submitHolderSignature(input.requestId, input.verifier, alg === \"EIP191\" ? `0x${Array.from(signature, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\")}` : encodeBase64Url(signature), input.signal, input.signing.holderKey);\n } else {\n input.onProgress?.({ state: next.type === \"collect_input\" ? \"collecting\" : \"proving\", stepId: next.id, correlationId: state.correlationId });\n const handler = input.handlers?.[next.type];\n const inlineHandler = input.proofHandler;\n if (!handler && !inlineHandler) { await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal)); continue; }\n if (next.constraints.challengeRequired === true) await input.transport.beginStep(input.requestId, input.verifier, next.type, input.signal);\n if (handler) {\n const proof = await handler({ descriptor: input.descriptor, requirement: input.requirement, stepId: next.id, constraints: next.constraints, signal: input.signal });\n await input.transport.submitStep(input.requestId, input.verifier, next.id, proof, input.signal);\n } else {\n let feedback: InlineCredentialProofRequest[\"feedback\"];\n // A mistyped code is re-entered against the same challenge; the issuer\n // counts every attempt and reports when the bounded budget is spent.\n for (let attempt = 1; ; attempt += 1) {\n const proof = await inlineHandler!({\n stepId: next.id,\n constraints: next.constraints,\n display: {\n title: input.descriptor.display.title,\n description: input.descriptor.display.description,\n consent: input.descriptor.display.consent,\n progressLabel: input.descriptor.accessibility.progressLabel,\n errorLiveRegion: input.descriptor.accessibility.errorLiveRegion,\n },\n inputs: input.descriptor.inputs.map(({ id, label, schema }) => ({ id, label, schema })),\n ...(feedback === undefined ? {} : { feedback }),\n signal: input.signal,\n });\n try {\n await input.transport.submitStep(input.requestId, input.verifier, next.id, proof, input.signal);\n break;\n } catch (cause) {\n if (!(cause instanceof CredentialError) || cause.code !== \"PROOF_REJECTED\") throw cause;\n const attemptsRemaining = input.descriptor.lifecycle.maxProofAttempts - attempt;\n // Once this view stops re-prompting, the rejection is final: a\n // host must not silently start a new acquisition on its behalf.\n if (next.type !== \"mailbox_otp\" || attemptsRemaining <= 0) throw new CredentialError(\"VERIFICATION_FAILED\", \"The proof was not accepted\", { state: attemptsRemaining <= 0 ? \"proof_attempts_exhausted\" : \"proof_rejected\", cause });\n feedback = { kind: \"rejected\", attemptsRemaining };\n }\n }\n }\n }\n completed.add(next.id);\n }\n}\n","import { canonicalDigest, CredentialError, type CredentialFlowDescriptor, type CredentialHolderBinding, type CredentialRequirement } from \"@tinycloud/sdk-core\";\nimport { HELD_ACCOUNT_BINDING_PINS as PINS } from \"./pins\";\n\n/** The witness's request window: the signed binding runs from request creation to creation + 600 s. */\nconst REQUEST_TTL_MS = 600_000;\n\n/** What the SDK itself computed for this request. The witness's binding must equal all of it before the wallet is called. */\nexport interface HeldBindingExpectation {\n readonly descriptor: CredentialFlowDescriptor;\n readonly requirement: CredentialRequirement;\n readonly requestId: string;\n readonly descriptorDigest: string;\n readonly requirementDigest: string;\n readonly holderDid: string;\n readonly openerOrigin: string;\n /** The nonce this client's transport stored from the challenge, or undefined if none was stored. */\n readonly challengeNonce: string | undefined;\n /** `expiresAt` of the R1 create response (ISO), or undefined if none was kept. */\n readonly serverExpiresAt: string | undefined;\n}\n\nconst substituted = (): CredentialError => new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding was substituted\");\n\n/**\n * Refuses (REQUEST_SUBSTITUTED) unless the held account binding is exactly what this client asked for: the domain comes from the\n * pinned descriptor, never from the response. The field shape, `protocol`, `profileVersion`, the 365-day lifetime and the `urn:uuid`\n * `jti` were already checked by `validateCredentialHolderBinding`. The `jti` itself cannot be known in advance (on the retry path: format only;\n * see the residual risk above `ensureHeldBinding` in the interpreter); it is kept by the caller and compared with the credential's after issuance.\n */\nexport async function assertHeldBinding(binding: CredentialHolderBinding, expected: HeldBindingExpectation): Promise<void> {\n const { descriptor } = expected;\n if (\n descriptor.holderBinding.alg !== \"EIP191\" ||\n binding.type !== descriptor.holderBinding.domain ||\n binding.requestId !== expected.requestId ||\n binding.descriptorDigest !== expected.descriptorDigest ||\n binding.requirementDigest !== expected.requirementDigest ||\n binding.holderDid !== expected.holderDid ||\n binding.issuer !== descriptor.issuer.did ||\n binding.issuerKid !== descriptor.issuer.kid ||\n binding.profile !== descriptor.profile ||\n binding.openerOrigin !== expected.openerOrigin ||\n binding.completionOrigin !== expected.openerOrigin ||\n binding.audience !== PINS.audience ||\n binding.completionContext !== PINS.completionContext ||\n // The static fields, against the one frozen object the request body and the account signer also use (A2.4).\n binding.type !== PINS.type || binding.protocol !== PINS.protocol || binding.profile !== PINS.profile || binding.profileVersion !== PINS.profileVersion ||\n binding.descriptorDigest !== PINS.descriptorDigest || binding.issuer !== PINS.issuer || binding.issuerKid !== PINS.issuerKid ||\n binding.openerOrigin !== PINS.openerOrigin || binding.completionOrigin !== PINS.completionOrigin || binding.credentialLifetimeSeconds !== PINS.credentialLifetimeSeconds\n ) throw substituted();\n if (expected.challengeNonce === undefined || binding.challengeNonce !== expected.challengeNonce) throw substituted();\n if (binding.normalizedClaimsDigest !== await canonicalDigest(expected.requirement.claims)) throw substituted();\n const expires = Date.parse(binding.expiresAt);\n if (expected.serverExpiresAt === undefined || expires !== Date.parse(expected.serverExpiresAt) || Date.parse(binding.issuedAt) !== expires - REQUEST_TTL_MS) throw substituted();\n}\n","import { ACCOUNT_HOLDER_BINDING_DOMAIN } from \"@tinycloud/sdk-core\";\n\n/** Production pins. Credentials verify only against these values. Tests pin them literally. */\nexport const EMAIL_PROOF_PINS = Object.freeze({\n witnessOrigin: \"https://witness.credentials.org\",\n issuerDid: \"did:web:issuer.credentials.org\",\n issuerKid: \"did:web:issuer.credentials.org#controller\",\n issuerX: \"eEJI4xEobto4HtQ7Pg9R1vBOwcpfGRTlXDG5QfqLnGQ\",\n descriptorDigest: \"1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0\",\n} as const);\n\n/**\n * The static fields of the held account binding (contract A2.4). One frozen object: the create request body, the binding checks\n * the SDK runs on the witness's binding (S11 (a)) and the account signer's guard all read these values, so the three cannot drift.\n * The per-request fields (`requestId`, `challengeNonce`, the digests of the request, `jti`, `issuedAt`, `expiresAt`) and the\n * signer's own `holderDid` are not static and are not here.\n */\nexport const HELD_ACCOUNT_BINDING_PINS = Object.freeze({\n type: ACCOUNT_HOLDER_BINDING_DOMAIN,\n protocol: \"tinycloud.credentials/acquisition/v1\",\n profile: \"tinycloud.email-held/v1\",\n profileVersion: 1,\n descriptorDigest: \"dc3_DuvzipXwYkTfjKqTaK1c3VS0YSPnaBsYSQriC0w\",\n issuer: EMAIL_PROOF_PINS.issuerDid,\n issuerKid: EMAIL_PROOF_PINS.issuerKid,\n audience: \"tinycloud://credentials\",\n openerOrigin: \"https://localhost\",\n completionOrigin: \"https://localhost\",\n completionContext: \"sdk-acquisition\",\n credentialLifetimeSeconds: 31_536_000,\n} as const);\n","import {\n CredentialError, canonicalDigest, credentialEndpointPath, decodeBase64Url, encodeBase64Url,\n holderBindingCanonicalBytes, sha256Base64Url, validateCredentialHolderBinding,\n type CredentialFlowDescriptor, type CredentialHolderBinding, type CredentialIssuerMetadata,\n type CredentialRequirement, type IssuedCredentialEnvelope,\n} from \"@tinycloud/sdk-core\";\nimport { HELD_ACCOUNT_BINDING_PINS } from \"./pins\";\nimport type { CredentialAcquisitionTransport, CredentialRequestState, HolderSignatureKey, PrimitiveStepResult } from \"./types\";\n\nfunction object(value: unknown, label: string): Record<string, unknown> { if (typeof value !== \"object\" || value === null || Array.isArray(value)) throw new CredentialError(\"VERIFICATION_FAILED\", `${label} is invalid`); return value as Record<string, unknown>; }\nfunction opaque(value: unknown, label: string): string { if (typeof value !== \"string\" || !/^[A-Za-z0-9_-]{16,128}$/.test(value)) throw new CredentialError(\"REQUEST_SUBSTITUTED\", `${label} is invalid`); return value; }\nfunction timestamp(value: unknown, label: string): string { if (typeof value !== \"string\" || !Number.isFinite(Date.parse(value))) throw new CredentialError(\"VERIFICATION_FAILED\", `${label} is invalid`); return new Date(value).toISOString(); }\nfunction jwtPayload(credential: string): Record<string, unknown> { const part = credential.split(\"~\", 1)[0]?.split(\".\")[1]; if (!part) throw new CredentialError(\"VERIFICATION_FAILED\", \"Issued SD-JWT is invalid\"); try { return object(JSON.parse(new TextDecoder().decode(decodeBase64Url(part))), \"SD-JWT payload\"); } catch (cause) { if (cause instanceof CredentialError) throw cause; throw new CredentialError(\"VERIFICATION_FAILED\", \"Issued SD-JWT is invalid\", { cause }); } }\nconst SERVER_CODES = new Set([\"REQUEST_EXPIRED\", \"ISSUER_UNREADY\", \"UNSUPPORTED_PROFILE\", \"UNSUPPORTED_VERSION\", \"SIGNATURE_REJECTED\"] as const);\nfunction serverError(value: unknown, response: Response, honorServerRecoverable: boolean): CredentialError | undefined {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return undefined;\n const body = value as Record<string, unknown>;\n const keys = Object.keys(body).sort();\n if (keys.join(\",\") !== \"code,correlationId,recoverable,state,type\" || body.type !== \"tinycloud.credentials/error/v1\" || typeof body.code !== \"string\" || typeof body.recoverable !== \"boolean\" || typeof body.state !== \"string\" || typeof body.correlationId !== \"string\") return undefined;\n // A rejected proof is retryable only while the issuer still has attempts\n // left for this challenge; once exhausted the acquisition cannot finish.\n if (body.code === \"PROOF_REJECTED\") return body.recoverable\n ? new CredentialError(\"PROOF_REJECTED\", \"The proof was not accepted\", { state: body.state, correlationId: body.correlationId })\n : new CredentialError(\"VERIFICATION_FAILED\", \"The proof attempt limit was reached\", { state: \"proof_attempts_exhausted\", correlationId: body.correlationId });\n if (body.code === \"RATE_LIMITED\") return new CredentialError(\"ISSUER_UNREADY\", \"Too many verification codes were requested\", { state: \"rate_limited\", correlationId: body.correlationId, retryAfterMs: Number(response.headers.get(\"retry-after\")) * 1000 || undefined, recoverable: honorServerRecoverable ? body.recoverable : undefined });\n if (body.code === \"CHALLENGE_EXPIRED\") return new CredentialError(\"REQUEST_EXPIRED\", \"The credential challenge expired\", { state: body.state, correlationId: body.correlationId, recoverable: honorServerRecoverable ? body.recoverable : undefined });\n // The witness changed its descriptor between the catalog read and the create: the pinned profile is no longer offered.\n if (body.code === \"DESCRIPTOR_MISMATCH\") body.code = \"UNSUPPORTED_PROFILE\";\n if (!SERVER_CODES.has(body.code as any)) return undefined;\n const code = body.code as \"REQUEST_EXPIRED\" | \"ISSUER_UNREADY\" | \"UNSUPPORTED_PROFILE\" | \"UNSUPPORTED_VERSION\" | \"SIGNATURE_REJECTED\";\n return new CredentialError(code, `OpenCredentials rejected the request: ${code}`, {\n state: body.state,\n correlationId: body.correlationId,\n retryAfterMs: Number(response.headers.get(\"retry-after\")) * 1000 || undefined,\n recoverable: honorServerRecoverable ? body.recoverable : undefined,\n });\n}\n\nexport class OpenCredentialsHttpTransport implements CredentialAcquisitionTransport {\n private readonly nonces = new Map<string, string>();\n private readonly bindings = new Map<string, CredentialHolderBinding>();\n /**\n * `allowAccountSubject` is passed only by the email-proof functions, after the per-call held opt-in was accepted. Without it an account-bound\n * (held) descriptor is refused here, before any request (TC-2254 A2.5).\n */\n constructor(private readonly descriptor: CredentialFlowDescriptor, private readonly fetchFn: typeof fetch = globalThis.fetch.bind(globalThis), private readonly options: { readonly honorServerRecoverable?: boolean; readonly allowAccountSubject?: true } = {}) {\n if (descriptor.subjectRelationship === \"account_is_subject\" && options.allowAccountSubject !== true) throw new CredentialError(\"UNSUPPORTED_PROFILE\", \"The held credential profile requires an explicit opt-in\");\n }\n private url(endpoint: Parameters<typeof credentialEndpointPath>[0], id?: string): string { return new URL(credentialEndpointPath(endpoint, id), this.descriptor.issuer.origin).href; }\n private async request(endpoint: Parameters<typeof credentialEndpointPath>[0], input: { readonly id?: string; readonly method?: \"GET\" | \"POST\"; readonly verifier?: string; readonly body?: unknown; readonly signal?: AbortSignal }): Promise<unknown> {\n let response: Response;\n try { const init: RequestInit & { readonly cache: \"no-store\" } = { method: input.method ?? \"GET\", credentials: \"omit\", cache: \"no-store\", redirect: \"error\", referrerPolicy: \"no-referrer\", signal: input.signal, headers: { accept: \"application/json\", ...(input.body === undefined ? {} : { \"content-type\": \"application/json\" }), ...(input.verifier === undefined ? {} : { authorization: `Bearer ${input.verifier}` }) }, ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }) }; response = await this.fetchFn(this.url(endpoint, input.id), init); }\n catch (cause) { if (cause instanceof DOMException && cause.name === \"AbortError\") throw new CredentialError(\"CANCELED\", \"Credential acquisition was canceled\", { cause }); throw new CredentialError(\"OFFLINE\", \"OpenCredentials is unavailable\", { cause }); }\n if (!response.ok) {\n let parsed: unknown;\n try { parsed = await response.json(); } catch { parsed = undefined; }\n const typed = serverError(parsed, response, this.options.honorServerRecoverable === true);\n if (typed) throw typed;\n if (response.status === 410) throw new CredentialError(\"REQUEST_EXPIRED\", \"Credential request expired\");\n if (response.status === 409) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential request was replayed or substituted\");\n if (response.status === 503) throw new CredentialError(\"ISSUER_UNREADY\", \"Credential issuer is not ready\", { retryAfterMs: Number(response.headers.get(\"retry-after\")) * 1000 || undefined });\n throw new CredentialError(\"VERIFICATION_FAILED\", \"OpenCredentials rejected the request\");\n }\n if (response.status === 204) return undefined; try { return await response.json(); } catch (cause) { throw new CredentialError(\"VERIFICATION_FAILED\", \"OpenCredentials response is invalid\", { cause }); }\n }\n async create(input: { descriptor: CredentialFlowDescriptor; descriptorDigest: string; requirement: CredentialRequirement; requirementDigest: string; holderDid: string; openerOrigin: string; completionVerifierChallenge: string; inputs?: Readonly<Record<string, string>>; signal?: AbortSignal }) {\n const body = object(await this.request(\"request\", { method: \"POST\", body: { protocol: input.descriptor.protocol, profile: input.descriptor.profile, profileVersion: input.descriptor.profileVersion, descriptorDigest: input.descriptorDigest, requirementDigest: input.requirementDigest, holderDid: input.holderDid, inputs: input.inputs ?? input.requirement.claims, audience: HELD_ACCOUNT_BINDING_PINS.audience, openerOrigin: input.openerOrigin, completionOrigin: input.openerOrigin, completionContext: HELD_ACCOUNT_BINDING_PINS.completionContext, completionVerifierChallenge: input.completionVerifierChallenge }, signal: input.signal }), \"create response\");\n if (body.type !== \"tinycloud.credentials/acquisition-request/v1\" || body.protocol !== input.descriptor.protocol || body.next !== \"challenge\" || body.endpoint !== \"challenge\") throw new CredentialError(\"VERIFICATION_FAILED\", \"create response is invalid\");\n const requestId = opaque(body.requestId, \"requestId\"); return { requestId, locator: requestId, expiresAt: timestamp(body.expiresAt, \"expiresAt\"), correlationId: requestId };\n }\n /** The nonce stored from R3 or R2 for this request. Held acquisition compares the signed binding with it. */\n challengeNonce(id: string): string | undefined { return this.nonces.get(id); }\n /** The binding fetched and validated for this request, as the account signed it. */\n signedBinding(id: string): CredentialHolderBinding | undefined { return this.bindings.get(id); }\n private proofStep(): \"mailbox_otp\" | \"collect_input\" { return this.descriptor.steps.some((step) => step.type === \"mailbox_otp\") ? \"mailbox_otp\" : \"collect_input\"; }\n async state(id: string, verifier: string, signal?: AbortSignal): Promise<CredentialRequestState> {\n const raw = object(await this.request(\"state\", { id, verifier, signal }), \"state response\");\n if (raw.type !== \"tinycloud.credentials/acquisition-state/v1\" || raw.requestId !== id || raw.profile !== this.descriptor.profile || raw.profileVersion !== 1) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential state response is substituted\");\n const base = { type: \"OpenCredentialsAcquisitionState\" as const, version: 1 as const, requestId: id, transitionId: `${String(raw.state)}:${String(raw.resultPreviouslyRead ?? false)}`, correlationId: id };\n if (raw.state === \"challenge_required\") { const type = this.proofStep(); return { ...base, state: \"pending\", nextStep: { id: type, type, version: 1, constraints: { challengeRequired: true } } }; }\n // Held: the challenge nonce is read from R2 in every state after `challenge_required`, so a call that resumes the request (a retry after\n // a failed wallet prompt or a failed notice) has it. A missing nonce, or one that differs from the one this transport already holds, is a substitution.\n if (this.descriptor.subjectRelationship === \"account_is_subject\" && (raw.state === \"proof_required\" || raw.state === \"holder_binding_required\" || raw.state === \"holder_signature_required\" || raw.state === \"ready_to_issue\" || raw.state === \"issued\")) {\n const nonce = raw.challengeNonce;\n const prior = this.nonces.get(id);\n if (typeof nonce !== \"string\" || nonce === \"\" || (prior !== undefined && prior !== nonce)) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential challenge nonce is missing or changed\");\n this.nonces.set(id, opaque(nonce, \"challengeNonce\"));\n }\n if (raw.state === \"proof_required\") { const type = this.proofStep(); if (this.descriptor.subjectRelationship !== \"account_is_subject\") this.nonces.set(id, opaque(raw.challengeNonce, \"challengeNonce\")); return { ...base, state: \"pending\", nextStep: { id: type, type, version: 1, constraints: {} } }; }\n if (raw.state === \"holder_binding_required\" || raw.state === \"holder_signature_required\") return { ...base, state: \"pending\", nextStep: { id: \"holder_signature\", type: \"holder_signature\", version: 1, constraints: {} } };\n if (raw.state === \"ready_to_issue\") return { ...base, state: \"ready_to_issue\" };\n if (raw.state === \"issued\") return { ...base, state: \"complete\" };\n throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential state is unsupported\");\n }\n async beginStep(id: string, verifier: string, stepId: \"collect_input\" | \"mailbox_otp\", signal?: AbortSignal): Promise<void> { const step = this.proofStep(); if (stepId !== step) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential challenge step is substituted\"); const challenge = object(await this.request(\"challenge\", { id, verifier, method: \"POST\", body: { step, stepVersion: 1 }, signal }), \"challenge response\"); if (challenge.type !== \"tinycloud.credentials/challenge/v1\" || challenge.step !== step || challenge.stepVersion !== 1) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential challenge is substituted\"); this.nonces.set(id, opaque(challenge.challengeNonce, \"challengeNonce\")); }\n async submitStep(id: string, verifier: string, stepId: string, proof: PrimitiveStepResult, signal?: AbortSignal): Promise<void> { const step = this.proofStep(); if (stepId !== step) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential proof step is substituted\"); const nonce = this.nonces.get(id); if (!nonce) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Credential challenge is missing\"); await this.request(\"proof\", { id, verifier, method: \"POST\", body: { step, stepVersion: 1, challengeNonce: nonce, proof }, signal }); }\n async holderBinding(id: string, verifier: string, signal?: AbortSignal): Promise<CredentialHolderBinding> { const raw = object(await this.request(\"holder_binding\", { id, verifier, signal }), \"holder binding response\"); if (raw.type !== \"tinycloud.credentials/holder-binding-request/v1\" || raw.signingDomain !== this.descriptor.holderBinding.domain || raw.alg !== this.descriptor.holderBinding.alg || typeof raw.canonical !== \"string\") throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding response is invalid\"); const binding = validateCredentialHolderBinding(raw.binding); if (binding.type !== raw.signingDomain) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding type does not match its signing domain\"); if (encodeBase64Url(holderBindingCanonicalBytes(binding)) !== raw.canonical) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding canonical bytes were substituted\"); this.bindings.set(id, binding); return binding; }\n async submitHolderSignature(id: string, verifier: string, signature: string, signal?: AbortSignal, key?: HolderSignatureKey): Promise<void> { const binding = this.bindings.get(id); if (!binding) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"Holder binding was not retrieved\"); if (this.descriptor.holderBinding.alg === \"EIP191\" && !key) throw new CredentialError(\"REQUEST_SUBSTITUTED\", \"An account holder must name its key and algorithm\"); const alg = key?.alg ?? \"EdDSA\"; const kid = key?.kid ?? `${binding.holderDid}#${binding.holderDid.replace(\"did:key:\", \"\")}`; await this.request(\"holder_signature\", { id, verifier, method: \"POST\", body: { alg, kid, signature }, signal }); }\n async issue(id: string, verifier: string, signal?: AbortSignal): Promise<void> { await this.request(\"issue\", { id, verifier, method: \"POST\", signal }); }\n async result(id: string, verifier: string, signal?: AbortSignal): Promise<IssuedCredentialEnvelope> {\n const raw = object(await this.request(\"result\", { id, verifier, signal }), \"result response\"); if (raw.type !== \"tinycloud.credentials/result/v1\" || raw.protocol !== this.descriptor.protocol || raw.profile !== this.descriptor.profile || raw.profileVersion !== 1 || raw.format !== \"vc+sd-jwt\" || raw.descriptorDigest !== await canonicalDigest(this.descriptor) || raw.issuer !== this.descriptor.issuer.did || raw.issuerKid !== this.descriptor.issuer.kid || typeof raw.credential !== \"string\" || await sha256Base64Url(raw.credential) !== raw.credentialDigest) throw new CredentialError(\"VERIFICATION_FAILED\", \"Issued result metadata is invalid\");\n const holder = object(raw.holderBinding, \"result holder binding\"); if (raw.subject !== holder.did || holder.signingDomain !== this.descriptor.holderBinding.domain) throw new CredentialError(\"HOLDER_MISMATCH\", \"Issued holder binding is invalid\");\n const claimsRaw = object(raw.claims, \"result claims\"); const claims: Record<string, string> = {}; for (const [name, value] of Object.entries(claimsRaw)) { if (typeof value !== \"string\") throw new CredentialError(\"VERIFICATION_FAILED\", \"Issued claims are invalid\"); claims[name] = value; }\n const validity = object(raw.validity, \"result validity\"); const payload = jwtPayload(raw.credential); if (typeof payload.jti !== \"string\") throw new CredentialError(\"VERIFICATION_FAILED\", \"Credential id is invalid\"); const issuedAt = timestamp(validity.issuedAt, \"issuedAt\"); const expiresAt = timestamp(validity.expiresAt, \"expiresAt\");\n const status = object(raw.status, \"result status\"); if (status.type !== \"none\" || status.freshnessSeconds !== this.descriptor.status.freshnessSeconds) throw new CredentialError(\"VERIFICATION_FAILED\", \"Credential status is invalid\");\n return { type: \"OpenCredentialsIssuedCredential\", version: 1, protocol: this.descriptor.protocol, profile: { id: this.descriptor.profile, version: 1 }, credentialType: { id: this.descriptor.format.vct, version: 1 }, schema: this.descriptor.format.vct, format: \"vc+sd-jwt\", issuerDid: raw.issuer as string, issuerKid: raw.issuerKid as string, subjectDid: raw.subject as string, holderDid: raw.subject as string, claims, claimsDigest: await canonicalDigest(claims), descriptorDigest: raw.descriptorDigest as string, credentialId: payload.jti, issuedAt, notBefore: issuedAt, expiresAt, status: { method: \"none\", freshnessSeconds: status.freshnessSeconds as number }, credential: raw.credential };\n }\n async issuerMetadata(signal?: AbortSignal): Promise<CredentialIssuerMetadata> { return await this.request(\"issuer_metadata\", { signal }) as CredentialIssuerMetadata; }\n async checkStatus(status: IssuedCredentialEnvelope[\"status\"], _signal?: AbortSignal): Promise<boolean> { return status.method === \"none\" && status.freshnessSeconds === this.descriptor.status.freshnessSeconds; }\n}\n"],"mappings":"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","names":["import_sdk_core","import_sdk_core","import_sdk_core","import_sdk_core"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as EmailProofPendingStore, a as EmailProofPending } from './email-proof-B4DRizIL.cjs';
|
|
2
2
|
import '@tinycloud/sdk-core';
|
|
3
|
+
import '@tinycloud/sdk-core/held-credentials';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* File-backed pending store for the email-proof flow (`@tinycloud/node-sdk/credential-pending-store`).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { E as EmailProofPendingStore, a as EmailProofPending } from './email-proof-B4DRizIL.js';
|
|
2
2
|
import '@tinycloud/sdk-core';
|
|
3
|
+
import '@tinycloud/sdk-core/held-credentials';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* File-backed pending store for the email-proof flow (`@tinycloud/node-sdk/credential-pending-store`).
|