@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.
@@ -27,7 +27,49 @@ __export(core_exports, {
27
27
  module.exports = __toCommonJS(core_exports);
28
28
 
29
29
  // src/credentials/interpreter.ts
30
+ var import_sdk_core3 = require("@tinycloud/sdk-core");
31
+
32
+ // src/credentials/held-binding.ts
33
+ var import_sdk_core2 = require("@tinycloud/sdk-core");
34
+
35
+ // src/credentials/pins.ts
30
36
  var import_sdk_core = require("@tinycloud/sdk-core");
37
+ var EMAIL_PROOF_PINS = Object.freeze({
38
+ witnessOrigin: "https://witness.credentials.org",
39
+ issuerDid: "did:web:issuer.credentials.org",
40
+ issuerKid: "did:web:issuer.credentials.org#controller",
41
+ issuerX: "eEJI4xEobto4HtQ7Pg9R1vBOwcpfGRTlXDG5QfqLnGQ",
42
+ descriptorDigest: "1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0"
43
+ });
44
+ var HELD_ACCOUNT_BINDING_PINS = Object.freeze({
45
+ type: import_sdk_core.ACCOUNT_HOLDER_BINDING_DOMAIN,
46
+ protocol: "tinycloud.credentials/acquisition/v1",
47
+ profile: "tinycloud.email-held/v1",
48
+ profileVersion: 1,
49
+ descriptorDigest: "dc3_DuvzipXwYkTfjKqTaK1c3VS0YSPnaBsYSQriC0w",
50
+ issuer: EMAIL_PROOF_PINS.issuerDid,
51
+ issuerKid: EMAIL_PROOF_PINS.issuerKid,
52
+ audience: "tinycloud://credentials",
53
+ openerOrigin: "https://localhost",
54
+ completionOrigin: "https://localhost",
55
+ completionContext: "sdk-acquisition",
56
+ credentialLifetimeSeconds: 31536e3
57
+ });
58
+
59
+ // src/credentials/held-binding.ts
60
+ var REQUEST_TTL_MS = 6e5;
61
+ var substituted = () => new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
62
+ async function assertHeldBinding(binding, expected) {
63
+ const { descriptor } = expected;
64
+ 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).
65
+ 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();
66
+ if (expected.challengeNonce === void 0 || binding.challengeNonce !== expected.challengeNonce) throw substituted();
67
+ if (binding.normalizedClaimsDigest !== await (0, import_sdk_core2.canonicalDigest)(expected.requirement.claims)) throw substituted();
68
+ const expires = Date.parse(binding.expiresAt);
69
+ if (expected.serverExpiresAt === void 0 || expires !== Date.parse(expected.serverExpiresAt) || Date.parse(binding.issuedAt) !== expires - REQUEST_TTL_MS) throw substituted();
70
+ }
71
+
72
+ // src/credentials/interpreter.ts
31
73
  var delay = (ms, signal) => new Promise((resolve, reject) => {
32
74
  const timer = setTimeout(resolve, ms);
33
75
  signal?.addEventListener("abort", () => {
@@ -40,18 +82,36 @@ function mailboxSubject(requirement) {
40
82
  return typeof email === "string" && email.length > 0 ? Object.freeze({ kind: "email", value: email }) : void 0;
41
83
  }
42
84
  async function interpretCredentialFlow(input) {
85
+ const held = input.descriptor.subjectRelationship === "account_is_subject";
86
+ if (held && input.allowAccountSubject !== true) throw new import_sdk_core3.CredentialError("UNSUPPORTED_PROFILE", "The held credential profile requires an explicit opt-in");
43
87
  const steps = new Map(input.descriptor.steps.map((step) => [step.type, step]));
44
88
  const completed = /* @__PURE__ */ new Set();
45
89
  let lastTransition = "";
90
+ const checkBinding = async (value) => {
91
+ const binding = (0, import_sdk_core3.validateCredentialHolderBinding)(value);
92
+ 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");
93
+ const alg = input.descriptor.holderBinding.alg;
94
+ if (binding.type !== input.descriptor.holderBinding.domain || alg === "EIP191" && !input.transport.challengeNonce) throw new import_sdk_core3.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
95
+ 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 });
96
+ return binding;
97
+ };
98
+ const ensureHeldBinding = async () => {
99
+ if (!held || input.transport.signedBinding?.(input.requestId) !== void 0) return;
100
+ await checkBinding(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));
101
+ };
46
102
  for (; ; ) {
47
103
  input.signal?.throwIfAborted();
48
104
  const state = await input.transport.state(input.requestId, input.verifier, input.signal);
49
- if (state.requestId !== input.requestId) throw new import_sdk_core.CredentialError("REQUEST_SUBSTITUTED", "Credential request identity changed");
50
- if (state.state === "expired") throw new import_sdk_core.CredentialError("REQUEST_EXPIRED", "Credential request expired", { state: state.state, correlationId: state.correlationId });
51
- if (state.state === "canceled") throw new import_sdk_core.CredentialError("CANCELED", "Credential request was canceled", { state: state.state, correlationId: state.correlationId });
52
- 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 });
53
- if (state.state === "complete") return;
105
+ if (state.requestId !== input.requestId) throw new import_sdk_core3.CredentialError("REQUEST_SUBSTITUTED", "Credential request identity changed");
106
+ if (state.state === "expired") throw new import_sdk_core3.CredentialError("REQUEST_EXPIRED", "Credential request expired", { state: state.state, correlationId: state.correlationId });
107
+ if (state.state === "canceled") throw new import_sdk_core3.CredentialError("CANCELED", "Credential request was canceled", { state: state.state, correlationId: state.correlationId });
108
+ 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 });
109
+ if (state.state === "complete") {
110
+ await ensureHeldBinding();
111
+ return;
112
+ }
54
113
  if (state.state === "ready_to_issue" || state.state === "issued") {
114
+ await ensureHeldBinding();
55
115
  if (state.state === "ready_to_issue" && !completed.has("issue")) {
56
116
  await input.transport.issue(input.requestId, input.verifier, input.signal);
57
117
  completed.add("issue");
@@ -64,7 +124,7 @@ async function interpretCredentialFlow(input) {
64
124
  continue;
65
125
  }
66
126
  const declared = steps.get(next.type);
67
- 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");
127
+ 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");
68
128
  if (completed.has(next.id) || state.transitionId === lastTransition) {
69
129
  await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal));
70
130
  continue;
@@ -72,21 +132,22 @@ async function interpretCredentialFlow(input) {
72
132
  lastTransition = state.transitionId;
73
133
  if (next.type === "holder_signature") {
74
134
  input.onProgress?.({ state: "signing", stepId: next.id, correlationId: state.correlationId });
75
- const binding = (0, import_sdk_core.validateCredentialHolderBinding)(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));
76
- 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");
77
- const bytes = (0, import_sdk_core.holderBindingSigningBytes)(binding);
135
+ const binding = await checkBinding(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));
136
+ const alg = input.descriptor.holderBinding.alg;
137
+ if (alg === "EIP191" !== (input.signing.holderKey?.alg === "EIP191")) throw new import_sdk_core3.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
138
+ const bytes = (0, import_sdk_core3.holderBindingSigningBytes)(binding);
78
139
  let signature = await input.signing.autoSign?.(binding, bytes, input.signal);
79
140
  if (signature === void 0) {
80
- if (!input.signing.requestApproval) throw new import_sdk_core.CredentialError("SIGNATURE_REJECTED", "OpenKey approval is required");
141
+ if (!input.signing.requestApproval) throw new import_sdk_core3.CredentialError("SIGNATURE_REJECTED", "OpenKey approval is required");
81
142
  try {
82
143
  signature = await input.signing.requestApproval(binding, bytes, input.signal);
83
144
  } catch (cause) {
84
- if (cause instanceof import_sdk_core.CredentialError) throw cause;
85
- throw new import_sdk_core.CredentialError("SIGNATURE_REJECTED", "OpenKey approval was rejected", { cause });
145
+ if (cause instanceof import_sdk_core3.CredentialError) throw cause;
146
+ throw new import_sdk_core3.CredentialError("SIGNATURE_REJECTED", "OpenKey approval was rejected", { cause });
86
147
  }
87
148
  }
88
- if (!(signature instanceof Uint8Array) || signature.length === 0) throw new import_sdk_core.CredentialError("SIGNATURE_REJECTED", "OpenKey signature is invalid");
89
- await input.transport.submitHolderSignature(input.requestId, input.verifier, (0, import_sdk_core.encodeBase64Url)(signature), input.signal, input.signing.holderKey);
149
+ if (!(signature instanceof Uint8Array) || signature.length === 0) throw new import_sdk_core3.CredentialError("SIGNATURE_REJECTED", "OpenKey signature is invalid");
150
+ 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);
90
151
  } else {
91
152
  input.onProgress?.({ state: next.type === "collect_input" ? "collecting" : "proving", stepId: next.id, correlationId: state.correlationId });
92
153
  const handler = input.handlers?.[next.type];
@@ -120,9 +181,9 @@ async function interpretCredentialFlow(input) {
120
181
  await input.transport.submitStep(input.requestId, input.verifier, next.id, proof, input.signal);
121
182
  break;
122
183
  } catch (cause) {
123
- if (!(cause instanceof import_sdk_core.CredentialError) || cause.code !== "PROOF_REJECTED") throw cause;
184
+ if (!(cause instanceof import_sdk_core3.CredentialError) || cause.code !== "PROOF_REJECTED") throw cause;
124
185
  const attemptsRemaining = input.descriptor.lifecycle.maxProofAttempts - attempt;
125
- 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 });
186
+ 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 });
126
187
  feedback = { kind: "rejected", attemptsRemaining };
127
188
  }
128
189
  }
@@ -133,27 +194,27 @@ async function interpretCredentialFlow(input) {
133
194
  }
134
195
 
135
196
  // src/credentials/transport.ts
136
- var import_sdk_core2 = require("@tinycloud/sdk-core");
197
+ var import_sdk_core4 = require("@tinycloud/sdk-core");
137
198
  function object(value, label) {
138
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", `${label} is invalid`);
199
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", `${label} is invalid`);
139
200
  return value;
140
201
  }
141
202
  function opaque(value, label) {
142
- if (typeof value !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(value)) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", `${label} is invalid`);
203
+ if (typeof value !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(value)) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", `${label} is invalid`);
143
204
  return value;
144
205
  }
145
206
  function timestamp(value, label) {
146
- if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", `${label} is invalid`);
207
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", `${label} is invalid`);
147
208
  return new Date(value).toISOString();
148
209
  }
149
210
  function jwtPayload(credential) {
150
211
  const part = credential.split("~", 1)[0]?.split(".")[1];
151
- if (!part) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Issued SD-JWT is invalid");
212
+ if (!part) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Issued SD-JWT is invalid");
152
213
  try {
153
- return object(JSON.parse(new TextDecoder().decode((0, import_sdk_core2.decodeBase64Url)(part))), "SD-JWT payload");
214
+ return object(JSON.parse(new TextDecoder().decode((0, import_sdk_core4.decodeBase64Url)(part))), "SD-JWT payload");
154
215
  } catch (cause) {
155
- if (cause instanceof import_sdk_core2.CredentialError) throw cause;
156
- throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Issued SD-JWT is invalid", { cause });
216
+ if (cause instanceof import_sdk_core4.CredentialError) throw cause;
217
+ throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Issued SD-JWT is invalid", { cause });
157
218
  }
158
219
  }
159
220
  var SERVER_CODES = /* @__PURE__ */ new Set(["REQUEST_EXPIRED", "ISSUER_UNREADY", "UNSUPPORTED_PROFILE", "UNSUPPORTED_VERSION", "SIGNATURE_REJECTED"]);
@@ -162,12 +223,13 @@ function serverError(value, response, honorServerRecoverable) {
162
223
  const body = value;
163
224
  const keys = Object.keys(body).sort();
164
225
  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;
165
- 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 });
166
- 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 });
167
- 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 });
226
+ 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 });
227
+ 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 });
228
+ 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 });
229
+ if (body.code === "DESCRIPTOR_MISMATCH") body.code = "UNSUPPORTED_PROFILE";
168
230
  if (!SERVER_CODES.has(body.code)) return void 0;
169
231
  const code = body.code;
170
- return new import_sdk_core2.CredentialError(code, `OpenCredentials rejected the request: ${code}`, {
232
+ return new import_sdk_core4.CredentialError(code, `OpenCredentials rejected the request: ${code}`, {
171
233
  state: body.state,
172
234
  correlationId: body.correlationId,
173
235
  retryAfterMs: Number(response.headers.get("retry-after")) * 1e3 || void 0,
@@ -175,15 +237,20 @@ function serverError(value, response, honorServerRecoverable) {
175
237
  });
176
238
  }
177
239
  var OpenCredentialsHttpTransport = class {
240
+ /**
241
+ * `allowAccountSubject` is passed only by the email-proof functions, after the per-call held opt-in was accepted. Without it an account-bound
242
+ * (held) descriptor is refused here, before any request (TC-2254 A2.5).
243
+ */
178
244
  constructor(descriptor, fetchFn = globalThis.fetch.bind(globalThis), options = {}) {
179
245
  this.descriptor = descriptor;
180
246
  this.fetchFn = fetchFn;
181
247
  this.options = options;
182
248
  this.nonces = /* @__PURE__ */ new Map();
183
249
  this.bindings = /* @__PURE__ */ new Map();
250
+ 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");
184
251
  }
185
252
  url(endpoint, id) {
186
- return new URL((0, import_sdk_core2.credentialEndpointPath)(endpoint, id), this.descriptor.issuer.origin).href;
253
+ return new URL((0, import_sdk_core4.credentialEndpointPath)(endpoint, id), this.descriptor.issuer.origin).href;
187
254
  }
188
255
  async request(endpoint, input) {
189
256
  let response;
@@ -191,8 +258,8 @@ var OpenCredentialsHttpTransport = class {
191
258
  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) } };
192
259
  response = await this.fetchFn(this.url(endpoint, input.id), init);
193
260
  } catch (cause) {
194
- if (cause instanceof DOMException && cause.name === "AbortError") throw new import_sdk_core2.CredentialError("CANCELED", "Credential acquisition was canceled", { cause });
195
- throw new import_sdk_core2.CredentialError("OFFLINE", "OpenCredentials is unavailable", { cause });
261
+ if (cause instanceof DOMException && cause.name === "AbortError") throw new import_sdk_core4.CredentialError("CANCELED", "Credential acquisition was canceled", { cause });
262
+ throw new import_sdk_core4.CredentialError("OFFLINE", "OpenCredentials is unavailable", { cause });
196
263
  }
197
264
  if (!response.ok) {
198
265
  let parsed;
@@ -203,95 +270,112 @@ var OpenCredentialsHttpTransport = class {
203
270
  }
204
271
  const typed = serverError(parsed, response, this.options.honorServerRecoverable === true);
205
272
  if (typed) throw typed;
206
- if (response.status === 410) throw new import_sdk_core2.CredentialError("REQUEST_EXPIRED", "Credential request expired");
207
- if (response.status === 409) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential request was replayed or substituted");
208
- 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 });
209
- throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "OpenCredentials rejected the request");
273
+ if (response.status === 410) throw new import_sdk_core4.CredentialError("REQUEST_EXPIRED", "Credential request expired");
274
+ if (response.status === 409) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential request was replayed or substituted");
275
+ 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 });
276
+ throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "OpenCredentials rejected the request");
210
277
  }
211
278
  if (response.status === 204) return void 0;
212
279
  try {
213
280
  return await response.json();
214
281
  } catch (cause) {
215
- throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "OpenCredentials response is invalid", { cause });
282
+ throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "OpenCredentials response is invalid", { cause });
216
283
  }
217
284
  }
218
285
  async create(input) {
219
- 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");
220
- 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");
286
+ 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");
287
+ 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");
221
288
  const requestId = opaque(body.requestId, "requestId");
222
289
  return { requestId, locator: requestId, expiresAt: timestamp(body.expiresAt, "expiresAt"), correlationId: requestId };
223
290
  }
291
+ /** The nonce stored from R3 or R2 for this request. Held acquisition compares the signed binding with it. */
292
+ challengeNonce(id) {
293
+ return this.nonces.get(id);
294
+ }
295
+ /** The binding fetched and validated for this request, as the account signed it. */
296
+ signedBinding(id) {
297
+ return this.bindings.get(id);
298
+ }
224
299
  proofStep() {
225
300
  return this.descriptor.steps.some((step) => step.type === "mailbox_otp") ? "mailbox_otp" : "collect_input";
226
301
  }
227
302
  async state(id, verifier, signal) {
228
303
  const raw = object(await this.request("state", { id, verifier, signal }), "state response");
229
- 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");
304
+ 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");
230
305
  const base = { type: "OpenCredentialsAcquisitionState", version: 1, requestId: id, transitionId: `${String(raw.state)}:${String(raw.resultPreviouslyRead ?? false)}`, correlationId: id };
231
306
  if (raw.state === "challenge_required") {
232
307
  const type = this.proofStep();
233
308
  return { ...base, state: "pending", nextStep: { id: type, type, version: 1, constraints: { challengeRequired: true } } };
234
309
  }
310
+ 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")) {
311
+ const nonce = raw.challengeNonce;
312
+ const prior = this.nonces.get(id);
313
+ 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");
314
+ this.nonces.set(id, opaque(nonce, "challengeNonce"));
315
+ }
235
316
  if (raw.state === "proof_required") {
236
317
  const type = this.proofStep();
237
- this.nonces.set(id, opaque(raw.challengeNonce, "challengeNonce"));
318
+ if (this.descriptor.subjectRelationship !== "account_is_subject") this.nonces.set(id, opaque(raw.challengeNonce, "challengeNonce"));
238
319
  return { ...base, state: "pending", nextStep: { id: type, type, version: 1, constraints: {} } };
239
320
  }
240
321
  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: {} } };
241
322
  if (raw.state === "ready_to_issue") return { ...base, state: "ready_to_issue" };
242
323
  if (raw.state === "issued") return { ...base, state: "complete" };
243
- throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential state is unsupported");
324
+ throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential state is unsupported");
244
325
  }
245
326
  async beginStep(id, verifier, stepId, signal) {
246
327
  const step = this.proofStep();
247
- if (stepId !== step) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge step is substituted");
328
+ if (stepId !== step) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge step is substituted");
248
329
  const challenge = object(await this.request("challenge", { id, verifier, method: "POST", body: { step, stepVersion: 1 }, signal }), "challenge response");
249
- 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");
330
+ 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");
250
331
  this.nonces.set(id, opaque(challenge.challengeNonce, "challengeNonce"));
251
332
  }
252
333
  async submitStep(id, verifier, stepId, proof, signal) {
253
334
  const step = this.proofStep();
254
- if (stepId !== step) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential proof step is substituted");
335
+ if (stepId !== step) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential proof step is substituted");
255
336
  const nonce = this.nonces.get(id);
256
- if (!nonce) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge is missing");
337
+ if (!nonce) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Credential challenge is missing");
257
338
  await this.request("proof", { id, verifier, method: "POST", body: { step, stepVersion: 1, challengeNonce: nonce, proof }, signal });
258
339
  }
259
340
  async holderBinding(id, verifier, signal) {
260
341
  const raw = object(await this.request("holder_binding", { id, verifier, signal }), "holder binding response");
261
- 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");
262
- const binding = (0, import_sdk_core2.validateCredentialHolderBinding)(raw.binding);
263
- 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");
342
+ 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");
343
+ const binding = (0, import_sdk_core4.validateCredentialHolderBinding)(raw.binding);
344
+ if (binding.type !== raw.signingDomain) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Holder binding type does not match its signing domain");
345
+ 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");
264
346
  this.bindings.set(id, binding);
265
347
  return binding;
266
348
  }
267
349
  async submitHolderSignature(id, verifier, signature, signal, key) {
268
350
  const binding = this.bindings.get(id);
269
- if (!binding) throw new import_sdk_core2.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was not retrieved");
351
+ if (!binding) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "Holder binding was not retrieved");
352
+ if (this.descriptor.holderBinding.alg === "EIP191" && !key) throw new import_sdk_core4.CredentialError("REQUEST_SUBSTITUTED", "An account holder must name its key and algorithm");
353
+ const alg = key?.alg ?? "EdDSA";
270
354
  const kid = key?.kid ?? `${binding.holderDid}#${binding.holderDid.replace("did:key:", "")}`;
271
- await this.request("holder_signature", { id, verifier, method: "POST", body: { alg: key?.alg ?? "EdDSA", kid, signature }, signal });
355
+ await this.request("holder_signature", { id, verifier, method: "POST", body: { alg, kid, signature }, signal });
272
356
  }
273
357
  async issue(id, verifier, signal) {
274
358
  await this.request("issue", { id, verifier, method: "POST", signal });
275
359
  }
276
360
  async result(id, verifier, signal) {
277
361
  const raw = object(await this.request("result", { id, verifier, signal }), "result response");
278
- 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");
362
+ 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");
279
363
  const holder = object(raw.holderBinding, "result holder binding");
280
- 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");
364
+ if (raw.subject !== holder.did || holder.signingDomain !== this.descriptor.holderBinding.domain) throw new import_sdk_core4.CredentialError("HOLDER_MISMATCH", "Issued holder binding is invalid");
281
365
  const claimsRaw = object(raw.claims, "result claims");
282
366
  const claims = {};
283
367
  for (const [name, value] of Object.entries(claimsRaw)) {
284
- if (typeof value !== "string") throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Issued claims are invalid");
368
+ if (typeof value !== "string") throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Issued claims are invalid");
285
369
  claims[name] = value;
286
370
  }
287
371
  const validity = object(raw.validity, "result validity");
288
372
  const payload = jwtPayload(raw.credential);
289
- if (typeof payload.jti !== "string") throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Credential id is invalid");
373
+ if (typeof payload.jti !== "string") throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Credential id is invalid");
290
374
  const issuedAt = timestamp(validity.issuedAt, "issuedAt");
291
375
  const expiresAt = timestamp(validity.expiresAt, "expiresAt");
292
376
  const status = object(raw.status, "result status");
293
- if (status.type !== "none" || status.freshnessSeconds !== this.descriptor.status.freshnessSeconds) throw new import_sdk_core2.CredentialError("VERIFICATION_FAILED", "Credential status is invalid");
294
- 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 };
377
+ if (status.type !== "none" || status.freshnessSeconds !== this.descriptor.status.freshnessSeconds) throw new import_sdk_core4.CredentialError("VERIFICATION_FAILED", "Credential status is invalid");
378
+ 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 };
295
379
  }
296
380
  async issuerMetadata(signal) {
297
381
  return await this.request("issuer_metadata", { signal });