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