@tinycloud/node-sdk 3.6.0-beta.3 → 3.6.0-beta.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/{TinyCloudNode-N4mfZI_k.d.cts → TinyCloudNode-B5WCe_ps.d.ts} +3 -250
- package/dist/{TinyCloudNode-N4mfZI_k.d.ts → TinyCloudNode-CIGurdHw.d.cts} +3 -250
- package/dist/{authority-BgZpUFss.d.cts → authority-Beg1WM2U.d.cts} +1 -1
- package/dist/{authority-C670-9su.d.ts → authority-Vh7ike0I.d.ts} +1 -1
- package/dist/core.d.cts +3 -2
- package/dist/core.d.ts +3 -2
- package/dist/credential-pending-store.cjs +401 -0
- package/dist/credential-pending-store.cjs.map +1 -0
- package/dist/credential-pending-store.d.cts +42 -0
- package/dist/credential-pending-store.d.ts +42 -0
- package/dist/credential-pending-store.js +389 -0
- package/dist/credential-pending-store.js.map +1 -0
- package/dist/credentials-core.cjs +309 -0
- package/dist/credentials-core.cjs.map +1 -0
- package/dist/credentials-core.d.cts +337 -0
- package/dist/credentials-core.d.ts +337 -0
- package/dist/credentials-core.js +294 -0
- package/dist/credentials-core.js.map +1 -0
- package/dist/credentials.cjs +746 -0
- package/dist/credentials.cjs.map +1 -0
- package/dist/credentials.d.cts +4 -0
- package/dist/credentials.d.ts +4 -0
- package/dist/credentials.js +732 -0
- package/dist/credentials.js.map +1 -0
- package/dist/delegation-0LKZkmP2.d.cts +251 -0
- package/dist/delegation-0LKZkmP2.d.ts +251 -0
- package/dist/email-proof-Blhdm4ox.d.cts +155 -0
- package/dist/email-proof-Blhdm4ox.d.ts +155 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/replication/authority.d.cts +3 -2
- package/dist/replication/authority.d.ts +3 -2
- package/package.json +18 -2
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
// src/credentials/interpreter.ts
|
|
2
|
+
import {
|
|
3
|
+
CredentialError,
|
|
4
|
+
encodeBase64Url,
|
|
5
|
+
holderBindingSigningBytes,
|
|
6
|
+
validateCredentialHolderBinding
|
|
7
|
+
} from "@tinycloud/sdk-core";
|
|
8
|
+
var delay = (ms, signal) => new Promise((resolve, reject) => {
|
|
9
|
+
const timer = setTimeout(resolve, ms);
|
|
10
|
+
signal?.addEventListener("abort", () => {
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
13
|
+
}, { once: true });
|
|
14
|
+
});
|
|
15
|
+
function mailboxSubject(requirement) {
|
|
16
|
+
const email = requirement.claims.email;
|
|
17
|
+
return typeof email === "string" && email.length > 0 ? Object.freeze({ kind: "email", value: email }) : void 0;
|
|
18
|
+
}
|
|
19
|
+
async function interpretCredentialFlow(input) {
|
|
20
|
+
const steps = new Map(input.descriptor.steps.map((step) => [step.type, step]));
|
|
21
|
+
const completed = /* @__PURE__ */ new Set();
|
|
22
|
+
let lastTransition = "";
|
|
23
|
+
for (; ; ) {
|
|
24
|
+
input.signal?.throwIfAborted();
|
|
25
|
+
const state = await input.transport.state(input.requestId, input.verifier, input.signal);
|
|
26
|
+
if (state.requestId !== input.requestId) throw new CredentialError("REQUEST_SUBSTITUTED", "Credential request identity changed");
|
|
27
|
+
if (state.state === "expired") throw new CredentialError("REQUEST_EXPIRED", "Credential request expired", { state: state.state, correlationId: state.correlationId });
|
|
28
|
+
if (state.state === "canceled") throw new CredentialError("CANCELED", "Credential request was canceled", { state: state.state, correlationId: state.correlationId });
|
|
29
|
+
if (state.state === "issuer_unready") throw new CredentialError("ISSUER_UNREADY", "Credential issuer is not ready", { retryAfterMs: state.retryAfterMs, state: state.state, correlationId: state.correlationId });
|
|
30
|
+
if (state.state === "complete") return;
|
|
31
|
+
if (state.state === "ready_to_issue" || state.state === "issued") {
|
|
32
|
+
if (state.state === "ready_to_issue" && !completed.has("issue")) {
|
|
33
|
+
await input.transport.issue(input.requestId, input.verifier, input.signal);
|
|
34
|
+
completed.add("issue");
|
|
35
|
+
} else await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const next = state.nextStep;
|
|
39
|
+
if (!next) {
|
|
40
|
+
await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const declared = steps.get(next.type);
|
|
44
|
+
if (next.id !== next.type || !declared || declared.type !== next.type || declared.version !== next.version) throw new CredentialError("REQUEST_SUBSTITUTED", "Server selected an undeclared credential step");
|
|
45
|
+
if (completed.has(next.id) || state.transitionId === lastTransition) {
|
|
46
|
+
await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
lastTransition = state.transitionId;
|
|
50
|
+
if (next.type === "holder_signature") {
|
|
51
|
+
input.onProgress?.({ state: "signing", stepId: next.id, correlationId: state.correlationId });
|
|
52
|
+
const binding = validateCredentialHolderBinding(await input.transport.holderBinding(input.requestId, input.verifier, input.signal));
|
|
53
|
+
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 CredentialError("REQUEST_SUBSTITUTED", "Holder binding was substituted");
|
|
54
|
+
const bytes = holderBindingSigningBytes(binding);
|
|
55
|
+
let signature = await input.signing.autoSign?.(binding, bytes, input.signal);
|
|
56
|
+
if (signature === void 0) {
|
|
57
|
+
if (!input.signing.requestApproval) throw new CredentialError("SIGNATURE_REJECTED", "OpenKey approval is required");
|
|
58
|
+
try {
|
|
59
|
+
signature = await input.signing.requestApproval(binding, bytes, input.signal);
|
|
60
|
+
} catch (cause) {
|
|
61
|
+
if (cause instanceof CredentialError) throw cause;
|
|
62
|
+
throw new CredentialError("SIGNATURE_REJECTED", "OpenKey approval was rejected", { cause });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!(signature instanceof Uint8Array) || signature.length === 0) throw new CredentialError("SIGNATURE_REJECTED", "OpenKey signature is invalid");
|
|
66
|
+
await input.transport.submitHolderSignature(input.requestId, input.verifier, encodeBase64Url(signature), input.signal, input.signing.holderKey);
|
|
67
|
+
} else {
|
|
68
|
+
input.onProgress?.({ state: next.type === "collect_input" ? "collecting" : "proving", stepId: next.id, correlationId: state.correlationId });
|
|
69
|
+
const handler = input.handlers?.[next.type];
|
|
70
|
+
const inlineHandler = input.proofHandler;
|
|
71
|
+
if (!handler && !inlineHandler) {
|
|
72
|
+
await (input.onWait?.() ?? delay(state.retryAfterMs ?? 50, input.signal));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (next.constraints.challengeRequired === true) await input.transport.beginStep(input.requestId, input.verifier, next.type, input.signal);
|
|
76
|
+
if (handler) {
|
|
77
|
+
const proof = await handler({ descriptor: input.descriptor, requirement: input.requirement, stepId: next.id, constraints: next.constraints, signal: input.signal });
|
|
78
|
+
await input.transport.submitStep(input.requestId, input.verifier, next.id, proof, input.signal);
|
|
79
|
+
} else {
|
|
80
|
+
let feedback;
|
|
81
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
82
|
+
const proof = await inlineHandler({
|
|
83
|
+
stepId: next.id,
|
|
84
|
+
constraints: next.constraints,
|
|
85
|
+
display: {
|
|
86
|
+
title: input.descriptor.display.title,
|
|
87
|
+
description: input.descriptor.display.description,
|
|
88
|
+
consent: input.descriptor.display.consent,
|
|
89
|
+
progressLabel: input.descriptor.accessibility.progressLabel,
|
|
90
|
+
errorLiveRegion: input.descriptor.accessibility.errorLiveRegion
|
|
91
|
+
},
|
|
92
|
+
inputs: input.descriptor.inputs.map(({ id, label, schema }) => ({ id, label, schema })),
|
|
93
|
+
...feedback === void 0 ? {} : { feedback },
|
|
94
|
+
signal: input.signal
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
await input.transport.submitStep(input.requestId, input.verifier, next.id, proof, input.signal);
|
|
98
|
+
break;
|
|
99
|
+
} catch (cause) {
|
|
100
|
+
if (!(cause instanceof CredentialError) || cause.code !== "PROOF_REJECTED") throw cause;
|
|
101
|
+
const attemptsRemaining = input.descriptor.lifecycle.maxProofAttempts - attempt;
|
|
102
|
+
if (next.type !== "mailbox_otp" || attemptsRemaining <= 0) throw new CredentialError("VERIFICATION_FAILED", "The proof was not accepted", { state: attemptsRemaining <= 0 ? "proof_attempts_exhausted" : "proof_rejected", cause });
|
|
103
|
+
feedback = { kind: "rejected", attemptsRemaining };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
completed.add(next.id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/credentials/transport.ts
|
|
113
|
+
import {
|
|
114
|
+
CredentialError as CredentialError2,
|
|
115
|
+
canonicalDigest,
|
|
116
|
+
credentialEndpointPath,
|
|
117
|
+
decodeBase64Url,
|
|
118
|
+
encodeBase64Url as encodeBase64Url2,
|
|
119
|
+
holderBindingCanonicalBytes,
|
|
120
|
+
sha256Base64Url,
|
|
121
|
+
validateCredentialHolderBinding as validateCredentialHolderBinding2
|
|
122
|
+
} from "@tinycloud/sdk-core";
|
|
123
|
+
function object(value, label) {
|
|
124
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new CredentialError2("VERIFICATION_FAILED", `${label} is invalid`);
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function opaque(value, label) {
|
|
128
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{16,128}$/.test(value)) throw new CredentialError2("REQUEST_SUBSTITUTED", `${label} is invalid`);
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function timestamp(value, label) {
|
|
132
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) throw new CredentialError2("VERIFICATION_FAILED", `${label} is invalid`);
|
|
133
|
+
return new Date(value).toISOString();
|
|
134
|
+
}
|
|
135
|
+
function jwtPayload(credential) {
|
|
136
|
+
const part = credential.split("~", 1)[0]?.split(".")[1];
|
|
137
|
+
if (!part) throw new CredentialError2("VERIFICATION_FAILED", "Issued SD-JWT is invalid");
|
|
138
|
+
try {
|
|
139
|
+
return object(JSON.parse(new TextDecoder().decode(decodeBase64Url(part))), "SD-JWT payload");
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
if (cause instanceof CredentialError2) throw cause;
|
|
142
|
+
throw new CredentialError2("VERIFICATION_FAILED", "Issued SD-JWT is invalid", { cause });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
var SERVER_CODES = /* @__PURE__ */ new Set(["REQUEST_EXPIRED", "ISSUER_UNREADY", "UNSUPPORTED_PROFILE", "UNSUPPORTED_VERSION", "SIGNATURE_REJECTED"]);
|
|
146
|
+
function serverError(value, response, honorServerRecoverable) {
|
|
147
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
148
|
+
const body = value;
|
|
149
|
+
const keys = Object.keys(body).sort();
|
|
150
|
+
if (keys.join(",") !== "code,correlationId,recoverable,state,type" || body.type !== "tinycloud.credentials/error/v1" || typeof body.code !== "string" || typeof body.recoverable !== "boolean" || typeof body.state !== "string" || typeof body.correlationId !== "string") return void 0;
|
|
151
|
+
if (body.code === "PROOF_REJECTED") return body.recoverable ? new CredentialError2("PROOF_REJECTED", "The proof was not accepted", { state: body.state, correlationId: body.correlationId }) : new CredentialError2("VERIFICATION_FAILED", "The proof attempt limit was reached", { state: "proof_attempts_exhausted", correlationId: body.correlationId });
|
|
152
|
+
if (body.code === "RATE_LIMITED") return new CredentialError2("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 });
|
|
153
|
+
if (body.code === "CHALLENGE_EXPIRED") return new CredentialError2("REQUEST_EXPIRED", "The credential challenge expired", { state: body.state, correlationId: body.correlationId, recoverable: honorServerRecoverable ? body.recoverable : void 0 });
|
|
154
|
+
if (!SERVER_CODES.has(body.code)) return void 0;
|
|
155
|
+
const code = body.code;
|
|
156
|
+
return new CredentialError2(code, `OpenCredentials rejected the request: ${code}`, {
|
|
157
|
+
state: body.state,
|
|
158
|
+
correlationId: body.correlationId,
|
|
159
|
+
retryAfterMs: Number(response.headers.get("retry-after")) * 1e3 || void 0,
|
|
160
|
+
recoverable: honorServerRecoverable ? body.recoverable : void 0
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
var OpenCredentialsHttpTransport = class {
|
|
164
|
+
constructor(descriptor, fetchFn = globalThis.fetch.bind(globalThis), options = {}) {
|
|
165
|
+
this.descriptor = descriptor;
|
|
166
|
+
this.fetchFn = fetchFn;
|
|
167
|
+
this.options = options;
|
|
168
|
+
this.nonces = /* @__PURE__ */ new Map();
|
|
169
|
+
this.bindings = /* @__PURE__ */ new Map();
|
|
170
|
+
}
|
|
171
|
+
url(endpoint, id) {
|
|
172
|
+
return new URL(credentialEndpointPath(endpoint, id), this.descriptor.issuer.origin).href;
|
|
173
|
+
}
|
|
174
|
+
async request(endpoint, input) {
|
|
175
|
+
let response;
|
|
176
|
+
try {
|
|
177
|
+
const init = { method: input.method ?? "GET", credentials: "omit", cache: "no-store", redirect: "error", referrerPolicy: "no-referrer", signal: input.signal, headers: { accept: "application/json", ...input.body === void 0 ? {} : { "content-type": "application/json" }, ...input.verifier === void 0 ? {} : { authorization: `Bearer ${input.verifier}` } }, ...input.body === void 0 ? {} : { body: JSON.stringify(input.body) } };
|
|
178
|
+
response = await this.fetchFn(this.url(endpoint, input.id), init);
|
|
179
|
+
} catch (cause) {
|
|
180
|
+
if (cause instanceof DOMException && cause.name === "AbortError") throw new CredentialError2("CANCELED", "Credential acquisition was canceled", { cause });
|
|
181
|
+
throw new CredentialError2("OFFLINE", "OpenCredentials is unavailable", { cause });
|
|
182
|
+
}
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
let parsed;
|
|
185
|
+
try {
|
|
186
|
+
parsed = await response.json();
|
|
187
|
+
} catch {
|
|
188
|
+
parsed = void 0;
|
|
189
|
+
}
|
|
190
|
+
const typed = serverError(parsed, response, this.options.honorServerRecoverable === true);
|
|
191
|
+
if (typed) throw typed;
|
|
192
|
+
if (response.status === 410) throw new CredentialError2("REQUEST_EXPIRED", "Credential request expired");
|
|
193
|
+
if (response.status === 409) throw new CredentialError2("REQUEST_SUBSTITUTED", "Credential request was replayed or substituted");
|
|
194
|
+
if (response.status === 503) throw new CredentialError2("ISSUER_UNREADY", "Credential issuer is not ready", { retryAfterMs: Number(response.headers.get("retry-after")) * 1e3 || void 0 });
|
|
195
|
+
throw new CredentialError2("VERIFICATION_FAILED", "OpenCredentials rejected the request");
|
|
196
|
+
}
|
|
197
|
+
if (response.status === 204) return void 0;
|
|
198
|
+
try {
|
|
199
|
+
return await response.json();
|
|
200
|
+
} catch (cause) {
|
|
201
|
+
throw new CredentialError2("VERIFICATION_FAILED", "OpenCredentials response is invalid", { cause });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async create(input) {
|
|
205
|
+
const body = object(await this.request("request", { method: "POST", body: { protocol: input.descriptor.protocol, profile: input.descriptor.profile, profileVersion: input.descriptor.profileVersion, descriptorDigest: input.descriptorDigest, requirementDigest: input.requirementDigest, holderDid: input.holderDid, inputs: input.inputs ?? input.requirement.claims, audience: "tinycloud://credentials", openerOrigin: input.openerOrigin, completionOrigin: input.openerOrigin, completionContext: "sdk-acquisition", completionVerifierChallenge: input.completionVerifierChallenge }, signal: input.signal }), "create response");
|
|
206
|
+
if (body.type !== "tinycloud.credentials/acquisition-request/v1" || body.protocol !== input.descriptor.protocol || body.next !== "challenge" || body.endpoint !== "challenge") throw new CredentialError2("VERIFICATION_FAILED", "create response is invalid");
|
|
207
|
+
const requestId = opaque(body.requestId, "requestId");
|
|
208
|
+
return { requestId, locator: requestId, expiresAt: timestamp(body.expiresAt, "expiresAt"), correlationId: requestId };
|
|
209
|
+
}
|
|
210
|
+
proofStep() {
|
|
211
|
+
return this.descriptor.steps.some((step) => step.type === "mailbox_otp") ? "mailbox_otp" : "collect_input";
|
|
212
|
+
}
|
|
213
|
+
async state(id, verifier, signal) {
|
|
214
|
+
const raw = object(await this.request("state", { id, verifier, signal }), "state response");
|
|
215
|
+
if (raw.type !== "tinycloud.credentials/acquisition-state/v1" || raw.requestId !== id || raw.profile !== this.descriptor.profile || raw.profileVersion !== 1) throw new CredentialError2("REQUEST_SUBSTITUTED", "Credential state response is substituted");
|
|
216
|
+
const base = { type: "OpenCredentialsAcquisitionState", version: 1, requestId: id, transitionId: `${String(raw.state)}:${String(raw.resultPreviouslyRead ?? false)}`, correlationId: id };
|
|
217
|
+
if (raw.state === "challenge_required") {
|
|
218
|
+
const type = this.proofStep();
|
|
219
|
+
return { ...base, state: "pending", nextStep: { id: type, type, version: 1, constraints: { challengeRequired: true } } };
|
|
220
|
+
}
|
|
221
|
+
if (raw.state === "proof_required") {
|
|
222
|
+
const type = this.proofStep();
|
|
223
|
+
this.nonces.set(id, opaque(raw.challengeNonce, "challengeNonce"));
|
|
224
|
+
return { ...base, state: "pending", nextStep: { id: type, type, version: 1, constraints: {} } };
|
|
225
|
+
}
|
|
226
|
+
if (raw.state === "holder_binding_required" || raw.state === "holder_signature_required") return { ...base, state: "pending", nextStep: { id: "holder_signature", type: "holder_signature", version: 1, constraints: {} } };
|
|
227
|
+
if (raw.state === "ready_to_issue") return { ...base, state: "ready_to_issue" };
|
|
228
|
+
if (raw.state === "issued") return { ...base, state: "complete" };
|
|
229
|
+
throw new CredentialError2("REQUEST_SUBSTITUTED", "Credential state is unsupported");
|
|
230
|
+
}
|
|
231
|
+
async beginStep(id, verifier, stepId, signal) {
|
|
232
|
+
const step = this.proofStep();
|
|
233
|
+
if (stepId !== step) throw new CredentialError2("REQUEST_SUBSTITUTED", "Credential challenge step is substituted");
|
|
234
|
+
const challenge = object(await this.request("challenge", { id, verifier, method: "POST", body: { step, stepVersion: 1 }, signal }), "challenge response");
|
|
235
|
+
if (challenge.type !== "tinycloud.credentials/challenge/v1" || challenge.step !== step || challenge.stepVersion !== 1) throw new CredentialError2("REQUEST_SUBSTITUTED", "Credential challenge is substituted");
|
|
236
|
+
this.nonces.set(id, opaque(challenge.challengeNonce, "challengeNonce"));
|
|
237
|
+
}
|
|
238
|
+
async submitStep(id, verifier, stepId, proof, signal) {
|
|
239
|
+
const step = this.proofStep();
|
|
240
|
+
if (stepId !== step) throw new CredentialError2("REQUEST_SUBSTITUTED", "Credential proof step is substituted");
|
|
241
|
+
const nonce = this.nonces.get(id);
|
|
242
|
+
if (!nonce) throw new CredentialError2("REQUEST_SUBSTITUTED", "Credential challenge is missing");
|
|
243
|
+
await this.request("proof", { id, verifier, method: "POST", body: { step, stepVersion: 1, challengeNonce: nonce, proof }, signal });
|
|
244
|
+
}
|
|
245
|
+
async holderBinding(id, verifier, signal) {
|
|
246
|
+
const raw = object(await this.request("holder_binding", { id, verifier, signal }), "holder binding response");
|
|
247
|
+
if (raw.type !== "tinycloud.credentials/holder-binding-request/v1" || raw.signingDomain !== "tinycloud.credentials/holder-binding/v1" || raw.alg !== "EdDSA" || typeof raw.canonical !== "string") throw new CredentialError2("REQUEST_SUBSTITUTED", "Holder binding response is invalid");
|
|
248
|
+
const binding = validateCredentialHolderBinding2(raw.binding);
|
|
249
|
+
if (encodeBase64Url2(holderBindingCanonicalBytes(binding)) !== raw.canonical) throw new CredentialError2("REQUEST_SUBSTITUTED", "Holder binding canonical bytes were substituted");
|
|
250
|
+
this.bindings.set(id, binding);
|
|
251
|
+
return binding;
|
|
252
|
+
}
|
|
253
|
+
async submitHolderSignature(id, verifier, signature, signal, key) {
|
|
254
|
+
const binding = this.bindings.get(id);
|
|
255
|
+
if (!binding) throw new CredentialError2("REQUEST_SUBSTITUTED", "Holder binding was not retrieved");
|
|
256
|
+
const kid = key?.kid ?? `${binding.holderDid}#${binding.holderDid.replace("did:key:", "")}`;
|
|
257
|
+
await this.request("holder_signature", { id, verifier, method: "POST", body: { alg: key?.alg ?? "EdDSA", kid, signature }, signal });
|
|
258
|
+
}
|
|
259
|
+
async issue(id, verifier, signal) {
|
|
260
|
+
await this.request("issue", { id, verifier, method: "POST", signal });
|
|
261
|
+
}
|
|
262
|
+
async result(id, verifier, signal) {
|
|
263
|
+
const raw = object(await this.request("result", { id, verifier, signal }), "result response");
|
|
264
|
+
if (raw.type !== "tinycloud.credentials/result/v1" || raw.protocol !== this.descriptor.protocol || raw.profile !== this.descriptor.profile || raw.profileVersion !== 1 || raw.format !== "vc+sd-jwt" || raw.descriptorDigest !== await canonicalDigest(this.descriptor) || raw.issuer !== this.descriptor.issuer.did || raw.issuerKid !== this.descriptor.issuer.kid || typeof raw.credential !== "string" || await sha256Base64Url(raw.credential) !== raw.credentialDigest) throw new CredentialError2("VERIFICATION_FAILED", "Issued result metadata is invalid");
|
|
265
|
+
const holder = object(raw.holderBinding, "result holder binding");
|
|
266
|
+
if (raw.subject !== holder.did || holder.signingDomain !== "tinycloud.credentials/holder-binding/v1") throw new CredentialError2("HOLDER_MISMATCH", "Issued holder binding is invalid");
|
|
267
|
+
const claimsRaw = object(raw.claims, "result claims");
|
|
268
|
+
const claims = {};
|
|
269
|
+
for (const [name, value] of Object.entries(claimsRaw)) {
|
|
270
|
+
if (typeof value !== "string") throw new CredentialError2("VERIFICATION_FAILED", "Issued claims are invalid");
|
|
271
|
+
claims[name] = value;
|
|
272
|
+
}
|
|
273
|
+
const validity = object(raw.validity, "result validity");
|
|
274
|
+
const payload = jwtPayload(raw.credential);
|
|
275
|
+
if (typeof payload.jti !== "string") throw new CredentialError2("VERIFICATION_FAILED", "Credential id is invalid");
|
|
276
|
+
const issuedAt = timestamp(validity.issuedAt, "issuedAt");
|
|
277
|
+
const expiresAt = timestamp(validity.expiresAt, "expiresAt");
|
|
278
|
+
const status = object(raw.status, "result status");
|
|
279
|
+
if (status.type !== "none" || status.freshnessSeconds !== this.descriptor.status.freshnessSeconds) throw new CredentialError2("VERIFICATION_FAILED", "Credential status is invalid");
|
|
280
|
+
return { type: "OpenCredentialsIssuedCredential", version: 1, protocol: this.descriptor.protocol, profile: { id: this.descriptor.profile, version: 1 }, credentialType: { id: this.descriptor.format.vct, version: 1 }, schema: this.descriptor.format.vct, format: "vc+sd-jwt", issuerDid: raw.issuer, issuerKid: raw.issuerKid, subjectDid: raw.subject, holderDid: raw.subject, claims, claimsDigest: await canonicalDigest(claims), descriptorDigest: raw.descriptorDigest, credentialId: payload.jti, issuedAt, notBefore: issuedAt, expiresAt, status: { method: "none", freshnessSeconds: status.freshnessSeconds }, credential: raw.credential };
|
|
281
|
+
}
|
|
282
|
+
async issuerMetadata(signal) {
|
|
283
|
+
return await this.request("issuer_metadata", { signal });
|
|
284
|
+
}
|
|
285
|
+
async checkStatus(status, _signal) {
|
|
286
|
+
return status.method === "none" && status.freshnessSeconds === this.descriptor.status.freshnessSeconds;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/credentials/email-proof.ts
|
|
291
|
+
import {
|
|
292
|
+
canonicalDigest as canonicalDigest2,
|
|
293
|
+
createEmailCredentialRequirement,
|
|
294
|
+
credentialError,
|
|
295
|
+
credentialRequirementDigest,
|
|
296
|
+
CredentialError as CredentialError3,
|
|
297
|
+
descriptorSatisfiesRequirement,
|
|
298
|
+
encodeBase64Url as encodeBase64Url3,
|
|
299
|
+
sha256Base64Url as sha256Base64Url2,
|
|
300
|
+
validateCredentialFlowDescriptor,
|
|
301
|
+
verifyIssuedCredential
|
|
302
|
+
} from "@tinycloud/sdk-core";
|
|
303
|
+
var EMAIL_PROOF_PINS = Object.freeze({
|
|
304
|
+
witnessOrigin: "https://witness.credentials.org",
|
|
305
|
+
issuerDid: "did:web:issuer.credentials.org",
|
|
306
|
+
issuerKid: "did:web:issuer.credentials.org#controller",
|
|
307
|
+
issuerX: "eEJI4xEobto4HtQ7Pg9R1vBOwcpfGRTlXDG5QfqLnGQ",
|
|
308
|
+
descriptorDigest: "1tg-qphmKBVtNwzVg9xyz-xxqt_xtMXAsQyXw46m8S0"
|
|
309
|
+
});
|
|
310
|
+
var EMAIL_PROOF_CREDENTIAL_TYPE = { id: "opencredentials.email/v1", version: 1 };
|
|
311
|
+
var EMAIL_PROOF_PENDING_MAX_MS = 15 * 6e4;
|
|
312
|
+
var DISCOVERY_URL = `${EMAIL_PROOF_PINS.witnessOrigin}/.well-known/opencredentials`;
|
|
313
|
+
var EMAIL_PROOF_ISSUER_KEYS = [
|
|
314
|
+
{
|
|
315
|
+
kid: EMAIL_PROOF_PINS.issuerKid,
|
|
316
|
+
alg: "EdDSA",
|
|
317
|
+
jwk: { kty: "OKP", crv: "Ed25519", x: EMAIL_PROOF_PINS.issuerX },
|
|
318
|
+
validFrom: "2020-01-01T00:00:00Z",
|
|
319
|
+
validUntil: "2100-01-01T00:00:00Z"
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
var DEFAULT_EMAIL_PROOF_PROFILE = "tinycloud.email-proof/v1";
|
|
323
|
+
var PROFILE_PINS = {
|
|
324
|
+
"tinycloud.email-proof/v1": { descriptorDigest: EMAIL_PROOF_PINS.descriptorDigest, requiredStep: "mailbox_otp", enabled: true }
|
|
325
|
+
};
|
|
326
|
+
var OPENER_ORIGIN = "https://localhost";
|
|
327
|
+
var STEP_TIMEOUT_MS = 6e4;
|
|
328
|
+
var HOLDER_DID_PREFIX = "did:key:";
|
|
329
|
+
var HOLDER_DID_PATTERN = /^did:key:z6Mk[^#]+$/;
|
|
330
|
+
var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
331
|
+
var INSPECT = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
|
|
332
|
+
var RECOVERABILITY = {
|
|
333
|
+
ADDRESS_INVALID: false,
|
|
334
|
+
HOLDER_INVALID: false,
|
|
335
|
+
CODE_INVALID: true,
|
|
336
|
+
RATE_LIMITED: true,
|
|
337
|
+
ISSUER_UNAVAILABLE: true,
|
|
338
|
+
NETWORK: true,
|
|
339
|
+
PROFILE_UNAVAILABLE: false,
|
|
340
|
+
CODE_REJECTED: true,
|
|
341
|
+
CODE_ATTEMPTS_EXHAUSTED: false,
|
|
342
|
+
SIGNATURE_REJECTED: true,
|
|
343
|
+
CREDENTIAL_INVALID: false,
|
|
344
|
+
SESSION_EXPIRED: false,
|
|
345
|
+
SESSION_INVALID: false,
|
|
346
|
+
CANCELED: false,
|
|
347
|
+
FAILED: false
|
|
348
|
+
};
|
|
349
|
+
var MESSAGES = {
|
|
350
|
+
ADDRESS_INVALID: "Email address is invalid.",
|
|
351
|
+
HOLDER_INVALID: "Holder must be a did:key Ed25519 identifier.",
|
|
352
|
+
CODE_INVALID: "Verification code must be 8 digits.",
|
|
353
|
+
ISSUER_UNAVAILABLE: "Email verification is unavailable right now.",
|
|
354
|
+
NETWORK: "Could not reach the email verification service.",
|
|
355
|
+
PROFILE_UNAVAILABLE: "The witness does not offer the email verification profile.",
|
|
356
|
+
CODE_REJECTED: "Verification code was not accepted.",
|
|
357
|
+
CODE_ATTEMPTS_EXHAUSTED: "Too many wrong codes. Request a new code.",
|
|
358
|
+
SIGNATURE_REJECTED: "Holder signature was rejected.",
|
|
359
|
+
CREDENTIAL_INVALID: "Issued credential failed verification.",
|
|
360
|
+
SESSION_EXPIRED: "Email verification request expired. Request a new code.",
|
|
361
|
+
SESSION_INVALID: "Email verification request is no longer valid. Request a new code.",
|
|
362
|
+
CANCELED: "Email verification was canceled.",
|
|
363
|
+
FAILED: "Email verification failed."
|
|
364
|
+
};
|
|
365
|
+
function rateLimitMessage(retryAfterMs) {
|
|
366
|
+
const head = "Too many codes were requested for this address.";
|
|
367
|
+
if (retryAfterMs === void 0 || !Number.isFinite(retryAfterMs) || retryAfterMs <= 0) return `${head} Try again in about an hour.`;
|
|
368
|
+
const minutes = Math.max(1, Math.ceil(retryAfterMs / 6e4));
|
|
369
|
+
return `${head} Try again in ${minutes} ${minutes === 1 ? "minute" : "minutes"}.`;
|
|
370
|
+
}
|
|
371
|
+
var EmailProofError = class extends Error {
|
|
372
|
+
constructor(step, code, retryAfterMs, recoverable = RECOVERABILITY[code]) {
|
|
373
|
+
super(code === "RATE_LIMITED" ? rateLimitMessage(retryAfterMs) : MESSAGES[code]);
|
|
374
|
+
this.name = "EmailProofError";
|
|
375
|
+
this.step = step;
|
|
376
|
+
this.code = code;
|
|
377
|
+
this.retryAfterMs = retryAfterMs;
|
|
378
|
+
this.recoverable = recoverable;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
function createEd25519HolderSigner(did, sign) {
|
|
382
|
+
const fragment = did.indexOf("#");
|
|
383
|
+
const bare = fragment === -1 ? did : did.slice(0, fragment);
|
|
384
|
+
if (fragment !== -1 && did.slice(fragment + 1) !== bare.slice(HOLDER_DID_PREFIX.length)) throw new EmailProofError("request", "HOLDER_INVALID");
|
|
385
|
+
if (!HOLDER_DID_PATTERN.test(bare)) throw new EmailProofError("request", "HOLDER_INVALID");
|
|
386
|
+
return { did: bare, alg: "EdDSA", kid: holderKid(bare), sign };
|
|
387
|
+
}
|
|
388
|
+
function restoreEmailProofPending(fields) {
|
|
389
|
+
const shown = {
|
|
390
|
+
type: fields.type,
|
|
391
|
+
version: fields.version,
|
|
392
|
+
profile: fields.profile,
|
|
393
|
+
address: fields.address,
|
|
394
|
+
requestId: fields.requestId,
|
|
395
|
+
holderDid: fields.holderDid,
|
|
396
|
+
descriptorDigest: fields.descriptorDigest,
|
|
397
|
+
requirementDigest: fields.requirementDigest,
|
|
398
|
+
openerOrigin: fields.openerOrigin,
|
|
399
|
+
createdAt: fields.createdAt,
|
|
400
|
+
expiresAt: fields.expiresAt
|
|
401
|
+
};
|
|
402
|
+
const pending = { ...shown };
|
|
403
|
+
hide(pending, "verifier", fields.verifier);
|
|
404
|
+
hide(pending, "toJSON", () => ({ ...shown }));
|
|
405
|
+
hide(pending, INSPECT, () => ({ ...shown, verifier: "[redacted]" }));
|
|
406
|
+
return Object.freeze(pending);
|
|
407
|
+
}
|
|
408
|
+
function hide(target, key, value) {
|
|
409
|
+
Object.defineProperty(target, key, { value, enumerable: false, writable: false, configurable: false });
|
|
410
|
+
}
|
|
411
|
+
function pendingDeadlineMs(pending) {
|
|
412
|
+
return Math.min(Date.parse(pending.expiresAt), Date.parse(pending.createdAt) + EMAIL_PROOF_PENDING_MAX_MS);
|
|
413
|
+
}
|
|
414
|
+
function createMemoryEmailProofPendingStore() {
|
|
415
|
+
const byRequest = /* @__PURE__ */ new Map();
|
|
416
|
+
const prune = (now) => {
|
|
417
|
+
for (const [requestId, pending] of byRequest) if (pendingDeadlineMs(pending) <= now.getTime()) byRequest.delete(requestId);
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
async findLive(address, now) {
|
|
421
|
+
prune(now);
|
|
422
|
+
let newest;
|
|
423
|
+
for (const pending of byRequest.values()) {
|
|
424
|
+
if (pending.address === address && (!newest || Date.parse(pending.createdAt) > Date.parse(newest.createdAt))) newest = pending;
|
|
425
|
+
}
|
|
426
|
+
return newest;
|
|
427
|
+
},
|
|
428
|
+
async put(pending) {
|
|
429
|
+
byRequest.set(pending.requestId, pending);
|
|
430
|
+
},
|
|
431
|
+
async delete(requestId) {
|
|
432
|
+
byRequest.delete(requestId);
|
|
433
|
+
},
|
|
434
|
+
async get(requestId, now) {
|
|
435
|
+
prune(now);
|
|
436
|
+
return byRequest.get(requestId);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
var CodeRequested = class extends Error {
|
|
441
|
+
};
|
|
442
|
+
var pauseForCode = async () => {
|
|
443
|
+
throw new CodeRequested("code requested");
|
|
444
|
+
};
|
|
445
|
+
async function requestEmailProofCode(input) {
|
|
446
|
+
const profile = input.profile ?? DEFAULT_EMAIL_PROOF_PROFILE;
|
|
447
|
+
profilePin(profile, "request");
|
|
448
|
+
const address = normalizeAddress(input.address);
|
|
449
|
+
assertHolder(input.holder, "request");
|
|
450
|
+
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
451
|
+
const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);
|
|
452
|
+
const requirement = emailRequirement(address, profile);
|
|
453
|
+
const requirementDigest = await credentialRequirementDigest(requirement);
|
|
454
|
+
const descriptor = await witnessDescriptor(profile, requirement, { fetch: input.fetch, signal }, "request");
|
|
455
|
+
const descriptorDigest = await canonicalDigest2(descriptor);
|
|
456
|
+
const verifier = randomVerifier();
|
|
457
|
+
const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });
|
|
458
|
+
const createdAtMs = now().getTime();
|
|
459
|
+
let created;
|
|
460
|
+
try {
|
|
461
|
+
created = await transport.create({
|
|
462
|
+
descriptor,
|
|
463
|
+
descriptorDigest,
|
|
464
|
+
requirement,
|
|
465
|
+
requirementDigest,
|
|
466
|
+
holderDid: input.holder.did,
|
|
467
|
+
openerOrigin: OPENER_ORIGIN,
|
|
468
|
+
completionVerifierChallenge: await sha256Base64Url2(verifier),
|
|
469
|
+
signal
|
|
470
|
+
});
|
|
471
|
+
} catch (error) {
|
|
472
|
+
throw mapError(error, "request", signal);
|
|
473
|
+
}
|
|
474
|
+
const serverExpiry = Date.parse(created.expiresAt);
|
|
475
|
+
if (!Number.isFinite(serverExpiry)) throw new EmailProofError("request", "FAILED");
|
|
476
|
+
const pending = restoreEmailProofPending({
|
|
477
|
+
type: "TinyCloudEmailProofPending",
|
|
478
|
+
version: 1,
|
|
479
|
+
profile,
|
|
480
|
+
address,
|
|
481
|
+
requestId: created.requestId,
|
|
482
|
+
verifier,
|
|
483
|
+
holderDid: input.holder.did,
|
|
484
|
+
descriptorDigest,
|
|
485
|
+
requirementDigest,
|
|
486
|
+
openerOrigin: OPENER_ORIGIN,
|
|
487
|
+
createdAt: new Date(createdAtMs).toISOString(),
|
|
488
|
+
expiresAt: new Date(Math.min(serverExpiry, createdAtMs + EMAIL_PROOF_PENDING_MAX_MS)).toISOString()
|
|
489
|
+
});
|
|
490
|
+
try {
|
|
491
|
+
await interpretCredentialFlow({
|
|
492
|
+
descriptor,
|
|
493
|
+
requirement,
|
|
494
|
+
requestId: created.requestId,
|
|
495
|
+
verifier,
|
|
496
|
+
holderDid: input.holder.did,
|
|
497
|
+
descriptorDigest,
|
|
498
|
+
requirementDigest,
|
|
499
|
+
openerOrigin: OPENER_ORIGIN,
|
|
500
|
+
transport,
|
|
501
|
+
signing: holderSigning(input.holder),
|
|
502
|
+
handlers: { mailbox_otp: pauseForCode },
|
|
503
|
+
signal
|
|
504
|
+
});
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (error instanceof CodeRequested) return pending;
|
|
507
|
+
throw mapError(error, "request", signal);
|
|
508
|
+
}
|
|
509
|
+
throw new EmailProofError("request", "FAILED");
|
|
510
|
+
}
|
|
511
|
+
async function completeEmailProof(input) {
|
|
512
|
+
return completeWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);
|
|
513
|
+
}
|
|
514
|
+
async function completeWithIssuerKeys(input, issuerKeys) {
|
|
515
|
+
const { pending } = input;
|
|
516
|
+
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
517
|
+
if (!isPending(pending)) throw new EmailProofError("session", "SESSION_INVALID");
|
|
518
|
+
if (pendingDeadlineMs(pending) <= now().getTime()) throw new EmailProofError("session", "SESSION_EXPIRED");
|
|
519
|
+
if (!/^\d{8}$/.test(input.code)) throw new EmailProofError("code", "CODE_INVALID");
|
|
520
|
+
assertHolder(input.holder, "code");
|
|
521
|
+
if (input.holder.did !== pending.holderDid) throw new EmailProofError("session", "SESSION_INVALID");
|
|
522
|
+
const signal = input.signal ?? AbortSignal.timeout(STEP_TIMEOUT_MS);
|
|
523
|
+
const requirement = emailRequirement(pending.address, pending.profile);
|
|
524
|
+
if (await credentialRequirementDigest(requirement) !== pending.requirementDigest) throw new EmailProofError("session", "SESSION_INVALID");
|
|
525
|
+
const descriptor = await witnessDescriptor(pending.profile, requirement, { fetch: input.fetch, signal }, "session");
|
|
526
|
+
const descriptorDigest = await canonicalDigest2(descriptor);
|
|
527
|
+
if (descriptorDigest !== pending.descriptorDigest) throw new EmailProofError("session", "SESSION_INVALID");
|
|
528
|
+
const transport = new OpenCredentialsHttpTransport(descriptor, input.fetch, { honorServerRecoverable: true });
|
|
529
|
+
try {
|
|
530
|
+
const state = await transport.state(pending.requestId, pending.verifier, signal);
|
|
531
|
+
if (state.nextStep?.constraints.challengeRequired === true) throw new EmailProofError("session", "SESSION_INVALID");
|
|
532
|
+
await interpretCredentialFlow({
|
|
533
|
+
descriptor,
|
|
534
|
+
requirement,
|
|
535
|
+
requestId: pending.requestId,
|
|
536
|
+
verifier: pending.verifier,
|
|
537
|
+
holderDid: pending.holderDid,
|
|
538
|
+
descriptorDigest,
|
|
539
|
+
requirementDigest: pending.requirementDigest,
|
|
540
|
+
openerOrigin: pending.openerOrigin,
|
|
541
|
+
transport,
|
|
542
|
+
signing: holderSigning(input.holder),
|
|
543
|
+
handlers: { mailbox_otp: async () => ({ otp: input.code }) },
|
|
544
|
+
signal
|
|
545
|
+
});
|
|
546
|
+
} catch (error) {
|
|
547
|
+
throw mapError(error, "code", signal);
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const envelope = await transport.result(pending.requestId, pending.verifier, signal);
|
|
551
|
+
const verified = await verifyIssuedCredential({
|
|
552
|
+
envelope,
|
|
553
|
+
descriptor,
|
|
554
|
+
descriptorDigest,
|
|
555
|
+
requirement,
|
|
556
|
+
holderDid: pending.holderDid,
|
|
557
|
+
issuerMetadata: pinnedIssuerMetadata(issuerKeys),
|
|
558
|
+
now: now(),
|
|
559
|
+
checkStatus: (status, statusSignal) => transport.checkStatus(status, statusSignal),
|
|
560
|
+
signal
|
|
561
|
+
});
|
|
562
|
+
return { envelope, verified, credentialId: verified.credentialId, issuedAt: verified.issuedAt, expiresAt: verified.expiresAt };
|
|
563
|
+
} catch (error) {
|
|
564
|
+
throw mapError(error, "credential", signal);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function acquireEmailProof(input) {
|
|
568
|
+
return acquireWithIssuerKeys(input, EMAIL_PROOF_ISSUER_KEYS);
|
|
569
|
+
}
|
|
570
|
+
async function acquireWithIssuerKeys(input, issuerKeys) {
|
|
571
|
+
const address = normalizeAddress(input.address);
|
|
572
|
+
const now = input.now ?? (() => /* @__PURE__ */ new Date());
|
|
573
|
+
const store = input.pendingStore;
|
|
574
|
+
let pending = store ? await store.findLive(address, now()) : void 0;
|
|
575
|
+
if (!pending) {
|
|
576
|
+
pending = await requestEmailProofCode({ ...input, address });
|
|
577
|
+
await store?.put(pending);
|
|
578
|
+
}
|
|
579
|
+
const shown = restoreEmailProofPending(pending);
|
|
580
|
+
if (!input.getCode) return { status: "code-requested", pending: shown };
|
|
581
|
+
const code = await input.getCode({ pending: shown });
|
|
582
|
+
try {
|
|
583
|
+
const result = await completeWithIssuerKeys({ ...input, pending, code }, issuerKeys);
|
|
584
|
+
const warnings = [];
|
|
585
|
+
if (store) {
|
|
586
|
+
try {
|
|
587
|
+
await store.delete(pending.requestId);
|
|
588
|
+
} catch {
|
|
589
|
+
warnings.push("The pending request could not be deleted after success; it expires at its deadline.");
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return { status: "issued", result, warnings };
|
|
593
|
+
} catch (error) {
|
|
594
|
+
if (!(error instanceof EmailProofError && error.recoverable)) await store?.delete(pending.requestId).catch(() => void 0);
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function normalizeAddress(address) {
|
|
599
|
+
const value = address.trim();
|
|
600
|
+
if (value.length > 254 || !EMAIL_PATTERN.test(value)) throw new EmailProofError("request", "ADDRESS_INVALID");
|
|
601
|
+
return value;
|
|
602
|
+
}
|
|
603
|
+
function holderKid(did) {
|
|
604
|
+
return `${did}#${did.slice(HOLDER_DID_PREFIX.length)}`;
|
|
605
|
+
}
|
|
606
|
+
function assertHolder(holder, step) {
|
|
607
|
+
if (!HOLDER_DID_PATTERN.test(holder.did) || holder.alg !== "EdDSA" || holder.kid !== holderKid(holder.did)) throw new EmailProofError(step, "HOLDER_INVALID");
|
|
608
|
+
}
|
|
609
|
+
function profilePin(profile, step) {
|
|
610
|
+
const entry = Object.prototype.hasOwnProperty.call(PROFILE_PINS, profile) ? PROFILE_PINS[profile] : void 0;
|
|
611
|
+
if (!entry || !entry.enabled) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
|
|
612
|
+
return entry;
|
|
613
|
+
}
|
|
614
|
+
function emailRequirement(address, profile) {
|
|
615
|
+
return createEmailCredentialRequirement({ email: address, profile: { id: profile, version: 1 }, credentialType: EMAIL_PROOF_CREDENTIAL_TYPE });
|
|
616
|
+
}
|
|
617
|
+
function emailProofRequirement(address) {
|
|
618
|
+
return emailRequirement(normalizeAddress(address), DEFAULT_EMAIL_PROOF_PROFILE);
|
|
619
|
+
}
|
|
620
|
+
async function witnessDescriptor(profile, requirement, options, step) {
|
|
621
|
+
const pin = profilePin(profile, step);
|
|
622
|
+
let descriptor;
|
|
623
|
+
try {
|
|
624
|
+
descriptor = await discoverCatalogDescriptor(requirement, DISCOVERY_URL, { fetch: options.fetch, signal: options.signal });
|
|
625
|
+
} catch (error) {
|
|
626
|
+
throw discoveryFailure(error, options.signal, step);
|
|
627
|
+
}
|
|
628
|
+
if (descriptor.issuer.origin !== EMAIL_PROOF_PINS.witnessOrigin || descriptor.issuer.did !== EMAIL_PROOF_PINS.issuerDid || !descriptor.steps.some((candidate) => candidate.type === pin.requiredStep)) {
|
|
629
|
+
throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
|
|
630
|
+
}
|
|
631
|
+
if (await canonicalDigest2(descriptor) !== pin.descriptorDigest) throw new EmailProofError(step, "PROFILE_UNAVAILABLE");
|
|
632
|
+
return descriptor;
|
|
633
|
+
}
|
|
634
|
+
async function discoverCatalogDescriptor(requirement, discoveryUrl, options = {}) {
|
|
635
|
+
let url;
|
|
636
|
+
try {
|
|
637
|
+
url = new URL(discoveryUrl);
|
|
638
|
+
} catch {
|
|
639
|
+
throw new CredentialError3("UNSUPPORTED_PROFILE", "Discovery URL is invalid");
|
|
640
|
+
}
|
|
641
|
+
if (url.protocol !== "https:" || !["/.well-known/opencredentials", "/v1/credential-types"].includes(url.pathname) || url.search || url.hash) throw new CredentialError3("UNSUPPORTED_PROFILE", "Discovery URL is not an allowed catalog endpoint");
|
|
642
|
+
const init = { credentials: "omit", cache: "no-store", redirect: "error", referrerPolicy: "no-referrer", signal: options.signal };
|
|
643
|
+
let response;
|
|
644
|
+
try {
|
|
645
|
+
response = await (options.fetch ?? globalThis.fetch.bind(globalThis))(url, init);
|
|
646
|
+
} catch (cause) {
|
|
647
|
+
throw new CredentialError3("OFFLINE", "Credential discovery is unavailable", { cause });
|
|
648
|
+
}
|
|
649
|
+
if (!response.ok) throw new CredentialError3(response.status === 503 ? "ISSUER_UNREADY" : "OFFLINE", "Credential discovery is unavailable");
|
|
650
|
+
const catalog = await response.json();
|
|
651
|
+
if (catalog.type !== "tinycloud.credentials/catalog/v1" || catalog.protocol !== "tinycloud.credentials/acquisition/v1" || catalog.catalogVersion !== 1 || !Array.isArray(catalog.profiles)) throw new CredentialError3("DESCRIPTOR_INVALID", "Credential catalog is invalid");
|
|
652
|
+
for (const entryValue of catalog.profiles) {
|
|
653
|
+
if (typeof entryValue !== "object" || entryValue === null || Array.isArray(entryValue)) continue;
|
|
654
|
+
const entry = entryValue;
|
|
655
|
+
if (entry.supported !== true || entry.enabled !== true || entry.readiness !== "ready" && entry.readiness !== "degraded") continue;
|
|
656
|
+
try {
|
|
657
|
+
const candidate = validateCredentialFlowDescriptor(entry.descriptor);
|
|
658
|
+
if (descriptorSatisfiesRequirement(candidate, requirement) && entry.descriptorDigest === await canonicalDigest2(candidate)) return candidate;
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
throw new CredentialError3("UNSUPPORTED_PROFILE", "No available credential profile satisfies the requirement");
|
|
663
|
+
}
|
|
664
|
+
function discoveryFailure(error, signal, step) {
|
|
665
|
+
if (signal.aborted && signal.reason?.name !== "TimeoutError") return new EmailProofError(step, "CANCELED");
|
|
666
|
+
return mapError(error, step, signal);
|
|
667
|
+
}
|
|
668
|
+
function pinnedIssuerMetadata(keys) {
|
|
669
|
+
return { type: "OpenCredentialsIssuerMetadata", version: 1, origin: EMAIL_PROOF_PINS.witnessOrigin, issuerDid: EMAIL_PROOF_PINS.issuerDid, keys, cache: { maxAgeSeconds: 3600, etag: "pinned" } };
|
|
670
|
+
}
|
|
671
|
+
function holderSigning(holder) {
|
|
672
|
+
return { autoSign: async (_binding, bytes) => holder.sign(bytes), holderKey: { alg: "EdDSA", kid: holder.kid } };
|
|
673
|
+
}
|
|
674
|
+
function randomVerifier() {
|
|
675
|
+
return encodeBase64Url3(crypto.getRandomValues(new Uint8Array(32)));
|
|
676
|
+
}
|
|
677
|
+
function isPending(value) {
|
|
678
|
+
if (typeof value !== "object" || value === null) return false;
|
|
679
|
+
const pending = value;
|
|
680
|
+
return pending.type === "TinyCloudEmailProofPending" && pending.version === 1 && pending.openerOrigin === OPENER_ORIGIN && [pending.profile, pending.address, pending.requestId, pending.verifier, pending.holderDid, pending.descriptorDigest, pending.requirementDigest, pending.openerOrigin, pending.createdAt, pending.expiresAt].every((field) => typeof field === "string") && Number.isFinite(Date.parse(pending.createdAt)) && Number.isFinite(Date.parse(pending.expiresAt));
|
|
681
|
+
}
|
|
682
|
+
function mapError(value, step, signal) {
|
|
683
|
+
if (value instanceof EmailProofError) return value;
|
|
684
|
+
if (value instanceof DOMException && value.name === "TimeoutError") return new EmailProofError(step, "NETWORK");
|
|
685
|
+
const error = credentialError(value);
|
|
686
|
+
const details = error.details;
|
|
687
|
+
switch (error.code) {
|
|
688
|
+
case "ISSUER_UNREADY":
|
|
689
|
+
return details.state === "rate_limited" ? new EmailProofError(step, "RATE_LIMITED", details.retryAfterMs, error.recoverable) : new EmailProofError(step, "ISSUER_UNAVAILABLE", void 0, error.recoverable);
|
|
690
|
+
case "OFFLINE":
|
|
691
|
+
if (signal?.aborted && signal.reason?.name !== "TimeoutError") return new EmailProofError(step, "CANCELED");
|
|
692
|
+
return new EmailProofError(step, "NETWORK");
|
|
693
|
+
case "UNSUPPORTED_PROFILE":
|
|
694
|
+
case "DESCRIPTOR_INVALID":
|
|
695
|
+
return new EmailProofError(step, "PROFILE_UNAVAILABLE");
|
|
696
|
+
case "PROOF_REJECTED":
|
|
697
|
+
return new EmailProofError("code", "CODE_REJECTED", void 0, true);
|
|
698
|
+
case "VERIFICATION_FAILED":
|
|
699
|
+
if (details.state === "proof_attempts_exhausted") return new EmailProofError("code", "CODE_ATTEMPTS_EXHAUSTED");
|
|
700
|
+
if (step === "credential") return new EmailProofError("credential", "CREDENTIAL_INVALID");
|
|
701
|
+
if (step === "code") return new EmailProofError("code", "CODE_REJECTED", void 0, false);
|
|
702
|
+
return new EmailProofError(step, "FAILED");
|
|
703
|
+
case "HOLDER_MISMATCH":
|
|
704
|
+
return new EmailProofError("credential", "CREDENTIAL_INVALID");
|
|
705
|
+
case "REQUEST_EXPIRED":
|
|
706
|
+
return new EmailProofError("session", "SESSION_EXPIRED");
|
|
707
|
+
case "REQUEST_SUBSTITUTED":
|
|
708
|
+
return new EmailProofError("session", "SESSION_INVALID");
|
|
709
|
+
case "SIGNATURE_REJECTED":
|
|
710
|
+
return new EmailProofError("credential", "SIGNATURE_REJECTED", void 0, error.recoverable);
|
|
711
|
+
case "CANCELED":
|
|
712
|
+
return new EmailProofError(step, "CANCELED");
|
|
713
|
+
default:
|
|
714
|
+
return new EmailProofError(step, "FAILED");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
export {
|
|
718
|
+
DEFAULT_EMAIL_PROOF_PROFILE,
|
|
719
|
+
EMAIL_PROOF_CREDENTIAL_TYPE,
|
|
720
|
+
EMAIL_PROOF_PENDING_MAX_MS,
|
|
721
|
+
EmailProofError,
|
|
722
|
+
OpenCredentialsHttpTransport,
|
|
723
|
+
acquireEmailProof,
|
|
724
|
+
completeEmailProof,
|
|
725
|
+
createEd25519HolderSigner,
|
|
726
|
+
createMemoryEmailProofPendingStore,
|
|
727
|
+
emailProofRequirement,
|
|
728
|
+
interpretCredentialFlow,
|
|
729
|
+
mailboxSubject,
|
|
730
|
+
requestEmailProofCode
|
|
731
|
+
};
|
|
732
|
+
//# sourceMappingURL=credentials.js.map
|