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