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