@tinycloud/node-sdk 3.8.1-beta.2 → 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 +46 -8
- 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 +55 -16
- 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 +303 -114
- 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 +291 -104
- package/dist/credentials.js.map +1 -1
- package/dist/{email-proof-GXXQhIff.d.cts → email-proof-BaOqVlB-.d.cts} +25 -7
- package/dist/{email-proof-GXXQhIff.d.ts → email-proof-BaOqVlB-.d.ts} +25 -7
- package/dist/share-receive.cjs +289 -112
- 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 +273 -95
- 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`;
|
|
@@ -335,9 +416,17 @@ var EMAIL_PROOF_ISSUER_KEYS = [
|
|
|
335
416
|
];
|
|
336
417
|
var DEFAULT_EMAIL_PROOF_PROFILE = "tinycloud.email-proof/v1";
|
|
337
418
|
var PROFILE_PINS = {
|
|
338
|
-
"tinycloud.email-proof/v1": { descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest, requiredStep: "mailbox_otp", enabled: true }
|
|
419
|
+
"tinycloud.email-proof/v1": { descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest, requiredStep: "mailbox_otp", enabled: true },
|
|
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 }
|
|
339
423
|
};
|
|
340
|
-
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;
|
|
341
430
|
var STEP_TIMEOUT_MS = 6e4;
|
|
342
431
|
var HOLDER_DID_PREFIX = "did:key:";
|
|
343
432
|
var HOLDER_DID_PATTERN = /^did:key:z6Mk[^#]+$/;
|
|
@@ -357,6 +446,7 @@ var RECOVERABILITY = {
|
|
|
357
446
|
CREDENTIAL_INVALID: false,
|
|
358
447
|
SESSION_EXPIRED: false,
|
|
359
448
|
SESSION_INVALID: false,
|
|
449
|
+
CLIENT_CLOCK_SKEW: false,
|
|
360
450
|
CANCELED: false,
|
|
361
451
|
FAILED: false
|
|
362
452
|
};
|
|
@@ -373,6 +463,7 @@ var MESSAGES = {
|
|
|
373
463
|
CREDENTIAL_INVALID: "Issued credential failed verification.",
|
|
374
464
|
SESSION_EXPIRED: "Email verification request expired. Request a new code.",
|
|
375
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.",
|
|
376
467
|
CANCELED: "Email verification was canceled.",
|
|
377
468
|
FAILED: "Email verification failed."
|
|
378
469
|
};
|
|
@@ -411,7 +502,8 @@ function restoreEmailProofPending(fields) {
|
|
|
411
502
|
requirementDigest: fields.requirementDigest,
|
|
412
503
|
openerOrigin: fields.openerOrigin,
|
|
413
504
|
createdAt: fields.createdAt,
|
|
414
|
-
expiresAt: fields.expiresAt
|
|
505
|
+
expiresAt: fields.expiresAt,
|
|
506
|
+
...fields.serverExpiresAt === void 0 ? {} : { serverExpiresAt: fields.serverExpiresAt }
|
|
415
507
|
};
|
|
416
508
|
const pending = { ...shown };
|
|
417
509
|
hide(pending, "verifier", fields.verifier);
|
|
@@ -451,6 +543,9 @@ function createMemoryEmailProofPendingStore() {
|
|
|
451
543
|
}
|
|
452
544
|
};
|
|
453
545
|
}
|
|
546
|
+
function isTicketEmailProofResult(result) {
|
|
547
|
+
return result.envelope.profile.id !== HELD_PROFILE;
|
|
548
|
+
}
|
|
454
549
|
var CodeRequested = class extends Error {
|
|
455
550
|
};
|
|
456
551
|
var pauseForCode = async () => {
|
|
@@ -458,17 +553,19 @@ var pauseForCode = async () => {
|
|
|
458
553
|
};
|
|
459
554
|
async function requestEmailProofCode(input) {
|
|
460
555
|
const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;
|
|
461
|
-
profilePin(profile, "request");
|
|
462
|
-
const
|
|
463
|
-
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);
|
|
464
561
|
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
465
562
|
const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);
|
|
466
563
|
const requirement = emailRequirement(address, profile);
|
|
467
|
-
const requirementDigest = await (0,
|
|
468
|
-
const descriptor = await witnessDescriptor(profile, requirement, { fetch: input.fetch, signal }, "request");
|
|
469
|
-
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);
|
|
470
567
|
const verifier = randomVerifier();
|
|
471
|
-
const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });
|
|
568
|
+
const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true, ...held ? { allowAccountSubject: true } : {} });
|
|
472
569
|
const createdAtMs = now().getTime();
|
|
473
570
|
let created;
|
|
474
571
|
try {
|
|
@@ -479,7 +576,7 @@ async function requestEmailProofCode(input) {
|
|
|
479
576
|
requirementDigest,
|
|
480
577
|
holderDid: input.holder.did,
|
|
481
578
|
openerOrigin: OPENER_ORIGIN,
|
|
482
|
-
completionVerifierChallenge: await (0,
|
|
579
|
+
completionVerifierChallenge: await (0, import_sdk_core5.sha256Base64Url)(verifier),
|
|
483
580
|
signal
|
|
484
581
|
});
|
|
485
582
|
} catch (error) {
|
|
@@ -487,6 +584,8 @@ async function requestEmailProofCode(input) {
|
|
|
487
584
|
}
|
|
488
585
|
const serverExpiry = Date.parse(created.expiresAt);
|
|
489
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");
|
|
490
589
|
const pending = restoreEmailProofPending({
|
|
491
590
|
type: "TinyCloudEmailProofPending",
|
|
492
591
|
version: 1,
|
|
@@ -499,11 +598,14 @@ async function requestEmailProofCode(input) {
|
|
|
499
598
|
requirementDigest,
|
|
500
599
|
openerOrigin: OPENER_ORIGIN,
|
|
501
600
|
createdAt: new Date(createdAtMs).toISOString(),
|
|
502
|
-
|
|
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 } : {}
|
|
503
604
|
});
|
|
504
605
|
try {
|
|
505
606
|
await interpretCredentialFlow({
|
|
506
607
|
descriptor,
|
|
608
|
+
...held ? { allowAccountSubject: true } : {},
|
|
507
609
|
requirement,
|
|
508
610
|
requestId: created.requestId,
|
|
509
611
|
verifier,
|
|
@@ -511,8 +613,9 @@ async function requestEmailProofCode(input) {
|
|
|
511
613
|
descriptorDigest,
|
|
512
614
|
requirementDigest,
|
|
513
615
|
openerOrigin: OPENER_ORIGIN,
|
|
616
|
+
...held ? { expiresAt: created.expiresAt } : {},
|
|
514
617
|
transport,
|
|
515
|
-
signing: holderSigning(input.holder),
|
|
618
|
+
signing: holderSigning(input.holder, held),
|
|
516
619
|
handlers: { mailbox_otp: pauseForCode },
|
|
517
620
|
signal
|
|
518
621
|
});
|
|
@@ -529,22 +632,26 @@ async function completeWithIssuerKeys(input, issuerKeys) {
|
|
|
529
632
|
const { pending } = input;
|
|
530
633
|
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
531
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");
|
|
532
638
|
if (pendingDeadlineMs(pending) <= now().getTime()) throw new EmailProofError("session", "SESSION_EXPIRED");
|
|
533
639
|
if (!/^\d{8}$/.test(input.code)) throw new EmailProofError("code", "CODE_INVALID");
|
|
534
|
-
assertHolder(input.holder, "code");
|
|
640
|
+
assertHolder(input.holder, "code", pending.profile);
|
|
535
641
|
if (input.holder.did !== pending.holderDid) throw new EmailProofError("session", "SESSION_INVALID");
|
|
536
642
|
const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);
|
|
537
643
|
const requirement = emailRequirement(pending.address, pending.profile);
|
|
538
|
-
if (await (0,
|
|
539
|
-
const descriptor = await witnessDescriptor(pending.profile, requirement, { fetch: input.fetch, signal }, "session");
|
|
540
|
-
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);
|
|
541
647
|
if (descriptorDigest !== pending.descriptorDigest) throw new EmailProofError("session", "SESSION_INVALID");
|
|
542
|
-
const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });
|
|
648
|
+
const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true, ...held ? { allowAccountSubject: true } : {} });
|
|
543
649
|
try {
|
|
544
650
|
const state = await transport.state(pending.requestId, pending.verifier, signal);
|
|
545
651
|
if (state.nextStep?.constraints.challengeRequired === true) throw new EmailProofError("session", "SESSION_INVALID");
|
|
546
652
|
await interpretCredentialFlow({
|
|
547
653
|
descriptor,
|
|
654
|
+
...held ? { allowAccountSubject: true } : {},
|
|
548
655
|
requirement,
|
|
549
656
|
requestId: pending.requestId,
|
|
550
657
|
verifier: pending.verifier,
|
|
@@ -552,8 +659,9 @@ async function completeWithIssuerKeys(input, issuerKeys) {
|
|
|
552
659
|
descriptorDigest,
|
|
553
660
|
requirementDigest: pending.requirementDigest,
|
|
554
661
|
openerOrigin: pending.openerOrigin,
|
|
662
|
+
...held ? { expiresAt: pending.serverExpiresAt } : {},
|
|
555
663
|
transport,
|
|
556
|
-
signing: holderSigning(input.holder),
|
|
664
|
+
signing: holderSigning(input.holder, held),
|
|
557
665
|
handlers: { mailbox_otp: async () => ({ otp: input.code }) },
|
|
558
666
|
signal
|
|
559
667
|
});
|
|
@@ -562,7 +670,11 @@ async function completeWithIssuerKeys(input, issuerKeys) {
|
|
|
562
670
|
}
|
|
563
671
|
try {
|
|
564
672
|
const envelope = await transport.result(pending.requestId, pending.verifier, signal);
|
|
565
|
-
|
|
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)({
|
|
566
678
|
envelope,
|
|
567
679
|
descriptor,
|
|
568
680
|
descriptorDigest,
|
|
@@ -582,10 +694,17 @@ async function acquireEmailProof(input) {
|
|
|
582
694
|
return acquireWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);
|
|
583
695
|
}
|
|
584
696
|
async function acquireWithIssuerKeys(input, issuerKeys) {
|
|
585
|
-
const
|
|
697
|
+
const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;
|
|
698
|
+
profilePin(profile, "request", input.allowHeldProfile);
|
|
699
|
+
const address = normalizeAddress(input.address, profile);
|
|
586
700
|
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
587
701
|
const store = input.pendingStore;
|
|
588
|
-
|
|
702
|
+
const live = store ? await store.findLive(address, now()) : void 0;
|
|
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
|
+
}
|
|
589
708
|
if (!pending) {
|
|
590
709
|
pending = await requestEmailProofCode({ ...input, address });
|
|
591
710
|
await store?.put(pending);
|
|
@@ -609,30 +728,52 @@ async function acquireWithIssuerKeys(input, issuerKeys) {
|
|
|
609
728
|
throw error;
|
|
610
729
|
}
|
|
611
730
|
}
|
|
612
|
-
function normalizeAddress(address) {
|
|
731
|
+
function normalizeAddress(address, profile) {
|
|
732
|
+
if (profile === HELD_PROFILE) return normalizeHeldAddress(address);
|
|
613
733
|
const value = address.trim();
|
|
614
734
|
if (value.length > 254 || !EMAIL_PATTERN.test(value)) throw new EmailProofError("request", "ADDRESS_INVALID");
|
|
615
735
|
return value;
|
|
616
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
|
+
}
|
|
617
751
|
function holderKid(did) {
|
|
618
752
|
return `${did}#${did.slice(HOLDER_DID_PREFIX.length)}`;
|
|
619
753
|
}
|
|
620
|
-
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);
|
|
621
761
|
if (!HOLDER_DID_PATTERN.test(holder.did) || holder.alg !== "EdDSA" || holder.kid !== holderKid(holder.did)) throw new EmailProofError(step, "HOLDER_INVALID");
|
|
622
762
|
}
|
|
623
|
-
function profilePin(profile, step) {
|
|
763
|
+
function profilePin(profile, step, allowHeld) {
|
|
624
764
|
const entry = Object.prototype.hasOwnProperty.call(PROFILE_PINS, profile) ? PROFILE_PINS[profile] : void 0;
|
|
625
|
-
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");
|
|
626
766
|
return entry;
|
|
627
767
|
}
|
|
628
768
|
function emailRequirement(address, profile) {
|
|
629
|
-
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 });
|
|
630
771
|
}
|
|
631
772
|
function emailProofRequirement(address) {
|
|
632
|
-
return emailRequirement(normalizeAddress(address), DEFAULT_EMAIL_PROOF_PROFILE);
|
|
773
|
+
return emailRequirement(normalizeAddress(address, DEFAULT_EMAIL_PROOF_PROFILE), DEFAULT_EMAIL_PROOF_PROFILE);
|
|
633
774
|
}
|
|
634
|
-
async function witnessDescriptor(profile, requirement, options, step) {
|
|
635
|
-
const pin = profilePin(profile, step);
|
|
775
|
+
async function witnessDescriptor(profile, requirement, options, step, allowHeld) {
|
|
776
|
+
const pin = profilePin(profile, step, allowHeld);
|
|
636
777
|
let descriptor;
|
|
637
778
|
try {
|
|
638
779
|
descriptor = await discoverCatalogDescriptor(requirement, DISCOVERY_URL, { fetch: options.fetch, signal: options.signal });
|
|
@@ -642,7 +783,7 @@ async function witnessDescriptor(profile, requirement, options, step) {
|
|
|
642
783
|
if (descriptor.issuer.origin !== EMAIL_PROOF_PINS.witnessOrigin || descriptor.issuer.did !== EMAIL_PROOF_PINS.issuerDid || !descriptor.steps.some((candidate) => candidate.type === pin.requiredStep)) {
|
|
643
784
|
throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
|
|
644
785
|
}
|
|
645
|
-
if (await (0,
|
|
786
|
+
if (await (0, import_sdk_core5.canonicalDigest)(descriptor) !== pin.descriptorDigest) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
|
|
646
787
|
return descriptor;
|
|
647
788
|
}
|
|
648
789
|
async function discoverCatalogDescriptor(requirement, discoveryUrl, options = {}) {
|
|
@@ -650,30 +791,30 @@ async function discoverCatalogDescriptor(requirement, discoveryUrl, options = {}
|
|
|
650
791
|
try {
|
|
651
792
|
url = new URL(discoveryUrl);
|
|
652
793
|
} catch {
|
|
653
|
-
throw new
|
|
794
|
+
throw new import_sdk_core5.CredentialError("UNSUPPORTED_PROFILE", "Discovery URL is invalid");
|
|
654
795
|
}
|
|
655
|
-
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");
|
|
656
797
|
const init = { credentials: "omit", cache: "no-store", redirect: "error", referrerPolicy: "no-referrer", signal: options.signal };
|
|
657
798
|
let response;
|
|
658
799
|
try {
|
|
659
800
|
response = await (options.fetch ?? globalThis.fetch.bind(globalThis))(url, init);
|
|
660
801
|
} catch (cause) {
|
|
661
|
-
throw new
|
|
802
|
+
throw new import_sdk_core5.CredentialError("OFFLINE", "Credential discovery is unavailable", { cause });
|
|
662
803
|
}
|
|
663
|
-
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");
|
|
664
805
|
const catalog = await response.json();
|
|
665
|
-
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");
|
|
666
807
|
for (const entryValue of catalog.profiles) {
|
|
667
808
|
if (typeof entryValue !== "object" || entryValue === null || Array.isArray(entryValue)) continue;
|
|
668
809
|
const entry = entryValue;
|
|
669
810
|
if (entry.supported !== true || entry.enabled !== true || entry.readiness !== "ready" && entry.readiness !== "degraded") continue;
|
|
670
811
|
try {
|
|
671
|
-
const candidate = (0,
|
|
672
|
-
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;
|
|
673
814
|
} catch {
|
|
674
815
|
}
|
|
675
816
|
}
|
|
676
|
-
throw new
|
|
817
|
+
throw new import_sdk_core5.CredentialError("UNSUPPORTED_PROFILE", "No available credential profile satisfies the requirement");
|
|
677
818
|
}
|
|
678
819
|
function discoveryFailure(error, signal, step) {
|
|
679
820
|
if (signal.aborted && signal.reason?.name !== "TimeoutError") return new EmailProofError(step, "CANCELED");
|
|
@@ -688,8 +829,8 @@ async function verifyStoredEmailProof(input) {
|
|
|
688
829
|
async function verifyStoredWithIssuerKeys(input, issuerKeys) {
|
|
689
830
|
const { record, requirement, holderDid } = input;
|
|
690
831
|
const descriptor = record.descriptor;
|
|
691
|
-
if (record.descriptorDigest !== EMAIL_PROOF_PINS.descriptorDigest || await (0,
|
|
692
|
-
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");
|
|
693
834
|
}
|
|
694
835
|
const envelope = {
|
|
695
836
|
type: "OpenCredentialsIssuedCredential",
|
|
@@ -713,7 +854,7 @@ async function verifyStoredWithIssuerKeys(input, issuerKeys) {
|
|
|
713
854
|
status: record.status,
|
|
714
855
|
credential: record.credential
|
|
715
856
|
};
|
|
716
|
-
const verified = await (0,
|
|
857
|
+
const verified = await (0, import_sdk_core5.verifyIssuedCredential)({
|
|
717
858
|
envelope,
|
|
718
859
|
descriptor,
|
|
719
860
|
descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest,
|
|
@@ -724,17 +865,61 @@ async function verifyStoredWithIssuerKeys(input, issuerKeys) {
|
|
|
724
865
|
checkStatus: async (status) => status.method === "none" && status.freshnessSeconds === descriptor.status.freshnessSeconds,
|
|
725
866
|
signal: input.signal
|
|
726
867
|
});
|
|
727
|
-
if (record.holderDid !== holderDid || record.ownerDid !== holderDid) throw new
|
|
728
|
-
if (record.credentialDigest !== verified.credentialDigest) throw new
|
|
729
|
-
if (record.requirementDigest !== await (0,
|
|
730
|
-
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");
|
|
731
905
|
return verified;
|
|
732
906
|
}
|
|
733
|
-
function holderSigning(holder) {
|
|
734
|
-
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
|
+
};
|
|
735
920
|
}
|
|
736
921
|
function randomVerifier() {
|
|
737
|
-
return (0,
|
|
922
|
+
return (0, import_sdk_core5.encodeBase64Url)(crypto.getRandomValues(new Uint8Array(32)));
|
|
738
923
|
}
|
|
739
924
|
function isPending(value) {
|
|
740
925
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -744,7 +929,7 @@ function isPending(value) {
|
|
|
744
929
|
function mapError(value, step, signal) {
|
|
745
930
|
if (value instanceof EmailProofError) return value;
|
|
746
931
|
if (value instanceof DOMException && value.name === "TimeoutError") return new EmailProofError(step, "NETWORK");
|
|
747
|
-
const error = (0,
|
|
932
|
+
const error = (0, import_sdk_core5.credentialError)(value);
|
|
748
933
|
const details = error.details;
|
|
749
934
|
switch (error.code) {
|
|
750
935
|
case "ISSUER_UNREADY":
|
|
@@ -753,6 +938,7 @@ function mapError(value, step, signal) {
|
|
|
753
938
|
if (signal?.aborted && signal.reason?.name !== "TimeoutError") return new EmailProofError(step, "CANCELED");
|
|
754
939
|
return new EmailProofError(step, "NETWORK");
|
|
755
940
|
case "UNSUPPORTED_PROFILE":
|
|
941
|
+
case "UNSUPPORTED_VERSION":
|
|
756
942
|
case "DESCRIPTOR_INVALID":
|
|
757
943
|
return new EmailProofError(step, "PROFILE_UNAVAILABLE");
|
|
758
944
|
case "PROOF_REJECTED":
|
|
@@ -781,7 +967,9 @@ function mapError(value, step, signal) {
|
|
|
781
967
|
DEFAULT_EMAIL_PROOF_PROFILE,
|
|
782
968
|
EMAIL_PROOF_CREDENTIAL_TYPE,
|
|
783
969
|
EMAIL_PROOF_PENDING_MAX_MS,
|
|
970
|
+
EMAIL_PROOF_PINS,
|
|
784
971
|
EmailProofError,
|
|
972
|
+
HELD_ACCOUNT_BINDING_PINS,
|
|
785
973
|
OpenCredentialsHttpTransport,
|
|
786
974
|
acquireEmailProof,
|
|
787
975
|
completeEmailProof,
|
|
@@ -789,6 +977,7 @@ function mapError(value, step, signal) {
|
|
|
789
977
|
createMemoryEmailProofPendingStore,
|
|
790
978
|
emailProofRequirement,
|
|
791
979
|
interpretCredentialFlow,
|
|
980
|
+
isTicketEmailProofResult,
|
|
792
981
|
mailboxSubject,
|
|
793
982
|
requestEmailProofCode,
|
|
794
983
|
verifyStoredEmailProof
|