@tinycloud/node-sdk 3.9.0-beta.3 → 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.
- package/dist/credential-pending-store.cjs +45 -9
- package/dist/credential-pending-store.cjs.map +1 -1
- package/dist/credential-pending-store.d.cts +2 -1
- package/dist/credential-pending-store.d.ts +2 -1
- package/dist/credential-pending-store.js +54 -17
- package/dist/credential-pending-store.js.map +1 -1
- package/dist/credentials-core.cjs +140 -56
- package/dist/credentials-core.cjs.map +1 -1
- package/dist/credentials-core.d.cts +21 -1
- package/dist/credentials-core.d.ts +21 -1
- package/dist/credentials-core.js +138 -54
- package/dist/credentials-core.js.map +1 -1
- package/dist/credentials.cjs +300 -115
- package/dist/credentials.cjs.map +1 -1
- package/dist/credentials.d.cts +33 -1
- package/dist/credentials.d.ts +33 -1
- package/dist/credentials.js +288 -105
- package/dist/credentials.js.map +1 -1
- package/dist/{email-proof-D0r_WShV.d.cts → email-proof-BaOqVlB-.d.cts} +20 -3
- package/dist/{email-proof-D0r_WShV.d.ts → email-proof-BaOqVlB-.d.ts} +20 -3
- package/dist/share-receive.cjs +288 -113
- package/dist/share-receive.cjs.map +1 -1
- package/dist/share-receive.d.cts +2 -1
- package/dist/share-receive.d.ts +2 -1
- package/dist/share-receive.js +272 -96
- package/dist/share-receive.js.map +1 -1
- package/package.json +2 -2
|
@@ -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
|
|
50
|
-
if (state.state === "expired") throw new
|
|
51
|
-
if (state.state === "canceled") throw new
|
|
52
|
-
if (state.state === "issuer_unready") throw new
|
|
53
|
-
if (state.state === "complete")
|
|
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
|
|
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 =
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
|
85
|
-
throw new
|
|
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
|
|
89
|
-
await input.transport.submitHolderSignature(input.requestId, input.verifier, (0,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
156
|
-
throw new
|
|
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
|
|
166
|
-
if (body.code === "RATE_LIMITED") return new
|
|
167
|
-
if (body.code === "CHALLENGE_EXPIRED") return new
|
|
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
|
|
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,
|
|
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
|
|
195
|
-
throw new
|
|
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
|
|
207
|
-
if (response.status === 409) throw new
|
|
208
|
-
if (response.status === 503) throw new
|
|
209
|
-
throw new
|
|
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
|
|
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:
|
|
220
|
-
if (body.type !== "tinycloud.credentials/acquisition-request/v1" || body.protocol !== input.descriptor.protocol || body.next !== "challenge" || body.endpoint !== "challenge") throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 !==
|
|
262
|
-
const binding = (0,
|
|
263
|
-
if (
|
|
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
|
|
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
|
|
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,
|
|
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 !==
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 });
|