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