@tinycloud/node-sdk 3.8.1-beta.2 → 3.9.0-beta.4

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`;
@@ -335,9 +416,17 @@ var EMAIL_PROOF_ISSUER_KEYS = [
335
416
  ];
336
417
  var DEFAULT_EMAIL_PROOF_PROFILE = "tinycloud.email-proof/v1";
337
418
  var PROFILE_PINS = {
338
- "tinycloud.email-proof/v1": { descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest, requiredStep: "mailbox_otp", enabled: true }
419
+ "tinycloud.email-proof/v1": { descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest, requiredStep: "mailbox_otp", enabled: true },
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 }
339
423
  };
340
- 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;
341
430
  var STEP_TIMEOUT_MS = 6e4;
342
431
  var HOLDER_DID_PREFIX = "did:key:";
343
432
  var HOLDER_DID_PATTERN = /^did:key:z6Mk[^#]+$/;
@@ -357,6 +446,7 @@ var RECOVERABILITY = {
357
446
  CREDENTIAL_INVALID: false,
358
447
  SESSION_EXPIRED: false,
359
448
  SESSION_INVALID: false,
449
+ CLIENT_CLOCK_SKEW: false,
360
450
  CANCELED: false,
361
451
  FAILED: false
362
452
  };
@@ -373,6 +463,7 @@ var MESSAGES = {
373
463
  CREDENTIAL_INVALID: "Issued credential failed verification.",
374
464
  SESSION_EXPIRED: "Email verification request expired. Request a new code.",
375
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.",
376
467
  CANCELED: "Email verification was canceled.",
377
468
  FAILED: "Email verification failed."
378
469
  };
@@ -411,7 +502,8 @@ function restoreEmailProofPending(fields) {
411
502
  requirementDigest: fields.requirementDigest,
412
503
  openerOrigin: fields.openerOrigin,
413
504
  createdAt: fields.createdAt,
414
- expiresAt: fields.expiresAt
505
+ expiresAt: fields.expiresAt,
506
+ ...fields.serverExpiresAt === void 0 ? {} : { serverExpiresAt: fields.serverExpiresAt }
415
507
  };
416
508
  const pending = { ...shown };
417
509
  hide(pending, "verifier", fields.verifier);
@@ -451,6 +543,9 @@ function createMemoryEmailProofPendingStore() {
451
543
  }
452
544
  };
453
545
  }
546
+ function isTicketEmailProofResult(result) {
547
+ return result.envelope.profile.id !== HELD_PROFILE;
548
+ }
454
549
  var CodeRequested = class extends Error {
455
550
  };
456
551
  var pauseForCode = async () => {
@@ -458,17 +553,19 @@ var pauseForCode = async () => {
458
553
  };
459
554
  async function requestEmailProofCode(input) {
460
555
  const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;
461
- profilePin(profile, "request");
462
- const address = normalizeAddress(input.address);
463
- assertHolder(input.holder, "request");
556
+ profilePin(profile, "request", input.allowHeldProfile);
557
+ const held = profile === HELD_PROFILE;
558
+ if (held) assertHolder(input.holder, "request", profile);
559
+ const address = normalizeAddress(input.address, profile);
560
+ if (!held) assertHolder(input.holder, "request", profile);
464
561
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
465
562
  const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);
466
563
  const requirement = emailRequirement(address, profile);
467
- const requirementDigest = await (0, import_sdk_core3.credentialRequirementDigest)(requirement);
468
- const descriptor = await witnessDescriptor(profile, requirement, { fetch: input.fetch, signal }, "request");
469
- const descriptorDigest = await (0, import_sdk_core3.canonicalDigest)(descriptor);
564
+ const requirementDigest = await (0, import_sdk_core5.credentialRequirementDigest)(requirement);
565
+ const descriptor = await witnessDescriptor(profile, requirement, { fetch: input.fetch, signal }, "request", input.allowHeldProfile);
566
+ const descriptorDigest = await (0, import_sdk_core5.canonicalDigest)(descriptor);
470
567
  const verifier = randomVerifier();
471
- const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });
568
+ const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true, ...held ? { allowAccountSubject: true } : {} });
472
569
  const createdAtMs = now().getTime();
473
570
  let created;
474
571
  try {
@@ -479,7 +576,7 @@ async function requestEmailProofCode(input) {
479
576
  requirementDigest,
480
577
  holderDid: input.holder.did,
481
578
  openerOrigin: OPENER_ORIGIN,
482
- completionVerifierChallenge: await (0, import_sdk_core3.sha256Base64Url)(verifier),
579
+ completionVerifierChallenge: await (0, import_sdk_core5.sha256Base64Url)(verifier),
483
580
  signal
484
581
  });
485
582
  } catch (error) {
@@ -487,6 +584,8 @@ async function requestEmailProofCode(input) {
487
584
  }
488
585
  const serverExpiry = Date.parse(created.expiresAt);
489
586
  if (!Number.isFinite(serverExpiry)) throw new EmailProofError("request", "FAILED");
587
+ const serverNowMs = serverExpiry - HELD_REQUEST_TTL_MS;
588
+ if (held && serverNowMs - now().getTime() > CLOCK_SKEW_BOUND_MS) throw new EmailProofError("request", "CLIENT_CLOCK_SKEW");
490
589
  const pending = restoreEmailProofPending({
491
590
  type: "TinyCloudEmailProofPending",
492
591
  version: 1,
@@ -499,11 +598,14 @@ async function requestEmailProofCode(input) {
499
598
  requirementDigest,
500
599
  openerOrigin: OPENER_ORIGIN,
501
600
  createdAt: new Date(createdAtMs).toISOString(),
502
- expiresAt: new Date(Math.min(serverExpiry, createdAtMs + EMAIL_PROOF_PENDING_MAX_MS)).toISOString()
601
+ // Held: the deadline is measured on the client clock (a fast client must not lose window), the witness enforces its own 600 s.
602
+ expiresAt: new Date(held ? createdAtMs + Math.min(serverExpiry - serverNowMs, EMAIL_PROOF_PENDING_MAX_MS) : Math.min(serverExpiry, createdAtMs + EMAIL_PROOF_PENDING_MAX_MS)).toISOString(),
603
+ ...held ? { serverExpiresAt: created.expiresAt } : {}
503
604
  });
504
605
  try {
505
606
  await interpretCredentialFlow({
506
607
  descriptor,
608
+ ...held ? { allowAccountSubject: true } : {},
507
609
  requirement,
508
610
  requestId: created.requestId,
509
611
  verifier,
@@ -511,8 +613,9 @@ async function requestEmailProofCode(input) {
511
613
  descriptorDigest,
512
614
  requirementDigest,
513
615
  openerOrigin: OPENER_ORIGIN,
616
+ ...held ? { expiresAt: created.expiresAt } : {},
514
617
  transport,
515
- signing: holderSigning(input.holder),
618
+ signing: holderSigning(input.holder, held),
516
619
  handlers: { mailbox_otp: pauseForCode },
517
620
  signal
518
621
  });
@@ -529,22 +632,26 @@ async function completeWithIssuerKeys(input, issuerKeys) {
529
632
  const { pending } = input;
530
633
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
531
634
  if (!isPending(pending)) throw new EmailProofError("session", "SESSION_INVALID");
635
+ profilePin(pending.profile, "session", input.allowHeldProfile);
636
+ const held = pending.profile === HELD_PROFILE;
637
+ if (held && !Number.isFinite(Date.parse(pending.serverExpiresAt ?? ""))) throw new EmailProofError("session", "SESSION_INVALID");
532
638
  if (pendingDeadlineMs(pending) <= now().getTime()) throw new EmailProofError("session", "SESSION_EXPIRED");
533
639
  if (!/^\d{8}$/.test(input.code)) throw new EmailProofError("code", "CODE_INVALID");
534
- assertHolder(input.holder, "code");
640
+ assertHolder(input.holder, "code", pending.profile);
535
641
  if (input.holder.did !== pending.holderDid) throw new EmailProofError("session", "SESSION_INVALID");
536
642
  const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);
537
643
  const requirement = emailRequirement(pending.address, pending.profile);
538
- if (await (0, import_sdk_core3.credentialRequirementDigest)(requirement) !== pending.requirementDigest) throw new EmailProofError("session", "SESSION_INVALID");
539
- const descriptor = await witnessDescriptor(pending.profile, requirement, { fetch: input.fetch, signal }, "session");
540
- const descriptorDigest = await (0, import_sdk_core3.canonicalDigest)(descriptor);
644
+ if (await (0, import_sdk_core5.credentialRequirementDigest)(requirement) !== pending.requirementDigest) throw new EmailProofError("session", "SESSION_INVALID");
645
+ const descriptor = await witnessDescriptor(pending.profile, requirement, { fetch: input.fetch, signal }, "session", input.allowHeldProfile);
646
+ const descriptorDigest = await (0, import_sdk_core5.canonicalDigest)(descriptor);
541
647
  if (descriptorDigest !== pending.descriptorDigest) throw new EmailProofError("session", "SESSION_INVALID");
542
- const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });
648
+ const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true, ...held ? { allowAccountSubject: true } : {} });
543
649
  try {
544
650
  const state = await transport.state(pending.requestId, pending.verifier, signal);
545
651
  if (state.nextStep?.constraints.challengeRequired === true) throw new EmailProofError("session", "SESSION_INVALID");
546
652
  await interpretCredentialFlow({
547
653
  descriptor,
654
+ ...held ? { allowAccountSubject: true } : {},
548
655
  requirement,
549
656
  requestId: pending.requestId,
550
657
  verifier: pending.verifier,
@@ -552,8 +659,9 @@ async function completeWithIssuerKeys(input, issuerKeys) {
552
659
  descriptorDigest,
553
660
  requirementDigest: pending.requirementDigest,
554
661
  openerOrigin: pending.openerOrigin,
662
+ ...held ? { expiresAt: pending.serverExpiresAt } : {},
555
663
  transport,
556
- signing: holderSigning(input.holder),
664
+ signing: holderSigning(input.holder, held),
557
665
  handlers: { mailbox_otp: async () => ({ otp: input.code }) },
558
666
  signal
559
667
  });
@@ -562,7 +670,11 @@ async function completeWithIssuerKeys(input, issuerKeys) {
562
670
  }
563
671
  try {
564
672
  const envelope = await transport.result(pending.requestId, pending.verifier, signal);
565
- const verified = await (0, import_sdk_core3.verifyIssuedCredential)({
673
+ if (held) {
674
+ const verified2 = await verifyHeldResult({ envelope, pending, descriptor, descriptorDigest, transport, issuerKeys, now });
675
+ return { envelope, verified: verified2, credentialId: verified2.credentialId, issuedAt: verified2.issuedAt.toISOString(), expiresAt: verified2.expiresAt.toISOString(), descriptor };
676
+ }
677
+ const verified = await (0, import_sdk_core5.verifyIssuedCredential)({
566
678
  envelope,
567
679
  descriptor,
568
680
  descriptorDigest,
@@ -582,10 +694,17 @@ async function acquireEmailProof(input) {
582
694
  return acquireWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);
583
695
  }
584
696
  async function acquireWithIssuerKeys(input, issuerKeys) {
585
- const address = normalizeAddress(input.address);
697
+ const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;
698
+ profilePin(profile, "request", input.allowHeldProfile);
699
+ const address = normalizeAddress(input.address, profile);
586
700
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
587
701
  const store = input.pendingStore;
588
- let pending = store ? await store.findLive(address, now()) : void 0;
702
+ const live = store ? await store.findLive(address, now()) : void 0;
703
+ let pending = live?.profile === profile ? live : void 0;
704
+ if (pending && profile === HELD_PROFILE && !Number.isFinite(Date.parse(pending.serverExpiresAt ?? ""))) {
705
+ await store?.delete(pending.requestId).catch(() => void 0);
706
+ throw new EmailProofError("session", "SESSION_INVALID");
707
+ }
589
708
  if (!pending) {
590
709
  pending = await requestEmailProofCode({ ...input, address });
591
710
  await store?.put(pending);
@@ -609,30 +728,52 @@ async function acquireWithIssuerKeys(input, issuerKeys) {
609
728
  throw error;
610
729
  }
611
730
  }
612
- function normalizeAddress(address) {
731
+ function normalizeAddress(address, profile) {
732
+ if (profile === HELD_PROFILE) return normalizeHeldAddress(address);
613
733
  const value = address.trim();
614
734
  if (value.length > 254 || !EMAIL_PATTERN.test(value)) throw new EmailProofError("request", "ADDRESS_INVALID");
615
735
  return value;
616
736
  }
737
+ var HELD_LOCAL_ATOM = /^[a-z0-9!#$%&'*+\-/=?^_`{|}~]+(\.[a-z0-9!#$%&'*+\-/=?^_`{|}~]+)*$/;
738
+ var HELD_DOMAIN_LABEL = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
739
+ function normalizeHeldAddress(address) {
740
+ const invalid = () => new EmailProofError("request", "ADDRESS_INVALID");
741
+ const trimmed = address.trim();
742
+ if (/[^\x00-\x7f]/.test(trimmed)) throw invalid();
743
+ const value = trimmed.toLowerCase();
744
+ const parts = value.split("@");
745
+ if (value.length < 3 || value.length > 254 || parts.length !== 2 || /[^\x21-\x7e]/.test(value)) throw invalid();
746
+ const [local, domain] = parts;
747
+ if (local.length > 64 || !HELD_LOCAL_ATOM.test(local) || local.startsWith("+")) throw invalid();
748
+ if (domain.length > 253 || !domain.includes(".") || !domain.split(".").every((label) => label.length <= 63 && HELD_DOMAIN_LABEL.test(label))) throw invalid();
749
+ return value;
750
+ }
617
751
  function holderKid(did) {
618
752
  return `${did}#${did.slice(HOLDER_DID_PREFIX.length)}`;
619
753
  }
620
- function assertHolder(holder, step) {
754
+ function assertHeldHolder(holder, step) {
755
+ const parsed = (0, import_held_credentials.parsePkhDid)(holder.did);
756
+ const canonical = parsed !== null && parsed.chainId === "1" ? `did:pkh:eip155:1:${(0, import_held_credentials.toChecksumAddress)(parsed.address)}` : void 0;
757
+ if (canonical === void 0 || holder.did !== canonical || holder.alg !== "EIP191" || holder.kid !== `${canonical}#${HELD_KID_FRAGMENT}`) throw new EmailProofError(step, "HOLDER_INVALID");
758
+ }
759
+ function assertHolder(holder, step, profile) {
760
+ if (profile === HELD_PROFILE) return assertHeldHolder(holder, step);
621
761
  if (!HOLDER_DID_PATTERN.test(holder.did) || holder.alg !== "EdDSA" || holder.kid !== holderKid(holder.did)) throw new EmailProofError(step, "HOLDER_INVALID");
622
762
  }
623
- function profilePin(profile, step) {
763
+ function profilePin(profile, step, allowHeld) {
624
764
  const entry = Object.prototype.hasOwnProperty.call(PROFILE_PINS, profile) ? PROFILE_PINS[profile] : void 0;
625
- if (!entry || !entry.enabled) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
765
+ if (!entry || !entry.enabled && !(allowHeld === true && profile === HELD_PROFILE)) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
626
766
  return entry;
627
767
  }
628
768
  function emailRequirement(address, profile) {
629
- return (0, import_sdk_core3.createEmailCredentialRequirement)({ email: address, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE });
769
+ 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 });
770
+ return (0, import_sdk_core5.createEmailCredentialRequirement)({ email: address, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE });
630
771
  }
631
772
  function emailProofRequirement(address) {
632
- return emailRequirement(normalizeAddress(address), DEFAULT_EMAIL_PROOF_PROFILE);
773
+ return emailRequirement(normalizeAddress(address, DEFAULT_EMAIL_PROOF_PROFILE), DEFAULT_EMAIL_PROOF_PROFILE);
633
774
  }
634
- async function witnessDescriptor(profile, requirement, options, step) {
635
- const pin = profilePin(profile, step);
775
+ async function witnessDescriptor(profile, requirement, options, step, allowHeld) {
776
+ const pin = profilePin(profile, step, allowHeld);
636
777
  let descriptor;
637
778
  try {
638
779
  descriptor = await discoverCatalogDescriptor(requirement, DISCOVERY_URL, { fetch: options.fetch, signal: options.signal });
@@ -642,7 +783,7 @@ async function witnessDescriptor(profile, requirement, options, step) {
642
783
  if (descriptor.issuer.origin !== EMAIL_PROOF_PINS.witnessOrigin || descriptor.issuer.did !== EMAIL_PROOF_PINS.issuerDid || !descriptor.steps.some((candidate) => candidate.type === pin.requiredStep)) {
643
784
  throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
644
785
  }
645
- if (await (0, import_sdk_core3.canonicalDigest)(descriptor) !== pin.descriptorDigest) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
786
+ if (await (0, import_sdk_core5.canonicalDigest)(descriptor) !== pin.descriptorDigest) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
646
787
  return descriptor;
647
788
  }
648
789
  async function discoverCatalogDescriptor(requirement, discoveryUrl, options = {}) {
@@ -650,30 +791,30 @@ async function discoverCatalogDescriptor(requirement, discoveryUrl, options = {}
650
791
  try {
651
792
  url = new URL(discoveryUrl);
652
793
  } catch {
653
- throw new import_sdk_core3.CredentialError("UNSUPPORTED_PROFILE", "Discovery URL is invalid");
794
+ throw new import_sdk_core5.CredentialError("UNSUPPORTED_PROFILE", "Discovery URL is invalid");
654
795
  }
655
- 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");
796
+ 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");
656
797
  const init = { credentials: "omit", cache: "no-store", redirect: "error", referrerPolicy: "no-referrer", signal: options.signal };
657
798
  let response;
658
799
  try {
659
800
  response = await (options.fetch ?? globalThis.fetch.bind(globalThis))(url, init);
660
801
  } catch (cause) {
661
- throw new import_sdk_core3.CredentialError("OFFLINE", "Credential discovery is unavailable", { cause });
802
+ throw new import_sdk_core5.CredentialError("OFFLINE", "Credential discovery is unavailable", { cause });
662
803
  }
663
- if (!response.ok) throw new import_sdk_core3.CredentialError(response.status === 503 ? "ISSUER_UNREADY" : "OFFLINE", "Credential discovery is unavailable");
804
+ if (!response.ok) throw new import_sdk_core5.CredentialError(response.status === 503 ? "ISSUER_UNREADY" : "OFFLINE", "Credential discovery is unavailable");
664
805
  const catalog = await response.json();
665
- 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");
806
+ 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");
666
807
  for (const entryValue of catalog.profiles) {
667
808
  if (typeof entryValue !== "object" || entryValue === null || Array.isArray(entryValue)) continue;
668
809
  const entry = entryValue;
669
810
  if (entry.supported !== true || entry.enabled !== true || entry.readiness !== "ready" && entry.readiness !== "degraded") continue;
670
811
  try {
671
- const candidate = (0, import_sdk_core3.validateCredentialFlowDescriptor)(entry.descriptor);
672
- if ((0, import_sdk_core3.descriptorSatisfiesRequirement)(candidate, requirement) && entry.descriptorDigest === await (0, import_sdk_core3.canonicalDigest)(candidate)) return candidate;
812
+ const candidate = (0, import_sdk_core5.validateCredentialFlowDescriptor)(entry.descriptor);
813
+ if ((0, import_sdk_core5.descriptorSatisfiesRequirement)(candidate, requirement) && entry.descriptorDigest === await (0, import_sdk_core5.canonicalDigest)(candidate)) return candidate;
673
814
  } catch {
674
815
  }
675
816
  }
676
- throw new import_sdk_core3.CredentialError("UNSUPPORTED_PROFILE", "No available credential profile satisfies the requirement");
817
+ throw new import_sdk_core5.CredentialError("UNSUPPORTED_PROFILE", "No available credential profile satisfies the requirement");
677
818
  }
678
819
  function discoveryFailure(error, signal, step) {
679
820
  if (signal.aborted && signal.reason?.name !== "TimeoutError") return new EmailProofError(step, "CANCELED");
@@ -688,8 +829,8 @@ async function verifyStoredEmailProof(input) {
688
829
  async function verifyStoredWithIssuerKeys(input, issuerKeys) {
689
830
  const { record, requirement, holderDid } = input;
690
831
  const descriptor = record.descriptor;
691
- if (record.descriptorDigest !== EMAIL_PROOF_PINS.descriptorDigest || await (0, import_sdk_core3.canonicalDigest)(descriptor) !== EMAIL_PROOF_PINS.descriptorDigest) {
692
- throw new import_sdk_core3.CredentialError("VERIFICATION_FAILED", "stored descriptor does not match the pinned descriptor digest");
832
+ if (record.descriptorDigest !== EMAIL_PROOF_PINS.descriptorDigest || await (0, import_sdk_core5.canonicalDigest)(descriptor) !== EMAIL_PROOF_PINS.descriptorDigest) {
833
+ throw new import_sdk_core5.CredentialError("VERIFICATION_FAILED", "stored descriptor does not match the pinned descriptor digest");
693
834
  }
694
835
  const envelope = {
695
836
  type: "OpenCredentialsIssuedCredential",
@@ -713,7 +854,7 @@ async function verifyStoredWithIssuerKeys(input, issuerKeys) {
713
854
  status: record.status,
714
855
  credential: record.credential
715
856
  };
716
- const verified = await (0, import_sdk_core3.verifyIssuedCredential)({
857
+ const verified = await (0, import_sdk_core5.verifyIssuedCredential)({
717
858
  envelope,
718
859
  descriptor,
719
860
  descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest,
@@ -724,17 +865,61 @@ async function verifyStoredWithIssuerKeys(input, issuerKeys) {
724
865
  checkStatus: async (status) => status.method === "none" && status.freshnessSeconds === descriptor.status.freshnessSeconds,
725
866
  signal: input.signal
726
867
  });
727
- if (record.holderDid !== holderDid || record.ownerDid !== holderDid) throw new import_sdk_core3.CredentialError("HOLDER_MISMATCH", "stored record belongs to another holder");
728
- if (record.credentialDigest !== verified.credentialDigest) throw new import_sdk_core3.CredentialError("VERIFICATION_FAILED", "stored credential digest does not match the verified credential");
729
- 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");
730
- 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");
868
+ if (record.holderDid !== holderDid || record.ownerDid !== holderDid) throw new import_sdk_core5.CredentialError("HOLDER_MISMATCH", "stored record belongs to another holder");
869
+ if (record.credentialDigest !== verified.credentialDigest) throw new import_sdk_core5.CredentialError("VERIFICATION_FAILED", "stored credential digest does not match the verified credential");
870
+ 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");
871
+ 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");
872
+ return verified;
873
+ }
874
+ async function verifyHeldResult(input) {
875
+ const { envelope, pending, descriptor } = input;
876
+ const refuse = (message) => new import_sdk_core5.CredentialError("VERIFICATION_FAILED", message);
877
+ const iatMs = Date.parse(envelope.issuedAt);
878
+ const clientMs = input.now().getTime();
879
+ if (!Number.isFinite(iatMs) || iatMs - clientMs > CLOCK_SKEW_BOUND_MS) throw refuse("client clock is more than 5 minutes behind the issuer");
880
+ const key = input.issuerKeys.find((candidate) => candidate.kid === EMAIL_PROOF_PINS.issuerKid);
881
+ if (!key) throw refuse("no pinned issuer key");
882
+ const issuerSet = (0, import_held_credentials.parseIssuerSet)(
883
+ { 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 }] },
884
+ { didMethods: { issuer: ["web"] } }
885
+ );
886
+ const verified = (0, import_held_credentials.verifyHeldEmailCredential)(envelope.credential, {
887
+ now: new Date(Math.max(clientMs, iatMs)),
888
+ owner: pending.holderDid,
889
+ didMethods: { issuer: ["web"] },
890
+ acceptedIssuers: [{ issuerDid: EMAIL_PROOF_PINS.issuerDid, kid: EMAIL_PROOF_PINS.issuerKid }],
891
+ issuerSet
892
+ });
893
+ const signed = input.transport.signedBinding(pending.requestId);
894
+ const claims = Object.keys(envelope.claims).sort().join();
895
+ const toSecond = (ms) => Math.floor(ms / 1e3);
896
+ if (
897
+ // (i) the holder, as the one EIP-55 string
898
+ envelope.subjectDid !== pending.holderDid || envelope.holderDid !== pending.holderDid || // (ii) exactly {email, emailDomain}, equal to the credential's, and the requested mailbox
899
+ 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
900
+ toSecond(Date.parse(envelope.issuedAt)) !== toSecond(verified.issuedAt.getTime()) || toSecond(Date.parse(envelope.expiresAt)) !== toSecond(verified.expiresAt.getTime()) || // (v) the pins
901
+ 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
902
+ ) throw refuse("issued credential does not match the request");
903
+ if (signed?.type !== descriptor.holderBinding.domain || !("credentialLifetimeSeconds" in signed)) throw refuse("signed binding is missing");
904
+ 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");
731
905
  return verified;
732
906
  }
733
- function holderSigning(holder) {
734
- return { autoSign: async (_binding, bytes) => holder.sign(bytes), holderKey: { alg: "EdDSA", kid: holder.kid } };
907
+ function holderSigning(holder, held) {
908
+ return {
909
+ autoSign: async (_binding, bytes) => {
910
+ try {
911
+ return await holder.sign(bytes);
912
+ } catch (error) {
913
+ const aborted = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
914
+ if (!held || error instanceof EmailProofError || aborted) throw error;
915
+ throw new EmailProofError("credential", "SIGNATURE_REJECTED");
916
+ }
917
+ },
918
+ holderKey: { alg: holder.alg, kid: holder.kid }
919
+ };
735
920
  }
736
921
  function randomVerifier() {
737
- return (0, import_sdk_core3.encodeBase64Url)(crypto.getRandomValues(new Uint8Array(32)));
922
+ return (0, import_sdk_core5.encodeBase64Url)(crypto.getRandomValues(new Uint8Array(32)));
738
923
  }
739
924
  function isPending(value) {
740
925
  if (typeof value !== "object" || value === null) return false;
@@ -744,7 +929,7 @@ function isPending(value) {
744
929
  function mapError(value, step, signal) {
745
930
  if (value instanceof EmailProofError) return value;
746
931
  if (value instanceof DOMException && value.name === "TimeoutError") return new EmailProofError(step, "NETWORK");
747
- const error = (0, import_sdk_core3.credentialError)(value);
932
+ const error = (0, import_sdk_core5.credentialError)(value);
748
933
  const details = error.details;
749
934
  switch (error.code) {
750
935
  case "ISSUER_UNREADY":
@@ -753,6 +938,7 @@ function mapError(value, step, signal) {
753
938
  if (signal?.aborted && signal.reason?.name !== "TimeoutError") return new EmailProofError(step, "CANCELED");
754
939
  return new EmailProofError(step, "NETWORK");
755
940
  case "UNSUPPORTED_PROFILE":
941
+ case "UNSUPPORTED_VERSION":
756
942
  case "DESCRIPTOR_INVALID":
757
943
  return new EmailProofError(step, "PROFILE_UNAVAILABLE");
758
944
  case "PROOF_REJECTED":
@@ -781,7 +967,9 @@ function mapError(value, step, signal) {
781
967
  DEFAULT_EMAIL_PROOF_PROFILE,
782
968
  EMAIL_PROOF_CREDENTIAL_TYPE,
783
969
  EMAIL_PROOF_PENDING_MAX_MS,
970
+ EMAIL_PROOF_PINS,
784
971
  EmailProofError,
972
+ HELD_ACCOUNT_BINDING_PINS,
785
973
  OpenCredentialsHttpTransport,
786
974
  acquireEmailProof,
787
975
  completeEmailProof,
@@ -789,6 +977,7 @@ function mapError(value, step, signal) {
789
977
  createMemoryEmailProofPendingStore,
790
978
  emailProofRequirement,
791
979
  interpretCredentialFlow,
980
+ isTicketEmailProofResult,
792
981
  mailboxSubject,
793
982
  requestEmailProofCode,
794
983
  verifyStoredEmailProof