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