@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.
@@ -23,7 +23,9 @@ __export(credentials_exports, {
23
23
  DEFAULT_EMAIL_PROOF_PROFILE: () => DEFAULT_EMAIL_PROOF_PROFILE,
24
24
  EMAIL_PROOF_CREDENTIAL_TYPE: () => EMAIL_PROOF_CREDENTIAL_TYPE,
25
25
  EMAIL_PROOF_PENDING_MAX_MS: () => EMAIL_PROOF_PENDING_MAX_MS,
26
+ EMAIL_PROOF_PINS: () => EMAIL_PROOF_PINS,
26
27
  EmailProofError: () => EmailProofError,
28
+ HELD_ACCOUNT_BINDING_PINS: () => HELD_ACCOUNT_BINDING_PINS,
27
29
  OpenCredentialsHttpTransport: () => OpenCredentialsHttpTransport,
28
30
  acquireEmailProof: () => acquireEmailProof,
29
31
  completeEmailProof: () => completeEmailProof,
@@ -31,6 +33,7 @@ __export(credentials_exports, {
31
33
  createMemoryEmailProofPendingStore: () => createMemoryEmailProofPendingStore,
32
34
  emailProofRequirement: () => emailProofRequirement,
33
35
  interpretCredentialFlow: () => interpretCredentialFlow,
36
+ isTicketEmailProofResult: () => isTicketEmailProofResult,
34
37
  mailboxSubject: () => mailboxSubject,
35
38
  requestEmailProofCode: () => requestEmailProofCode,
36
39
  verifyStoredEmailProof: () => verifyStoredEmailProof
@@ -38,7 +41,49 @@ __export(credentials_exports, {
38
41
  module.exports = __toCommonJS(credentials_exports);
39
42
 
40
43
  // src/credentials/interpreter.ts
44
+ var import_sdk_core3 = require("@tinycloud/sdk-core");
45
+
46
+ // src/credentials/held-binding.ts
47
+ var import_sdk_core2 = require("@tinycloud/sdk-core");
48
+
49
+ // src/credentials/pins.ts
41
50
  var import_sdk_core = require("@tinycloud/sdk-core");
51
+ var EMAIL_PROOF_PINS = Object.freeze({
52
+ witnessOrigin: "https://witness.credentials.org",
53
+ issuerDid: "did:web:issuer.credentials.org",
54
+ issuerKid: "did:web:issuer.credentials.org#controller",
55
+ issuerX: "eEJI4xEobto4HtQ7Pg9R1vBOwcpfGRTlXDG5QfqLnGQ",
56
+ descriptorDigest: "1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0"
57
+ });
58
+ var HELD_ACCOUNT_BINDING_PINS = Object.freeze({
59
+ type: import_sdk_core.ACCOUNT_HOLDER_BINDING_DOMAIN,
60
+ protocol: "tinycloud.credentials/acquisition/v1",
61
+ profile: "tinycloud.email-held/v1",
62
+ profileVersion: 1,
63
+ descriptorDigest: "dc3_DuvzipXwYkTfjKqTaK1c3VS0YSPnaBsYSQriC0w",
64
+ issuer: EMAIL_PROOF_PINS.issuerDid,
65
+ issuerKid: EMAIL_PROOF_PINS.issuerKid,
66
+ audience: "tinycloud://credentials",
67
+ openerOrigin: "https://localhost",
68
+ completionOrigin: "https://localhost",
69
+ completionContext: "sdk-acquisition",
70
+ credentialLifetimeSeconds: 31536e3
71
+ });
72
+
73
+ // src/credentials/held-binding.ts
74
+ var REQUEST_TTL_MS = 6e5;
75
+ var substituted = () => new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
76
+ async function assertHeldBinding(binding, expected) {
77
+ const { descriptor } = expected;
78
+ if (descriptor.holderBinding.alg !== "EIP191" || binding.type !== descriptor.holderBinding.domain || binding.requestId !== expected.requestId || binding.descriptorDigest !== expected.descriptorDigest || binding.requirementDigest !== expected.requirementDigest || binding.holderDid !== expected.holderDid || binding.issuer !== descriptor.issuer.did || binding.issuerKid !== descriptor.issuer.kid || binding.profile !== descriptor.profile || binding.openerOrigin !== expected.openerOrigin || binding.completionOrigin !== expected.openerOrigin || binding.audience !== HELD_ACCOUNT_BINDING_PINS.audience || binding.completionContext !== HELD_ACCOUNT_BINDING_PINS.completionContext || // The static fields, against the one frozen object the request body and the account signer also use (A2.4).
79
+ binding.type !== HELD_ACCOUNT_BINDING_PINS.type || binding.protocol !== HELD_ACCOUNT_BINDING_PINS.protocol || binding.profile !== HELD_ACCOUNT_BINDING_PINS.profile || binding.profileVersion !== HELD_ACCOUNT_BINDING_PINS.profileVersion || binding.descriptorDigest !== HELD_ACCOUNT_BINDING_PINS.descriptorDigest || binding.issuer !== HELD_ACCOUNT_BINDING_PINS.issuer || binding.issuerKid !== HELD_ACCOUNT_BINDING_PINS.issuerKid || binding.openerOrigin !== HELD_ACCOUNT_BINDING_PINS.openerOrigin || binding.completionOrigin !== HELD_ACCOUNT_BINDING_PINS.completionOrigin || binding.credentialLifetimeSeconds !== HELD_ACCOUNT_BINDING_PINS.credentialLifetimeSeconds) throw substituted();
80
+ if (expected.challengeNonce === void 0 || binding.challengeNonce !== expected.challengeNonce) throw substituted();
81
+ if (binding.normalizedClaimsDigest !== await (0, import_sdk_core2.canonicalDigest)(expected.requirement.claims)) throw substituted();
82
+ const expires = Date.parse(binding.expiresAt);
83
+ if (expected.serverExpiresAt === void 0 || expires !== Date.parse(expected.serverExpiresAt) || Date.parse(binding.issuedAt) !== expires - REQUEST_TTL_MS) throw substituted();
84
+ }
85
+
86
+ // src/credentials/interpreter.ts
42
87
  var delay = (ms, signal) => new Promise((resolve, reject) => {
43
88
  const timer = setTimeout(resolve, ms);
44
89
  signal?.addEventListener("abort", () => {
@@ -51,18 +96,36 @@ function mailboxSubject(requirement) {
51
96
  return typeof email === "string" && email.length > 0 ? Object.freeze({ kind: "email", value: email }) : void 0;
52
97
  }
53
98
  async function interpretCredentialFlow(input) {
99
+ const held = input.descriptor.subjectRelationship === "account_is_subject";
100
+ if (held && input.allowAccountSubject !== true) throw new import_sdk_core3.CredentialError("UNSUPPORTED_PROFILE", "The held credential profile requires an explicit opt-in");
54
101
  const steps = new Map(input.descriptor.steps.map((step) => [step.type, step]));
55
102
  const completed = /* @__PURE__ */ new Set();
56
103
  let lastTransition = "";
104
+ const checkBinding = async (value) => {
105
+ const binding = (0, import_sdk_core3.validateCredentialHolderBinding)(value);
106
+ 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 import_sdk_core3.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
107
+ const alg = input.descriptor.holderBinding.alg;
108
+ if (binding.type !== input.descriptor.holderBinding.domain || alg === "EIP191" && !input.transport.challengeNonce) throw new import_sdk_core3.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
109
+ 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 });
110
+ return binding;
111
+ };
112
+ const ensureHeldBinding = async () => {
113
+ if (!held || input.transport.signedBinding?.(input.requestId) !== void 0) return;
114
+ await checkBinding(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));
115
+ };
57
116
  for (; ; ) {
58
117
  input.signal?.throwIfAborted();
59
118
  const state = await input.transport.state(input.requestId, input.verifier, input.signal);
60
- if (state.requestId !== input.requestId) throw new import_sdk_core.CredentialError("REQUEST_SUBSTITUTED", "Credential request identity changed");
61
- if (state.state === "expired") throw new import_sdk_core.CredentialError("REQUEST_EXPIRED", "Credential request expired", { state: state.state, correlationId: state.correlationId });
62
- if (state.state === "canceled") throw new import_sdk_core.CredentialError("CANCELED", "Credential request was canceled", { state: state.state, correlationId: state.correlationId });
63
- if (state.state === "issuer_unready") throw new import_sdk_core.CredentialError("ISSUER_UNREADY", "Credential issuer is not ready", { retryAfterMs: state.retryAfterMs, state: state.state, correlationId: state.correlationId });
64
- if (state.state === "complete") return;
119
+ if (state.requestId !== input.requestId) throw new import_sdk_core3.CredentialError("REQUEST_SUBSTITUTED", "Credential request identity changed");
120
+ if (state.state === "expired") throw new import_sdk_core3.CredentialError("REQUEST_EXPIRED", "Credential request expired", { state: state.state, correlationId: state.correlationId });
121
+ if (state.state === "canceled") throw new import_sdk_core3.CredentialError("CANCELED", "Credential request was canceled", { state: state.state, correlationId: state.correlationId });
122
+ if (state.state === "issuer_unready") throw new import_sdk_core3.CredentialError("ISSUER_UNREADY", "Credential issuer is not ready", { retryAfterMs: state.retryAfterMs, state: state.state, correlationId: state.correlationId });
123
+ if (state.state === "complete") {
124
+ await ensureHeldBinding();
125
+ return;
126
+ }
65
127
  if (state.state === "ready_to_issue" || state.state === "issued") {
128
+ await ensureHeldBinding();
66
129
  if (state.state === "ready_to_issue" && !completed.has("issue")) {
67
130
  await input.transport.issue(input.requestId, input.verifier, input.signal);
68
131
  completed.add("issue");
@@ -75,7 +138,7 @@ async function interpretCredentialFlow(input) {
75
138
  continue;
76
139
  }
77
140
  const declared = steps.get(next.type);
78
- if (next.id !== next.type || !declared || declared.type !== next.type || declared.version !== next.version) throw new import_sdk_core.CredentialError("REQUEST_SUBSTITUTED", "Server selected an undeclared credential step");
141
+ if (next.id !== next.type || !declared || declared.type !== next.type || declared.version !== next.version) throw new import_sdk_core3.CredentialError("REQUEST_SUBSTITUTED", "Server selected an undeclared credential step");
79
142
  if (completed.has(next.id) || state.transitionId === lastTransition) {
80
143
  await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal));
81
144
  continue;
@@ -83,21 +146,22 @@ async function interpretCredentialFlow(input) {
83
146
  lastTransition = state.transitionId;
84
147
  if (next.type === "holder_signature") {
85
148
  input.onProgress?.({ state: "signing", stepId: next.id, correlationId: state.correlationId });
86
- const binding = (0, import_sdk_core.validateCredentialHolderBinding)(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));
87
- 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 import_sdk_core.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
88
- const bytes = (0, import_sdk_core.holderBindingSigningBytes)(binding);
149
+ const binding = await checkBinding(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));
150
+ const alg = input.descriptor.holderBinding.alg;
151
+ if (alg === "EIP191" !== (input.signing.holderKey?.alg === "EIP191")) throw new import_sdk_core3.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
152
+ const bytes = (0, import_sdk_core3.holderBindingSigningBytes)(binding);
89
153
  let signature = await input.signing.autoSign?.(binding, bytes, input.signal);
90
154
  if (signature === void 0) {
91
- if (!input.signing.requestApproval) throw new import_sdk_core.CredentialError("SIGNATURE_REJECTED", "OpenKey approval is required");
155
+ if (!input.signing.requestApproval) throw new import_sdk_core3.CredentialError("SIGNATURE_REJECTED", "OpenKey approval is required");
92
156
  try {
93
157
  signature = await input.signing.requestApproval(binding, bytes, input.signal);
94
158
  } catch (cause) {
95
- if (cause instanceof import_sdk_core.CredentialError) throw cause;
96
- throw new import_sdk_core.CredentialError("SIGNATURE_REJECTED", "OpenKey approval was rejected", { cause });
159
+ if (cause instanceof import_sdk_core3.CredentialError) throw cause;
160
+ throw new import_sdk_core3.CredentialError("SIGNATURE_REJECTED", "OpenKey approval was rejected", { cause });
97
161
  }
98
162
  }
99
- if (!(signature instanceof Uint8Array) || signature.length === 0) throw new import_sdk_core.CredentialError("SIGNATURE_REJECTED", "OpenKey signature is invalid");
100
- await input.transport.submitHolderSignature(input.requestId, input.verifier, (0, import_sdk_core.encodeBase64Url)(signature), input.signal, input.signing.holderKey);
163
+ if (!(signature instanceof Uint8Array) || signature.length === 0) throw new import_sdk_core3.CredentialError("SIGNATURE_REJECTED", "OpenKey signature is invalid");
164
+ await input.transport.submitHolderSignature(input.requestId, input.verifier, alg === "EIP191" ? `0x${Array.from(signature, (byte) => byte.toString(16).padStart(2, "0")).join("")}` : (0, import_sdk_core3.encodeBase64Url)(signature), input.signal, input.signing.holderKey);
101
165
  } else {
102
166
  input.onProgress?.({ state: next.type === "collect_input" ? "collecting" : "proving", stepId: next.id, correlationId: state.correlationId });
103
167
  const handler = input.handlers?.[next.type];
@@ -131,9 +195,9 @@ async function interpretCredentialFlow(input) {
131
195
  await input.transport.submitStep(input.requestId, input.verifier, next.id, proof, input.signal);
132
196
  break;
133
197
  } catch (cause) {
134
- if (!(cause instanceof import_sdk_core.CredentialError) || cause.code !== "PROOF_REJECTED") throw cause;
198
+ if (!(cause instanceof import_sdk_core3.CredentialError) || cause.code !== "PROOF_REJECTED") throw cause;
135
199
  const attemptsRemaining = input.descriptor.lifecycle.maxProofAttempts - attempt;
136
- if (next.type !== "mailbox_otp" || attemptsRemaining <= 0) throw new import_sdk_core.CredentialError("VERIFICATION_FAILED", "The proof was not accepted", { state: attemptsRemaining <= 0 ? "proof_attempts_exhausted" : "proof_rejected", cause });
200
+ if (next.type !== "mailbox_otp" || attemptsRemaining <= 0) throw new import_sdk_core3.CredentialError("VERIFICATION_FAILED", "The proof was not accepted", { state: attemptsRemaining <= 0 ? "proof_attempts_exhausted" : "proof_rejected", cause });
137
201
  feedback = { kind: "rejected", attemptsRemaining };
138
202
  }
139
203
  }
@@ -144,27 +208,27 @@ async function interpretCredentialFlow(input) {
144
208
  }
145
209
 
146
210
  // src/credentials/transport.ts
147
- var import_sdk_core2 = require("@tinycloud/sdk-core");
211
+ var import_sdk_core4 = require("@tinycloud/sdk-core");
148
212
  function object(value, label) {
149
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", `${label} is invalid`);
213
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", `${label} is invalid`);
150
214
  return value;
151
215
  }
152
216
  function opaque(value, label) {
153
- if (typeof value !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(value)) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", `${label} is invalid`);
217
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(value)) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", `${label} is invalid`);
154
218
  return value;
155
219
  }
156
220
  function timestamp(value, label) {
157
- if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", `${label} is invalid`);
221
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", `${label} is invalid`);
158
222
  return new Date(value).toISOString();
159
223
  }
160
224
  function jwtPayload(credential) {
161
225
  const part = credential.split("~", 1)[0]?.split(".")[1];
162
- if (!part) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Issued SD-JWT is invalid");
226
+ if (!part) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Issued SD-JWT is invalid");
163
227
  try {
164
- return object(JSON.parse(new TextDecoder().decode((0, import_sdk_core2.decodeBase64Url)(part))), "SD-JWT payload");
228
+ return object(JSON.parse(new TextDecoder().decode((0, import_sdk_core4.decodeBase64Url)(part))), "SD-JWT payload");
165
229
  } catch (cause) {
166
- if (cause instanceof import_sdk_core2.CredentialError) throw cause;
167
- throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Issued SD-JWT is invalid", { cause });
230
+ if (cause instanceof import_sdk_core4.CredentialError) throw cause;
231
+ throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Issued SD-JWT is invalid", { cause });
168
232
  }
169
233
  }
170
234
  var SERVER_CODES = /* @__PURE__ */ new Set(["REQUEST_EXPIRED", "ISSUER_UNREADY", "UNSUPPORTED_PROFILE", "UNSUPPORTED_VERSION", "SIGNATURE_REJECTED"]);
@@ -173,12 +237,13 @@ function serverError(value, response, honorServerRecoverable) {
173
237
  const body = value;
174
238
  const keys = Object.keys(body).sort();
175
239
  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 void 0;
176
- if (body.code === "PROOF_REJECTED") return body.recoverable ? new import_sdk_core2.CredentialError("PROOF_REJECTED", "The proof was not accepted", { state: body.state, correlationId: body.correlationId }) : new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "The proof attempt limit was reached", { state: "proof_attempts_exhausted", correlationId: body.correlationId });
177
- if (body.code === "RATE_LIMITED") return new import_sdk_core2.CredentialError("ISSUER_UNREADY", "Too many verification codes were requested", { state: "rate_limited", correlationId: body.correlationId, retryAfterMs: Number(response.headers.get("retry-after")) * 1e3 || void 0, recoverable: honorServerRecoverable ? body.recoverable : void 0 });
178
- if (body.code === "CHALLENGE_EXPIRED") return new import_sdk_core2.CredentialError("REQUEST_EXPIRED", "The credential challenge expired", { state: body.state, correlationId: body.correlationId, recoverable: honorServerRecoverable ? body.recoverable : void 0 });
240
+ if (body.code === "PROOF_REJECTED") return body.recoverable ? new import_sdk_core4.CredentialError("PROOF_REJECTED", "The proof was not accepted", { state: body.state, correlationId: body.correlationId }) : new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "The proof attempt limit was reached", { state: "proof_attempts_exhausted", correlationId: body.correlationId });
241
+ if (body.code === "RATE_LIMITED") return new import_sdk_core4.CredentialError("ISSUER_UNREADY", "Too many verification codes were requested", { state: "rate_limited", correlationId: body.correlationId, retryAfterMs: Number(response.headers.get("retry-after")) * 1e3 || void 0, recoverable: honorServerRecoverable ? body.recoverable : void 0 });
242
+ if (body.code === "CHALLENGE_EXPIRED") return new import_sdk_core4.CredentialError("REQUEST_EXPIRED", "The credential challenge expired", { state: body.state, correlationId: body.correlationId, recoverable: honorServerRecoverable ? body.recoverable : void 0 });
243
+ if (body.code === "DESCRIPTOR_MISMATCH") body.code = "UNSUPPORTED_PROFILE";
179
244
  if (!SERVER_CODES.has(body.code)) return void 0;
180
245
  const code = body.code;
181
- return new import_sdk_core2.CredentialError(code, `OpenCredentials rejected the request: ${code}`, {
246
+ return new import_sdk_core4.CredentialError(code, `OpenCredentials rejected the request: ${code}`, {
182
247
  state: body.state,
183
248
  correlationId: body.correlationId,
184
249
  retryAfterMs: Number(response.headers.get("retry-after")) * 1e3 || void 0,
@@ -186,15 +251,20 @@ function serverError(value, response, honorServerRecoverable) {
186
251
  });
187
252
  }
188
253
  var OpenCredentialsHttpTransport = class {
254
+ /**
255
+ * `allowAccountSubject` is passed only by the email-proof functions, after the per-call held opt-in was accepted. Without it an account-bound
256
+ * (held) descriptor is refused here, before any request (TC-2254 A2.5).
257
+ */
189
258
  constructor(descriptor, fetchFn = globalThis.fetch.bind(globalThis), options = {}) {
190
259
  this.descriptor = descriptor;
191
260
  this.fetchFn = fetchFn;
192
261
  this.options = options;
193
262
  this.nonces = /* @__PURE__ */ new Map();
194
263
  this.bindings = /* @__PURE__ */ new Map();
264
+ if (descriptor.subjectRelationship === "account_is_subject" && options.allowAccountSubject !== true) throw new import_sdk_core4.CredentialError("UNSUPPORTED_PROFILE", "The held credential profile requires an explicit opt-in");
195
265
  }
196
266
  url(endpoint, id) {
197
- return new URL((0, import_sdk_core2.credentialEndpointPath)(endpoint, id), this.descriptor.issuer.origin).href;
267
+ return new URL((0, import_sdk_core4.credentialEndpointPath)(endpoint, id), this.descriptor.issuer.origin).href;
198
268
  }
199
269
  async request(endpoint, input) {
200
270
  let response;
@@ -202,8 +272,8 @@ var OpenCredentialsHttpTransport = class {
202
272
  const init = { method: input.method ?? "GET", credentials: "omit", cache: "no-store", redirect: "error", referrerPolicy: "no-referrer", signal: input.signal, headers: { accept: "application/json", ...input.body === void 0 ? {} : { "content-type": "application/json" }, ...input.verifier === void 0 ? {} : { authorization: `Bearer ${input.verifier}` } }, ...input.body === void 0 ? {} : { body: JSON.stringify(input.body) } };
203
273
  response = await this.fetchFn(this.url(endpoint, input.id), init);
204
274
  } catch (cause) {
205
- if (cause instanceof DOMException && cause.name === "AbortError") throw new import_sdk_core2.CredentialError("CANCELED", "Credential acquisition was canceled", { cause });
206
- throw new import_sdk_core2.CredentialError("OFFLINE", "OpenCredentials is unavailable", { cause });
275
+ if (cause instanceof DOMException && cause.name === "AbortError") throw new import_sdk_core4.CredentialError("CANCELED", "Credential acquisition was canceled", { cause });
276
+ throw new import_sdk_core4.CredentialError("OFFLINE", "OpenCredentials is unavailable", { cause });
207
277
  }
208
278
  if (!response.ok) {
209
279
  let parsed;
@@ -214,95 +284,112 @@ var OpenCredentialsHttpTransport = class {
214
284
  }
215
285
  const typed = serverError(parsed, response, this.options.honorServerRecoverable === true);
216
286
  if (typed) throw typed;
217
- if (response.status === 410) throw new import_sdk_core2.CredentialError("REQUEST_EXPIRED", "Credential request expired");
218
- if (response.status === 409) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential request was replayed or substituted");
219
- if (response.status === 503) throw new import_sdk_core2.CredentialError("ISSUER_UNREADY", "Credential issuer is not ready", { retryAfterMs: Number(response.headers.get("retry-after")) * 1e3 || void 0 });
220
- throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "OpenCredentials rejected the request");
287
+ if (response.status === 410) throw new import_sdk_core4.CredentialError("REQUEST_EXPIRED", "Credential request expired");
288
+ if (response.status === 409) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential request was replayed or substituted");
289
+ if (response.status === 503) throw new import_sdk_core4.CredentialError("ISSUER_UNREADY", "Credential issuer is not ready", { retryAfterMs: Number(response.headers.get("retry-after")) * 1e3 || void 0 });
290
+ throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "OpenCredentials rejected the request");
221
291
  }
222
292
  if (response.status === 204) return void 0;
223
293
  try {
224
294
  return await response.json();
225
295
  } catch (cause) {
226
- throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "OpenCredentials response is invalid", { cause });
296
+ throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "OpenCredentials response is invalid", { cause });
227
297
  }
228
298
  }
229
299
  async create(input) {
230
- 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");
231
- if (body.type !== "tinycloud.credentials/acquisition-request/v1" || body.protocol !== input.descriptor.protocol || body.next !== "challenge" || body.endpoint !== "challenge") throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "create response is invalid");
300
+ 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");
301
+ if (body.type !== "tinycloud.credentials/acquisition-request/v1" || body.protocol !== input.descriptor.protocol || body.next !== "challenge" || body.endpoint !== "challenge") throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "create response is invalid");
232
302
  const requestId = opaque(body.requestId, "requestId");
233
303
  return { requestId, locator: requestId, expiresAt: timestamp(body.expiresAt, "expiresAt"), correlationId: requestId };
234
304
  }
305
+ /** The nonce stored from R3 or R2 for this request. Held acquisition compares the signed binding with it. */
306
+ challengeNonce(id) {
307
+ return this.nonces.get(id);
308
+ }
309
+ /** The binding fetched and validated for this request, as the account signed it. */
310
+ signedBinding(id) {
311
+ return this.bindings.get(id);
312
+ }
235
313
  proofStep() {
236
314
  return this.descriptor.steps.some((step) => step.type === "mailbox_otp") ? "mailbox_otp" : "collect_input";
237
315
  }
238
316
  async state(id, verifier, signal) {
239
317
  const raw = object(await this.request("state", { id, verifier, signal }), "state response");
240
- if (raw.type !== "tinycloud.credentials/acquisition-state/v1" || raw.requestId !== id || raw.profile !== this.descriptor.profile || raw.profileVersion !== 1) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential state response is substituted");
318
+ if (raw.type !== "tinycloud.credentials/acquisition-state/v1" || raw.requestId !== id || raw.profile !== this.descriptor.profile || raw.profileVersion !== 1) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential state response is substituted");
241
319
  const base = { type: "OpenCredentialsAcquisitionState", version: 1, requestId: id, transitionId: `${String(raw.state)}:${String(raw.resultPreviouslyRead ?? false)}`, correlationId: id };
242
320
  if (raw.state === "challenge_required") {
243
321
  const type = this.proofStep();
244
322
  return { ...base, state: "pending", nextStep: { id: type, type, version: 1, constraints: { challengeRequired: true } } };
245
323
  }
324
+ 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")) {
325
+ const nonce = raw.challengeNonce;
326
+ const prior = this.nonces.get(id);
327
+ if (typeof nonce !== "string" || nonce === "" || prior !== void 0 && prior !== nonce) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge nonce is missing or changed");
328
+ this.nonces.set(id, opaque(nonce, "challengeNonce"));
329
+ }
246
330
  if (raw.state === "proof_required") {
247
331
  const type = this.proofStep();
248
- this.nonces.set(id, opaque(raw.challengeNonce, "challengeNonce"));
332
+ if (this.descriptor.subjectRelationship !== "account_is_subject") this.nonces.set(id, opaque(raw.challengeNonce, "challengeNonce"));
249
333
  return { ...base, state: "pending", nextStep: { id: type, type, version: 1, constraints: {} } };
250
334
  }
251
335
  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: {} } };
252
336
  if (raw.state === "ready_to_issue") return { ...base, state: "ready_to_issue" };
253
337
  if (raw.state === "issued") return { ...base, state: "complete" };
254
- throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential state is unsupported");
338
+ throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential state is unsupported");
255
339
  }
256
340
  async beginStep(id, verifier, stepId, signal) {
257
341
  const step = this.proofStep();
258
- if (stepId !== step) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge step is substituted");
342
+ if (stepId !== step) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge step is substituted");
259
343
  const challenge = object(await this.request("challenge", { id, verifier, method: "POST", body: { step, stepVersion: 1 }, signal }), "challenge response");
260
- if (challenge.type !== "tinycloud.credentials/challenge/v1" || challenge.step !== step || challenge.stepVersion !== 1) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge is substituted");
344
+ if (challenge.type !== "tinycloud.credentials/challenge/v1" || challenge.step !== step || challenge.stepVersion !== 1) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge is substituted");
261
345
  this.nonces.set(id, opaque(challenge.challengeNonce, "challengeNonce"));
262
346
  }
263
347
  async submitStep(id, verifier, stepId, proof, signal) {
264
348
  const step = this.proofStep();
265
- if (stepId !== step) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential proof step is substituted");
349
+ if (stepId !== step) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential proof step is substituted");
266
350
  const nonce = this.nonces.get(id);
267
- if (!nonce) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge is missing");
351
+ if (!nonce) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge is missing");
268
352
  await this.request("proof", { id, verifier, method: "POST", body: { step, stepVersion: 1, challengeNonce: nonce, proof }, signal });
269
353
  }
270
354
  async holderBinding(id, verifier, signal) {
271
355
  const raw = object(await this.request("holder_binding", { id, verifier, signal }), "holder binding response");
272
- 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 import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Holder binding response is invalid");
273
- const binding = (0, import_sdk_core2.validateCredentialHolderBinding)(raw.binding);
274
- if ((0, import_sdk_core2.encodeBase64Url)((0, import_sdk_core2.holderBindingCanonicalBytes)(binding)) !== raw.canonical) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Holder binding canonical bytes were substituted");
356
+ 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 import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Holder binding response is invalid");
357
+ const binding = (0, import_sdk_core4.validateCredentialHolderBinding)(raw.binding);
358
+ if (binding.type !== raw.signingDomain) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Holder binding type does not match its signing domain");
359
+ if ((0, import_sdk_core4.encodeBase64Url)((0, import_sdk_core4.holderBindingCanonicalBytes)(binding)) !== raw.canonical) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Holder binding canonical bytes were substituted");
275
360
  this.bindings.set(id, binding);
276
361
  return binding;
277
362
  }
278
363
  async submitHolderSignature(id, verifier, signature, signal, key) {
279
364
  const binding = this.bindings.get(id);
280
- if (!binding) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was not retrieved");
365
+ if (!binding) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was not retrieved");
366
+ if (this.descriptor.holderBinding.alg === "EIP191" && !key) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "An account holder must name its key and algorithm");
367
+ const alg = key?.alg ?? "EdDSA";
281
368
  const kid = key?.kid ?? `${binding.holderDid}#${binding.holderDid.replace("did:key:", "")}`;
282
- await this.request("holder_signature", { id, verifier, method: "POST", body: { alg: key?.alg ?? "EdDSA", kid, signature }, signal });
369
+ await this.request("holder_signature", { id, verifier, method: "POST", body: { alg, kid, signature }, signal });
283
370
  }
284
371
  async issue(id, verifier, signal) {
285
372
  await this.request("issue", { id, verifier, method: "POST", signal });
286
373
  }
287
374
  async result(id, verifier, signal) {
288
375
  const raw = object(await this.request("result", { id, verifier, signal }), "result response");
289
- 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 (0, import_sdk_core2.canonicalDigest)(this.descriptor) || raw.issuer !== this.descriptor.issuer.did || raw.issuerKid !== this.descriptor.issuer.kid || typeof raw.credential !== "string" || await (0, import_sdk_core2.sha256Base64Url)(raw.credential) !== raw.credentialDigest) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Issued result metadata is invalid");
376
+ 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 (0, import_sdk_core4.canonicalDigest)(this.descriptor) || raw.issuer !== this.descriptor.issuer.did || raw.issuerKid !== this.descriptor.issuer.kid || typeof raw.credential !== "string" || await (0, import_sdk_core4.sha256Base64Url)(raw.credential) !== raw.credentialDigest) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Issued result metadata is invalid");
290
377
  const holder = object(raw.holderBinding, "result holder binding");
291
- if (raw.subject !== holder.did || holder.signingDomain !== "tinycloud.credentials/holder-binding/v1") throw new import_sdk_core2.CredentialError("HOLDER_MISMATCH", "Issued holder binding is invalid");
378
+ if (raw.subject !== holder.did || holder.signingDomain !== this.descriptor.holderBinding.domain) throw new import_sdk_core4.CredentialError("HOLDER_MISMATCH", "Issued holder binding is invalid");
292
379
  const claimsRaw = object(raw.claims, "result claims");
293
380
  const claims = {};
294
381
  for (const [name, value] of Object.entries(claimsRaw)) {
295
- if (typeof value !== "string") throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Issued claims are invalid");
382
+ if (typeof value !== "string") throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Issued claims are invalid");
296
383
  claims[name] = value;
297
384
  }
298
385
  const validity = object(raw.validity, "result validity");
299
386
  const payload = jwtPayload(raw.credential);
300
- if (typeof payload.jti !== "string") throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Credential id is invalid");
387
+ if (typeof payload.jti !== "string") throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Credential id is invalid");
301
388
  const issuedAt = timestamp(validity.issuedAt, "issuedAt");
302
389
  const expiresAt = timestamp(validity.expiresAt, "expiresAt");
303
390
  const status = object(raw.status, "result status");
304
- if (status.type !== "none" || status.freshnessSeconds !== this.descriptor.status.freshnessSeconds) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Credential status is invalid");
305
- 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, issuerKid: raw.issuerKid, subjectDid: raw.subject, holderDid: raw.subject, claims, claimsDigest: await (0, import_sdk_core2.canonicalDigest)(claims), descriptorDigest: raw.descriptorDigest, credentialId: payload.jti, issuedAt, notBefore: issuedAt, expiresAt, status: { method: "none", freshnessSeconds: status.freshnessSeconds }, credential: raw.credential };
391
+ if (status.type !== "none" || status.freshnessSeconds !== this.descriptor.status.freshnessSeconds) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Credential status is invalid");
392
+ 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, issuerKid: raw.issuerKid, subjectDid: raw.subject, holderDid: raw.subject, claims, claimsDigest: await (0, import_sdk_core4.canonicalDigest)(claims), descriptorDigest: raw.descriptorDigest, credentialId: payload.jti, issuedAt, notBefore: issuedAt, expiresAt, status: { method: "none", freshnessSeconds: status.freshnessSeconds }, credential: raw.credential };
306
393
  }
307
394
  async issuerMetadata(signal) {
308
395
  return await this.request("issuer_metadata", { signal });
@@ -313,14 +400,8 @@ var OpenCredentialsHttpTransport = class {
313
400
  };
314
401
 
315
402
  // src/credentials/email-proof.ts
316
- var import_sdk_core3 = require("@tinycloud/sdk-core");
317
- var EMAIL_PROOF_PINS = Object.freeze({
318
- witnessOrigin: "https://witness.credentials.org",
319
- issuerDid: "did:web:issuer.credentials.org",
320
- issuerKid: "did:web:issuer.credentials.org#controller",
321
- issuerX: "eEJI4xEobto4HtQ7Pg9R1vBOwcpfGRTlXDG5QfqLnGQ",
322
- descriptorDigest: "1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0"
323
- });
403
+ var import_sdk_core5 = require("@tinycloud/sdk-core");
404
+ var import_held_credentials = require("@tinycloud/sdk-core/held-credentials");
324
405
  var EMAIL_PROOF_CREDENTIAL_TYPE = { id: "opencredentials.email/v1", version: 1 };
325
406
  var EMAIL_PROOF_PENDING_MAX_MS = 15 * 6e4;
326
407
  var DISCOVERY_URL = `${EMAIL_PROOF_PINS.witnessOrigin}/.well-known/opencredentials`;
@@ -336,10 +417,16 @@ var EMAIL_PROOF_ISSUER_KEYS = [
336
417
  var DEFAULT_EMAIL_PROOF_PROFILE = "tinycloud.email-proof/v1";
337
418
  var PROFILE_PINS = {
338
419
  "tinycloud.email-proof/v1": { descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest, requiredStep: "mailbox_otp", enabled: true },
339
- // Held profile: disabled until the held wire path ships. Requests refuse with PROFILE_UNAVAILABLE before any network call.
340
- "tinycloud.email-held/v1": { descriptorDigest: "dc3_DuvzipXwYkTfjKqTaK1c3VS0YSPnaBsYSQriC0w", requiredStep: "mailbox_otp", enabled: false }
420
+ // Held profile: disabled by default. A call enables it only with the per-call opt-in `allowHeldProfile: true` (see `profilePin`).
421
+ // Without it, requests refuse with PROFILE_UNAVAILABLE before any network call.
422
+ "tinycloud.email-held/v1": { descriptorDigest: HELD_ACCOUNT_BINDING_PINS.descriptorDigest, requiredStep: "mailbox_otp", enabled: false }
341
423
  };
342
- var OPENER_ORIGIN = "https://localhost";
424
+ var OPENER_ORIGIN = HELD_ACCOUNT_BINDING_PINS.openerOrigin;
425
+ var HELD_PROFILE = "tinycloud.email-held/v1";
426
+ var HELD_KID_FRAGMENT = "blockchainAccountId";
427
+ var HELD_MAX_AGE_SECONDS = 31536e3;
428
+ var HELD_REQUEST_TTL_MS = 6e5;
429
+ var CLOCK_SKEW_BOUND_MS = 3e5;
343
430
  var STEP_TIMEOUT_MS = 6e4;
344
431
  var HOLDER_DID_PREFIX = "did:key:";
345
432
  var HOLDER_DID_PATTERN = /^did:key:z6Mk[^#]+$/;
@@ -359,6 +446,7 @@ var RECOVERABILITY = {
359
446
  CREDENTIAL_INVALID: false,
360
447
  SESSION_EXPIRED: false,
361
448
  SESSION_INVALID: false,
449
+ CLIENT_CLOCK_SKEW: false,
362
450
  CANCELED: false,
363
451
  FAILED: false
364
452
  };
@@ -375,9 +463,11 @@ var MESSAGES = {
375
463
  CREDENTIAL_INVALID: "Issued credential failed verification.",
376
464
  SESSION_EXPIRED: "Email verification request expired. Request a new code.",
377
465
  SESSION_INVALID: "Email verification request is no longer valid. Request a new code.",
466
+ CLIENT_CLOCK_SKEW: "Your device clock is more than 5 minutes behind. Correct it and try again.",
378
467
  CANCELED: "Email verification was canceled.",
379
468
  FAILED: "Email verification failed."
380
469
  };
470
+ var LOCAL_PROFILE_UNAVAILABLE = "This SDK build doesn't enable the email verification profile.";
381
471
  function rateLimitMessage(retryAfterMs) {
382
472
  const head = "Too many codes were requested for this address.";
383
473
  if (retryAfterMs === void 0 || !Number.isFinite(retryAfterMs) || retryAfterMs <= 0) return `${head} Try again in about an hour.`;
@@ -385,8 +475,9 @@ function rateLimitMessage(retryAfterMs) {
385
475
  return `${head} Try again in ${minutes} ${minutes === 1 ? "minute" : "minutes"}.`;
386
476
  }
387
477
  var EmailProofError = class extends Error {
388
- constructor(step, code, retryAfterMs, recoverable = RECOVERABILITY[code]) {
389
- super(code === "RATE_LIMITED" ? rateLimitMessage(retryAfterMs) : MESSAGES[code]);
478
+ /** `message` overrides the code's text for one refusal whose cause the shared text would misdescribe. It never carries request data. */
479
+ constructor(step, code, retryAfterMs, recoverable = RECOVERABILITY[code], message) {
480
+ super(message ?? (code === "RATE_LIMITED" ? rateLimitMessage(retryAfterMs) : MESSAGES[code]));
390
481
  this.name = "EmailProofError";
391
482
  this.step = step;
392
483
  this.code = code;
@@ -413,7 +504,8 @@ function restoreEmailProofPending(fields) {
413
504
  requirementDigest: fields.requirementDigest,
414
505
  openerOrigin: fields.openerOrigin,
415
506
  createdAt: fields.createdAt,
416
- expiresAt: fields.expiresAt
507
+ expiresAt: fields.expiresAt,
508
+ ...fields.serverExpiresAt === void 0 ? {} : { serverExpiresAt: fields.serverExpiresAt }
417
509
  };
418
510
  const pending = { ...shown };
419
511
  hide(pending, "verifier", fields.verifier);
@@ -453,6 +545,9 @@ function createMemoryEmailProofPendingStore() {
453
545
  }
454
546
  };
455
547
  }
548
+ function isTicketEmailProofResult(result) {
549
+ return result.envelope.profile.id !== HELD_PROFILE;
550
+ }
456
551
  var CodeRequested = class extends Error {
457
552
  };
458
553
  var pauseForCode = async () => {
@@ -460,17 +555,19 @@ var pauseForCode = async () => {
460
555
  };
461
556
  async function requestEmailProofCode(input) {
462
557
  const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;
463
- profilePin(profile, "request");
464
- const address = normalizeAddress(input.address);
465
- assertHolder(input.holder, "request");
558
+ profilePin(profile, "request", input.allowHeldProfile);
559
+ const held = profile === HELD_PROFILE;
560
+ if (held) assertHolder(input.holder, "request", profile);
561
+ const address = normalizeAddress(input.address, profile);
562
+ if (!held) assertHolder(input.holder, "request", profile);
466
563
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
467
564
  const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);
468
565
  const requirement = emailRequirement(address, profile);
469
- const requirementDigest = await (0, import_sdk_core3.credentialRequirementDigest)(requirement);
470
- const descriptor = await witnessDescriptor(profile, requirement, { fetch: input.fetch, signal }, "request");
471
- const descriptorDigest = await (0, import_sdk_core3.canonicalDigest)(descriptor);
566
+ const requirementDigest = await (0, import_sdk_core5.credentialRequirementDigest)(requirement);
567
+ const descriptor = await witnessDescriptor(profile, requirement, { fetch: input.fetch, signal }, "request", input.allowHeldProfile);
568
+ const descriptorDigest = await (0, import_sdk_core5.canonicalDigest)(descriptor);
472
569
  const verifier = randomVerifier();
473
- const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });
570
+ const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true, ...held ? { allowAccountSubject: true } : {} });
474
571
  const createdAtMs = now().getTime();
475
572
  let created;
476
573
  try {
@@ -481,7 +578,7 @@ async function requestEmailProofCode(input) {
481
578
  requirementDigest,
482
579
  holderDid: input.holder.did,
483
580
  openerOrigin: OPENER_ORIGIN,
484
- completionVerifierChallenge: await (0, import_sdk_core3.sha256Base64Url)(verifier),
581
+ completionVerifierChallenge: await (0, import_sdk_core5.sha256Base64Url)(verifier),
485
582
  signal
486
583
  });
487
584
  } catch (error) {
@@ -489,6 +586,8 @@ async function requestEmailProofCode(input) {
489
586
  }
490
587
  const serverExpiry = Date.parse(created.expiresAt);
491
588
  if (!Number.isFinite(serverExpiry)) throw new EmailProofError("request", "FAILED");
589
+ const serverNowMs = serverExpiry - HELD_REQUEST_TTL_MS;
590
+ if (held && serverNowMs - now().getTime() > CLOCK_SKEW_BOUND_MS) throw new EmailProofError("request", "CLIENT_CLOCK_SKEW");
492
591
  const pending = restoreEmailProofPending({
493
592
  type: "TinyCloudEmailProofPending",
494
593
  version: 1,
@@ -501,11 +600,14 @@ async function requestEmailProofCode(input) {
501
600
  requirementDigest,
502
601
  openerOrigin: OPENER_ORIGIN,
503
602
  createdAt: new Date(createdAtMs).toISOString(),
504
- expiresAt: new Date(Math.min(serverExpiry, createdAtMs + EMAIL_PROOF_PENDING_MAX_MS)).toISOString()
603
+ // Held: the deadline is measured on the client clock (a fast client must not lose window), the witness enforces its own 600 s.
604
+ expiresAt: new Date(held ? createdAtMs + Math.min(serverExpiry - serverNowMs, EMAIL_PROOF_PENDING_MAX_MS) : Math.min(serverExpiry, createdAtMs + EMAIL_PROOF_PENDING_MAX_MS)).toISOString(),
605
+ ...held ? { serverExpiresAt: created.expiresAt } : {}
505
606
  });
506
607
  try {
507
608
  await interpretCredentialFlow({
508
609
  descriptor,
610
+ ...held ? { allowAccountSubject: true } : {},
509
611
  requirement,
510
612
  requestId: created.requestId,
511
613
  verifier,
@@ -513,8 +615,9 @@ async function requestEmailProofCode(input) {
513
615
  descriptorDigest,
514
616
  requirementDigest,
515
617
  openerOrigin: OPENER_ORIGIN,
618
+ ...held ? { expiresAt: created.expiresAt } : {},
516
619
  transport,
517
- signing: holderSigning(input.holder),
620
+ signing: holderSigning(input.holder, held),
518
621
  handlers: { mailbox_otp: pauseForCode },
519
622
  signal
520
623
  });
@@ -531,22 +634,26 @@ async function completeWithIssuerKeys(input, issuerKeys) {
531
634
  const { pending } = input;
532
635
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
533
636
  if (!isPending(pending)) throw new EmailProofError("session", "SESSION_INVALID");
637
+ profilePin(pending.profile, "session", input.allowHeldProfile);
638
+ const held = pending.profile === HELD_PROFILE;
639
+ if (held && !Number.isFinite(Date.parse(pending.serverExpiresAt ?? ""))) throw new EmailProofError("session", "SESSION_INVALID");
534
640
  if (pendingDeadlineMs(pending) <= now().getTime()) throw new EmailProofError("session", "SESSION_EXPIRED");
535
641
  if (!/^\d{8}$/.test(input.code)) throw new EmailProofError("code", "CODE_INVALID");
536
- assertHolder(input.holder, "code");
642
+ assertHolder(input.holder, "code", pending.profile);
537
643
  if (input.holder.did !== pending.holderDid) throw new EmailProofError("session", "SESSION_INVALID");
538
644
  const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);
539
645
  const requirement = emailRequirement(pending.address, pending.profile);
540
- if (await (0, import_sdk_core3.credentialRequirementDigest)(requirement) !== pending.requirementDigest) throw new EmailProofError("session", "SESSION_INVALID");
541
- const descriptor = await witnessDescriptor(pending.profile, requirement, { fetch: input.fetch, signal }, "session");
542
- const descriptorDigest = await (0, import_sdk_core3.canonicalDigest)(descriptor);
646
+ if (await (0, import_sdk_core5.credentialRequirementDigest)(requirement) !== pending.requirementDigest) throw new EmailProofError("session", "SESSION_INVALID");
647
+ const descriptor = await witnessDescriptor(pending.profile, requirement, { fetch: input.fetch, signal }, "session", input.allowHeldProfile);
648
+ const descriptorDigest = await (0, import_sdk_core5.canonicalDigest)(descriptor);
543
649
  if (descriptorDigest !== pending.descriptorDigest) throw new EmailProofError("session", "SESSION_INVALID");
544
- const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });
650
+ const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true, ...held ? { allowAccountSubject: true } : {} });
545
651
  try {
546
652
  const state = await transport.state(pending.requestId, pending.verifier, signal);
547
653
  if (state.nextStep?.constraints.challengeRequired === true) throw new EmailProofError("session", "SESSION_INVALID");
548
654
  await interpretCredentialFlow({
549
655
  descriptor,
656
+ ...held ? { allowAccountSubject: true } : {},
550
657
  requirement,
551
658
  requestId: pending.requestId,
552
659
  verifier: pending.verifier,
@@ -554,8 +661,9 @@ async function completeWithIssuerKeys(input, issuerKeys) {
554
661
  descriptorDigest,
555
662
  requirementDigest: pending.requirementDigest,
556
663
  openerOrigin: pending.openerOrigin,
664
+ ...held ? { expiresAt: pending.serverExpiresAt } : {},
557
665
  transport,
558
- signing: holderSigning(input.holder),
666
+ signing: holderSigning(input.holder, held),
559
667
  handlers: { mailbox_otp: async () => ({ otp: input.code }) },
560
668
  signal
561
669
  });
@@ -564,7 +672,11 @@ async function completeWithIssuerKeys(input, issuerKeys) {
564
672
  }
565
673
  try {
566
674
  const envelope = await transport.result(pending.requestId, pending.verifier, signal);
567
- const verified = await (0, import_sdk_core3.verifyIssuedCredential)({
675
+ if (held) {
676
+ const verified2 = await verifyHeldResult({ envelope, pending, descriptor, descriptorDigest, transport, issuerKeys, now });
677
+ return { envelope, verified: verified2, credentialId: verified2.credentialId, issuedAt: verified2.issuedAt.toISOString(), expiresAt: verified2.expiresAt.toISOString(), descriptor };
678
+ }
679
+ const verified = await (0, import_sdk_core5.verifyIssuedCredential)({
568
680
  envelope,
569
681
  descriptor,
570
682
  descriptorDigest,
@@ -584,12 +696,17 @@ async function acquireEmailProof(input) {
584
696
  return acquireWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);
585
697
  }
586
698
  async function acquireWithIssuerKeys(input, issuerKeys) {
587
- const address = normalizeAddress(input.address);
699
+ const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;
700
+ profilePin(profile, "request", input.allowHeldProfile);
701
+ const address = normalizeAddress(input.address, profile);
588
702
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
589
703
  const store = input.pendingStore;
590
- const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;
591
704
  const live = store ? await store.findLive(address, now()) : void 0;
592
705
  let pending = live?.profile === profile ? live : void 0;
706
+ if (pending && profile === HELD_PROFILE && !Number.isFinite(Date.parse(pending.serverExpiresAt ?? ""))) {
707
+ await store?.delete(pending.requestId).catch(() => void 0);
708
+ throw new EmailProofError("session", "SESSION_INVALID");
709
+ }
593
710
  if (!pending) {
594
711
  pending = await requestEmailProofCode({ ...input, address });
595
712
  await store?.put(pending);
@@ -613,30 +730,54 @@ async function acquireWithIssuerKeys(input, issuerKeys) {
613
730
  throw error;
614
731
  }
615
732
  }
616
- function normalizeAddress(address) {
733
+ function normalizeAddress(address, profile) {
734
+ if (profile === HELD_PROFILE) return normalizeHeldAddress(address);
617
735
  const value = address.trim();
618
736
  if (value.length > 254 || !EMAIL_PATTERN.test(value)) throw new EmailProofError("request", "ADDRESS_INVALID");
619
737
  return value;
620
738
  }
739
+ var HELD_LOCAL_ATOM = /^[a-z0-9!#$%&'*+\-/=?^_`{|}~]+(\.[a-z0-9!#$%&'*+\-/=?^_`{|}~]+)*$/;
740
+ var HELD_DOMAIN_LABEL = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
741
+ function normalizeHeldAddress(address) {
742
+ const invalid = () => new EmailProofError("request", "ADDRESS_INVALID");
743
+ const trimmed = address.trim();
744
+ if (/[^\x00-\x7f]/.test(trimmed)) throw invalid();
745
+ const value = trimmed.toLowerCase();
746
+ const parts = value.split("@");
747
+ if (value.length < 3 || value.length > 254 || parts.length !== 2 || /[^\x21-\x7e]/.test(value)) throw invalid();
748
+ const [local, domain] = parts;
749
+ if (local.length > 64 || !HELD_LOCAL_ATOM.test(local) || local.startsWith("+")) throw invalid();
750
+ if (domain.length > 253 || !domain.includes(".") || !domain.split(".").every((label) => label.length <= 63 && HELD_DOMAIN_LABEL.test(label))) throw invalid();
751
+ return value;
752
+ }
621
753
  function holderKid(did) {
622
754
  return `${did}#${did.slice(HOLDER_DID_PREFIX.length)}`;
623
755
  }
624
- function assertHolder(holder, step) {
756
+ function assertHeldHolder(holder, step) {
757
+ const parsed = (0, import_held_credentials.parsePkhDid)(holder.did);
758
+ const canonical = parsed !== null && parsed.chainId === "1" ? `did:pkh:eip155:1:${(0, import_held_credentials.toChecksumAddress)(parsed.address)}` : void 0;
759
+ if (canonical === void 0 || holder.did !== canonical || holder.alg !== "EIP191" || holder.kid !== `${canonical}#${HELD_KID_FRAGMENT}`) throw new EmailProofError(step, "HOLDER_INVALID");
760
+ }
761
+ function assertHolder(holder, step, profile) {
762
+ if (profile === HELD_PROFILE) return assertHeldHolder(holder, step);
625
763
  if (!HOLDER_DID_PATTERN.test(holder.did) || holder.alg !== "EdDSA" || holder.kid !== holderKid(holder.did)) throw new EmailProofError(step, "HOLDER_INVALID");
626
764
  }
627
- function profilePin(profile, step) {
765
+ function profilePin(profile, step, allowHeld) {
628
766
  const entry = Object.prototype.hasOwnProperty.call(PROFILE_PINS, profile) ? PROFILE_PINS[profile] : void 0;
629
- if (!entry || !entry.enabled) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
767
+ if (!entry || !entry.enabled && !(allowHeld === true && profile === HELD_PROFILE)) {
768
+ throw new EmailProofError(step, "PROFILE_UNAVAILABLE", void 0, void 0, LOCAL_PROFILE_UNAVAILABLE);
769
+ }
630
770
  return entry;
631
771
  }
632
772
  function emailRequirement(address, profile) {
633
- return (0, import_sdk_core3.createEmailCredentialRequirement)({ email: address, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE });
773
+ if (profile === HELD_PROFILE) return (0, import_sdk_core5.validateCredentialRequirement)({ type: "TinyCloudCredentialRequirement", version: 1, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE, claims: { email: address }, maxAgeSeconds: HELD_MAX_AGE_SECONDS });
774
+ return (0, import_sdk_core5.createEmailCredentialRequirement)({ email: address, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE });
634
775
  }
635
776
  function emailProofRequirement(address) {
636
- return emailRequirement(normalizeAddress(address), DEFAULT_EMAIL_PROOF_PROFILE);
777
+ return emailRequirement(normalizeAddress(address, DEFAULT_EMAIL_PROOF_PROFILE), DEFAULT_EMAIL_PROOF_PROFILE);
637
778
  }
638
- async function witnessDescriptor(profile, requirement, options, step) {
639
- const pin = profilePin(profile, step);
779
+ async function witnessDescriptor(profile, requirement, options, step, allowHeld) {
780
+ const pin = profilePin(profile, step, allowHeld);
640
781
  let descriptor;
641
782
  try {
642
783
  descriptor = await discoverCatalogDescriptor(requirement, DISCOVERY_URL, { fetch: options.fetch, signal: options.signal });
@@ -646,7 +787,7 @@ async function witnessDescriptor(profile, requirement, options, step) {
646
787
  if (descriptor.issuer.origin !== EMAIL_PROOF_PINS.witnessOrigin || descriptor.issuer.did !== EMAIL_PROOF_PINS.issuerDid || !descriptor.steps.some((candidate) => candidate.type === pin.requiredStep)) {
647
788
  throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
648
789
  }
649
- if (await (0, import_sdk_core3.canonicalDigest)(descriptor) !== pin.descriptorDigest) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
790
+ if (await (0, import_sdk_core5.canonicalDigest)(descriptor) !== pin.descriptorDigest) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
650
791
  return descriptor;
651
792
  }
652
793
  async function discoverCatalogDescriptor(requirement, discoveryUrl, options = {}) {
@@ -654,30 +795,30 @@ async function discoverCatalogDescriptor(requirement, discoveryUrl, options = {}
654
795
  try {
655
796
  url = new URL(discoveryUrl);
656
797
  } catch {
657
- throw new import_sdk_core3.CredentialError("UNSUPPORTED_PROFILE", "Discovery URL is invalid");
798
+ throw new import_sdk_core5.CredentialError("UNSUPPORTED_PROFILE", "Discovery URL is invalid");
658
799
  }
659
- if (url.protocol !== "https:" || !["/.well-known/opencredentials", "/v1/credential-types"].includes(url.pathname) || url.search || url.hash) throw new import_sdk_core3.CredentialError("UNSUPPORTED_PROFILE", "Discovery URL is not an allowed catalog endpoint");
800
+ if (url.protocol !== "https:" || !["/.well-known/opencredentials", "/v1/credential-types"].includes(url.pathname) || url.search || url.hash) throw new import_sdk_core5.CredentialError("UNSUPPORTED_PROFILE", "Discovery URL is not an allowed catalog endpoint");
660
801
  const init = { credentials: "omit", cache: "no-store", redirect: "error", referrerPolicy: "no-referrer", signal: options.signal };
661
802
  let response;
662
803
  try {
663
804
  response = await (options.fetch ?? globalThis.fetch.bind(globalThis))(url, init);
664
805
  } catch (cause) {
665
- throw new import_sdk_core3.CredentialError("OFFLINE", "Credential discovery is unavailable", { cause });
806
+ throw new import_sdk_core5.CredentialError("OFFLINE", "Credential discovery is unavailable", { cause });
666
807
  }
667
- if (!response.ok) throw new import_sdk_core3.CredentialError(response.status === 503 ? "ISSUER_UNREADY" : "OFFLINE", "Credential discovery is unavailable");
808
+ if (!response.ok) throw new import_sdk_core5.CredentialError(response.status === 503 ? "ISSUER_UNREADY" : "OFFLINE", "Credential discovery is unavailable");
668
809
  const catalog = await response.json();
669
- if (catalog.type !== "tinycloud.credentials/catalog/v1" || catalog.protocol !== "tinycloud.credentials/acquisition/v1" || catalog.catalogVersion !== 1 || !Array.isArray(catalog.profiles)) throw new import_sdk_core3.CredentialError("DESCRIPTOR_INVALID", "Credential catalog is invalid");
810
+ if (catalog.type !== "tinycloud.credentials/catalog/v1" || catalog.protocol !== "tinycloud.credentials/acquisition/v1" || catalog.catalogVersion !== 1 || !Array.isArray(catalog.profiles)) throw new import_sdk_core5.CredentialError("DESCRIPTOR_INVALID", "Credential catalog is invalid");
670
811
  for (const entryValue of catalog.profiles) {
671
812
  if (typeof entryValue !== "object" || entryValue === null || Array.isArray(entryValue)) continue;
672
813
  const entry = entryValue;
673
814
  if (entry.supported !== true || entry.enabled !== true || entry.readiness !== "ready" && entry.readiness !== "degraded") continue;
674
815
  try {
675
- const candidate = (0, import_sdk_core3.validateCredentialFlowDescriptor)(entry.descriptor);
676
- if ((0, import_sdk_core3.descriptorSatisfiesRequirement)(candidate, requirement) && entry.descriptorDigest === await (0, import_sdk_core3.canonicalDigest)(candidate)) return candidate;
816
+ const candidate = (0, import_sdk_core5.validateCredentialFlowDescriptor)(entry.descriptor);
817
+ if ((0, import_sdk_core5.descriptorSatisfiesRequirement)(candidate, requirement) && entry.descriptorDigest === await (0, import_sdk_core5.canonicalDigest)(candidate)) return candidate;
677
818
  } catch {
678
819
  }
679
820
  }
680
- throw new import_sdk_core3.CredentialError("UNSUPPORTED_PROFILE", "No available credential profile satisfies the requirement");
821
+ throw new import_sdk_core5.CredentialError("UNSUPPORTED_PROFILE", "No available credential profile satisfies the requirement");
681
822
  }
682
823
  function discoveryFailure(error, signal, step) {
683
824
  if (signal.aborted && signal.reason?.name !== "TimeoutError") return new EmailProofError(step, "CANCELED");
@@ -692,8 +833,8 @@ async function verifyStoredEmailProof(input) {
692
833
  async function verifyStoredWithIssuerKeys(input, issuerKeys) {
693
834
  const { record, requirement, holderDid } = input;
694
835
  const descriptor = record.descriptor;
695
- if (record.descriptorDigest !== EMAIL_PROOF_PINS.descriptorDigest || await (0, import_sdk_core3.canonicalDigest)(descriptor) !== EMAIL_PROOF_PINS.descriptorDigest) {
696
- throw new import_sdk_core3.CredentialError("VERIFICATION_FAILED", "stored descriptor does not match the pinned descriptor digest");
836
+ if (record.descriptorDigest !== EMAIL_PROOF_PINS.descriptorDigest || await (0, import_sdk_core5.canonicalDigest)(descriptor) !== EMAIL_PROOF_PINS.descriptorDigest) {
837
+ throw new import_sdk_core5.CredentialError("VERIFICATION_FAILED", "stored descriptor does not match the pinned descriptor digest");
697
838
  }
698
839
  const envelope = {
699
840
  type: "OpenCredentialsIssuedCredential",
@@ -717,7 +858,7 @@ async function verifyStoredWithIssuerKeys(input, issuerKeys) {
717
858
  status: record.status,
718
859
  credential: record.credential
719
860
  };
720
- const verified = await (0, import_sdk_core3.verifyIssuedCredential)({
861
+ const verified = await (0, import_sdk_core5.verifyIssuedCredential)({
721
862
  envelope,
722
863
  descriptor,
723
864
  descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest,
@@ -728,17 +869,61 @@ async function verifyStoredWithIssuerKeys(input, issuerKeys) {
728
869
  checkStatus: async (status) => status.method === "none" && status.freshnessSeconds === descriptor.status.freshnessSeconds,
729
870
  signal: input.signal
730
871
  });
731
- if (record.holderDid !== holderDid || record.ownerDid !== holderDid) throw new import_sdk_core3.CredentialError("HOLDER_MISMATCH", "stored record belongs to another holder");
732
- if (record.credentialDigest !== verified.credentialDigest) throw new import_sdk_core3.CredentialError("VERIFICATION_FAILED", "stored credential digest does not match the verified credential");
733
- if (record.requirementDigest !== await (0, import_sdk_core3.credentialRequirementDigest)(requirement)) throw new import_sdk_core3.CredentialError("VERIFICATION_FAILED", "stored requirement digest does not match the requirement");
734
- if (record.recordId !== await (0, import_sdk_core3.canonicalDigest)({ credentialDigest: verified.credentialDigest, holderDid: record.ownerDid })) throw new import_sdk_core3.CredentialError("VERIFICATION_FAILED", "stored record id does not match its credential");
872
+ if (record.holderDid !== holderDid || record.ownerDid !== holderDid) throw new import_sdk_core5.CredentialError("HOLDER_MISMATCH", "stored record belongs to another holder");
873
+ if (record.credentialDigest !== verified.credentialDigest) throw new import_sdk_core5.CredentialError("VERIFICATION_FAILED", "stored credential digest does not match the verified credential");
874
+ if (record.requirementDigest !== await (0, import_sdk_core5.credentialRequirementDigest)(requirement)) throw new import_sdk_core5.CredentialError("VERIFICATION_FAILED", "stored requirement digest does not match the requirement");
875
+ if (record.recordId !== await (0, import_sdk_core5.canonicalDigest)({ credentialDigest: verified.credentialDigest, holderDid: record.ownerDid })) throw new import_sdk_core5.CredentialError("VERIFICATION_FAILED", "stored record id does not match its credential");
876
+ return verified;
877
+ }
878
+ async function verifyHeldResult(input) {
879
+ const { envelope, pending, descriptor } = input;
880
+ const refuse = (message) => new import_sdk_core5.CredentialError("VERIFICATION_FAILED", message);
881
+ const iatMs = Date.parse(envelope.issuedAt);
882
+ const clientMs = input.now().getTime();
883
+ if (!Number.isFinite(iatMs) || iatMs - clientMs > CLOCK_SKEW_BOUND_MS) throw refuse("client clock is more than 5 minutes behind the issuer");
884
+ const key = input.issuerKeys.find((candidate) => candidate.kid === EMAIL_PROOF_PINS.issuerKid);
885
+ if (!key) throw refuse("no pinned issuer key");
886
+ const issuerSet = (0, import_held_credentials.parseIssuerSet)(
887
+ { 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 }] },
888
+ { didMethods: { issuer: ["web"] } }
889
+ );
890
+ const verified = (0, import_held_credentials.verifyHeldEmailCredential)(envelope.credential, {
891
+ now: new Date(Math.max(clientMs, iatMs)),
892
+ owner: pending.holderDid,
893
+ didMethods: { issuer: ["web"] },
894
+ acceptedIssuers: [{ issuerDid: EMAIL_PROOF_PINS.issuerDid, kid: EMAIL_PROOF_PINS.issuerKid }],
895
+ issuerSet
896
+ });
897
+ const signed = input.transport.signedBinding(pending.requestId);
898
+ const claims = Object.keys(envelope.claims).sort().join();
899
+ const toSecond = (ms) => Math.floor(ms / 1e3);
900
+ if (
901
+ // (i) the holder, as the one EIP-55 string
902
+ envelope.subjectDid !== pending.holderDid || envelope.holderDid !== pending.holderDid || // (ii) exactly {email, emailDomain}, equal to the credential's, and the requested mailbox
903
+ claims !== "email,emailDomain" || envelope.claims.email !== verified.claims.email || envelope.claims.emailDomain !== verified.claims.emailDomain || envelope.claims.email !== pending.address || // (iii) validity to the second
904
+ toSecond(Date.parse(envelope.issuedAt)) !== toSecond(verified.issuedAt.getTime()) || toSecond(Date.parse(envelope.expiresAt)) !== toSecond(verified.expiresAt.getTime()) || // (v) the pins
905
+ envelope.descriptorDigest !== input.descriptorDigest || envelope.profile.id !== HELD_PROFILE || envelope.profile.version !== 1 || envelope.issuerDid !== EMAIL_PROOF_PINS.issuerDid || envelope.issuerKid !== EMAIL_PROOF_PINS.issuerKid || envelope.format !== "vc+sd-jwt" || envelope.status.method !== "none" || envelope.status.freshnessSeconds !== descriptor.status.freshnessSeconds
906
+ ) throw refuse("issued credential does not match the request");
907
+ if (signed?.type !== descriptor.holderBinding.domain || !("credentialLifetimeSeconds" in signed)) throw refuse("signed binding is missing");
908
+ if (verified.credentialId !== signed.jti || (verified.expiresAt.getTime() - verified.issuedAt.getTime()) / 1e3 !== signed.credentialLifetimeSeconds) throw refuse("credential does not match the binding the account signed");
735
909
  return verified;
736
910
  }
737
- function holderSigning(holder) {
738
- return { autoSign: async (_binding, bytes) => holder.sign(bytes), holderKey: { alg: "EdDSA", kid: holder.kid } };
911
+ function holderSigning(holder, held) {
912
+ return {
913
+ autoSign: async (_binding, bytes) => {
914
+ try {
915
+ return await holder.sign(bytes);
916
+ } catch (error) {
917
+ const aborted = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
918
+ if (!held || error instanceof EmailProofError || aborted) throw error;
919
+ throw new EmailProofError("credential", "SIGNATURE_REJECTED");
920
+ }
921
+ },
922
+ holderKey: { alg: holder.alg, kid: holder.kid }
923
+ };
739
924
  }
740
925
  function randomVerifier() {
741
- return (0, import_sdk_core3.encodeBase64Url)(crypto.getRandomValues(new Uint8Array(32)));
926
+ return (0, import_sdk_core5.encodeBase64Url)(crypto.getRandomValues(new Uint8Array(32)));
742
927
  }
743
928
  function isPending(value) {
744
929
  if (typeof value !== "object" || value === null) return false;
@@ -748,7 +933,7 @@ function isPending(value) {
748
933
  function mapError(value, step, signal) {
749
934
  if (value instanceof EmailProofError) return value;
750
935
  if (value instanceof DOMException && value.name === "TimeoutError") return new EmailProofError(step, "NETWORK");
751
- const error = (0, import_sdk_core3.credentialError)(value);
936
+ const error = (0, import_sdk_core5.credentialError)(value);
752
937
  const details = error.details;
753
938
  switch (error.code) {
754
939
  case "ISSUER_UNREADY":
@@ -757,6 +942,7 @@ function mapError(value, step, signal) {
757
942
  if (signal?.aborted && signal.reason?.name !== "TimeoutError") return new EmailProofError(step, "CANCELED");
758
943
  return new EmailProofError(step, "NETWORK");
759
944
  case "UNSUPPORTED_PROFILE":
945
+ case "UNSUPPORTED_VERSION":
760
946
  case "DESCRIPTOR_INVALID":
761
947
  return new EmailProofError(step, "PROFILE_UNAVAILABLE");
762
948
  case "PROOF_REJECTED":
@@ -785,7 +971,9 @@ function mapError(value, step, signal) {
785
971
  DEFAULT_EMAIL_PROOF_PROFILE,
786
972
  EMAIL_PROOF_CREDENTIAL_TYPE,
787
973
  EMAIL_PROOF_PENDING_MAX_MS,
974
+ EMAIL_PROOF_PINS,
788
975
  EmailProofError,
976
+ HELD_ACCOUNT_BINDING_PINS,
789
977
  OpenCredentialsHttpTransport,
790
978
  acquireEmailProof,
791
979
  completeEmailProof,
@@ -793,6 +981,7 @@ function mapError(value, step, signal) {
793
981
  createMemoryEmailProofPendingStore,
794
982
  emailProofRequirement,
795
983
  interpretCredentialFlow,
984
+ isTicketEmailProofResult,
796
985
  mailboxSubject,
797
986
  requestEmailProofCode,
798
987
  verifyStoredEmailProof