@voidly/session 1.0.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -19
- package/dist/index.d.ts +190 -3
- package/dist/index.mjs +1 -1
- package/dist/proofs.d.ts +67 -0
- package/dist/proofs.mjs +389 -0
- package/dist/proofsAuto.d.ts +111 -0
- package/dist/proofsAuto.mjs +554 -0
- package/dist/proofsCli.mjs +630 -0
- package/package.json +33 -4
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import nacl3 from "tweetnacl";
|
|
2
|
+
import nacl from "tweetnacl";
|
|
3
|
+
import __naclUtil0 from "tweetnacl-util";
|
|
4
|
+
const { decodeBase64 } = __naclUtil0;
|
|
5
|
+
var MAX_WINDOW_MS = 3600 * 1e3, MAX_CLOCK_SKEW_MS = 30 * 1e3;
|
|
6
|
+
function canonicalize(value) {
|
|
7
|
+
if (value == null) return "null";
|
|
8
|
+
if (typeof value == "boolean") return value ? "true" : "false";
|
|
9
|
+
if (typeof value == "number") {
|
|
10
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) throw new Error("canonicalize: only finite integers supported");
|
|
11
|
+
return value.toString(10);
|
|
12
|
+
}
|
|
13
|
+
if (typeof value == "bigint") return value.toString(10);
|
|
14
|
+
if (typeof value == "string") return JSON.stringify(value);
|
|
15
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
|
|
16
|
+
if (typeof value == "object") {
|
|
17
|
+
let obj = value;
|
|
18
|
+
return "{" + Object.keys(obj).filter((k) => obj[k] !== null && obj[k] !== void 0).sort().map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`canonicalize: unsupported type ${typeof value}`);
|
|
21
|
+
}
|
|
22
|
+
function canonicalBytes(value) {
|
|
23
|
+
return new TextEncoder().encode(canonicalize(value));
|
|
24
|
+
}
|
|
25
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
26
|
+
function toBase58(bytes) {
|
|
27
|
+
let result = "", num = BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
|
28
|
+
for (; num > 0n; ) {
|
|
29
|
+
let remainder = num % 58n;
|
|
30
|
+
num = num / 58n, result = BASE58_ALPHABET[Number(remainder)] + result;
|
|
31
|
+
}
|
|
32
|
+
for (let byte of bytes) if (byte === 0) result = "1" + result;
|
|
33
|
+
else break;
|
|
34
|
+
return result || "1";
|
|
35
|
+
}
|
|
36
|
+
function deriveDidFromSigningKey(signingPublicKey) {
|
|
37
|
+
return `did:voidly:${toBase58(signingPublicKey.slice(0, 16))}`;
|
|
38
|
+
}
|
|
39
|
+
async function sha256Bytes(bytes) {
|
|
40
|
+
let buf = await crypto.subtle.digest("SHA-256", bytes);
|
|
41
|
+
return new Uint8Array(buf);
|
|
42
|
+
}
|
|
43
|
+
async function sha256Hex(bytes) {
|
|
44
|
+
let digest = await sha256Bytes(bytes);
|
|
45
|
+
return Array.from(digest).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
46
|
+
}
|
|
47
|
+
var FRAME_OFFER_HASH_BYTES = 32, FRAME_BODY_NONCE_BYTES = 24, FRAME_HEADER_LENGTH = FRAME_OFFER_HASH_BYTES + FRAME_BODY_NONCE_BYTES + 4, MIN_FRAME_BUCKET = 512;
|
|
48
|
+
var SECRETBOX_TAG_BYTES = 16, CAPSULE_SALT_BYTES = 32;
|
|
49
|
+
function base64Length(bytes) {
|
|
50
|
+
return 4 * Math.ceil(bytes / 3);
|
|
51
|
+
}
|
|
52
|
+
var SESSION_MAX_BODY_BYTES = 32 * 1024, SESSION_WORST_CASE_ENVELOPE_BYTES = 4972, MAX_FRAME_BUCKET_BYTES = (() => {
|
|
53
|
+
let best = 0;
|
|
54
|
+
for (let bucket = MIN_FRAME_BUCKET; bucket <= SESSION_MAX_BODY_BYTES; bucket *= 2) base64Length(bucket + SECRETBOX_TAG_BYTES) + SESSION_WORST_CASE_ENVELOPE_BYTES <= SESSION_MAX_BODY_BYTES && (best = bucket);
|
|
55
|
+
if (best === 0) throw new Error("wireBudget: the transport ceiling cannot carry the minimum frame bucket");
|
|
56
|
+
return best;
|
|
57
|
+
})(), MAX_CAPSULE_BODY_BYTES = MAX_FRAME_BUCKET_BYTES + SECRETBOX_TAG_BYTES, MAX_CAPSULE_BODY_BASE64_LENGTH = base64Length(MAX_CAPSULE_BODY_BYTES), MAX_SEALED_PAYLOAD_BYTES = MAX_FRAME_BUCKET_BYTES - FRAME_HEADER_LENGTH, SESSION_EVIDENCE_HEADROOM_BYTES = SESSION_MAX_BODY_BYTES - MAX_CAPSULE_BODY_BASE64_LENGTH - SESSION_WORST_CASE_ENVELOPE_BYTES;
|
|
58
|
+
var TASK_BRIEF_SCHEMA = "voidly-task-brief/v1";
|
|
59
|
+
var TASK_RESULT_SCHEMA = "voidly-task-result/v1";
|
|
60
|
+
var MAX_OFFER_TTL_MS = 1440 * 60 * 1e3, MAX_GRANT_TTL_MS = 1440 * 60 * 1e3;
|
|
61
|
+
var SESSION_RAIL_MIN_CONFIRMATIONS = 12, SESSION_RAIL_BLOCK_TIME_MS = 2e3, MIN_GRANT_TTL_MS = SESSION_RAIL_MIN_CONFIRMATIONS * SESSION_RAIL_BLOCK_TIME_MS + MAX_CLOCK_SKEW_MS, MAX_SERVICE_REF_LENGTH = 128;
|
|
62
|
+
var BRIEF_SALT_BASE64_LENGTH = base64Length(CAPSULE_SALT_BYTES), canonicalPayloadOverhead = (schema, field) => canonicalBytes({ schema, [field]: "", salt_base64: "A".repeat(BRIEF_SALT_BASE64_LENGTH) }).length, MAX_BRIEF_LENGTH = MAX_SEALED_PAYLOAD_BYTES - Math.max(canonicalPayloadOverhead(TASK_BRIEF_SCHEMA, "brief"), canonicalPayloadOverhead(TASK_RESULT_SCHEMA, "result"));
|
|
63
|
+
var MAX_RECOVERY_TTL_MS = 10080 * 60 * 1e3;
|
|
64
|
+
var DID_RE = /^did:voidly:[A-Za-z0-9._-]{1,64}$/;
|
|
65
|
+
function hasOnlyKeys(raw, allowed) {
|
|
66
|
+
for (let k of Object.keys(raw)) if (!allowed.includes(k)) return false;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function isBase64Key32(value, decode) {
|
|
70
|
+
if (typeof value != "string" || value.length === 0) return false;
|
|
71
|
+
try {
|
|
72
|
+
return decode(value).length === 32;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
var CAIP2_RE = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}$/, CAIP10_RE = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}:[-.%a-zA-Z0-9]{1,128}$/, CAIP19_RE = /^[-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32}\/[-a-z0-9]{3,8}:[-.%a-zA-Z0-9]{1,128}(\/[-.%a-zA-Z0-9]{1,78})?$/, POSITIVE_DECIMAL_RE = /^[1-9][0-9]{0,77}$/;
|
|
78
|
+
function isCaip2(s) {
|
|
79
|
+
return typeof s == "string" && CAIP2_RE.test(s);
|
|
80
|
+
}
|
|
81
|
+
function isCaip10(s) {
|
|
82
|
+
return typeof s == "string" && CAIP10_RE.test(s);
|
|
83
|
+
}
|
|
84
|
+
function isCaip19(s) {
|
|
85
|
+
return typeof s == "string" && CAIP19_RE.test(s);
|
|
86
|
+
}
|
|
87
|
+
function isPositiveDecimalString(s) {
|
|
88
|
+
return typeof s == "string" && POSITIVE_DECIMAL_RE.test(s);
|
|
89
|
+
}
|
|
90
|
+
import __naclUtil1 from "tweetnacl-util";
|
|
91
|
+
const { decodeBase64: decodeBase642 } = __naclUtil1;
|
|
92
|
+
var SESSION_HIRE_SCHEMA = "voidly-session-hire/v1";
|
|
93
|
+
var AUTHORIZATION_ENTRY_POINTS = ["transfer_with_authorization", "receive_with_authorization"];
|
|
94
|
+
import nacl2 from "tweetnacl";
|
|
95
|
+
import __naclUtil2 from "tweetnacl-util";
|
|
96
|
+
const { decodeBase64: decodeBase643 } = __naclUtil2;
|
|
97
|
+
var MINTED = /* @__PURE__ */ new WeakMap();
|
|
98
|
+
function containerCopy(value) {
|
|
99
|
+
if (value === null || typeof value != "object") return null;
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
let source = value, sink = [];
|
|
102
|
+
for (let key of Object.keys(source)) sink[key] = source[key];
|
|
103
|
+
return sink;
|
|
104
|
+
}
|
|
105
|
+
return { ...value };
|
|
106
|
+
}
|
|
107
|
+
function rebuildDocumentDeep(document) {
|
|
108
|
+
let top = containerCopy(document);
|
|
109
|
+
if (top === null) return document;
|
|
110
|
+
let made = /* @__PURE__ */ new Map();
|
|
111
|
+
made.set(document, top);
|
|
112
|
+
let pending2 = [top];
|
|
113
|
+
for (; pending2.length > 0; ) {
|
|
114
|
+
let frame = pending2.pop();
|
|
115
|
+
if (frame === void 0) break;
|
|
116
|
+
for (let key of Object.keys(frame)) {
|
|
117
|
+
let child = frame[key];
|
|
118
|
+
if (child === null || typeof child != "object") continue;
|
|
119
|
+
let already = made.get(child);
|
|
120
|
+
if (already !== void 0) {
|
|
121
|
+
frame[key] = already;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
let copy = containerCopy(child);
|
|
125
|
+
copy !== null && (made.set(child, copy), frame[key] = copy, pending2.push(copy));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return top;
|
|
129
|
+
}
|
|
130
|
+
function freezeManifestDeep(m) {
|
|
131
|
+
let seen = /* @__PURE__ */ new Set(), pending2 = [m];
|
|
132
|
+
for (; pending2.length > 0; ) {
|
|
133
|
+
let node = pending2.pop();
|
|
134
|
+
if (node === null || typeof node != "object") continue;
|
|
135
|
+
let container = node;
|
|
136
|
+
if (!seen.has(container)) {
|
|
137
|
+
seen.add(container), Object.freeze(container);
|
|
138
|
+
for (let key of Reflect.ownKeys(container)) {
|
|
139
|
+
if (typeof key == "symbol") continue;
|
|
140
|
+
let d = Object.getOwnPropertyDescriptor(container, key);
|
|
141
|
+
d !== void 0 && "value" in d && pending2.push(d.value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return m;
|
|
146
|
+
}
|
|
147
|
+
function mintVerifiedProvider(manifest) {
|
|
148
|
+
let document = freezeManifestDeep(rebuildDocumentDeep(manifest)), provider = Object.freeze({ manifest: document });
|
|
149
|
+
return MINTED.set(provider, document), provider;
|
|
150
|
+
}
|
|
151
|
+
var PROVIDER_MANIFEST_SCHEMA = "voidly.session.provider.manifest/v1", PROVIDER_MANIFEST_KEYS = ["schema", "provider_did", "signing_public_key_base64", "encryption_public_key_base64", "attestor_public_key_base64", "accept_url", "hire_message_schema", "worker_base_url", "hint_url", "relays", "grant_ttl_ms", "acceptance_ttl_ms", "services", "payment_buys", "notes", "signature_base64"];
|
|
152
|
+
function manifestSigningBytes(m) {
|
|
153
|
+
return canonicalBytes(m);
|
|
154
|
+
}
|
|
155
|
+
function isNonEmptyString(v, max = 2048) {
|
|
156
|
+
return typeof v == "string" && v.length > 0 && v.length <= max;
|
|
157
|
+
}
|
|
158
|
+
function isPositiveInt(v) {
|
|
159
|
+
return typeof v == "number" && Number.isInteger(v) && v > 0;
|
|
160
|
+
}
|
|
161
|
+
function verifyManifest(raw, expectedProviderDid) {
|
|
162
|
+
if (typeof raw != "object" || raw === null || Array.isArray(raw)) return { ok: false, reason: "manifest_not_object" };
|
|
163
|
+
let r = { ...raw };
|
|
164
|
+
if (r.schema !== PROVIDER_MANIFEST_SCHEMA) return { ok: false, reason: "manifest_schema_mismatch" };
|
|
165
|
+
if (!hasOnlyKeys(r, PROVIDER_MANIFEST_KEYS)) return { ok: false, reason: "manifest_unexpected_field" };
|
|
166
|
+
if (r.signature_base64 === void 0 || r.signature_base64 === null) return { ok: false, reason: "manifest_signature_missing" };
|
|
167
|
+
if (typeof r.signature_base64 != "string" || r.signature_base64.length === 0) return { ok: false, reason: "manifest_signature_malformed" };
|
|
168
|
+
let signature;
|
|
169
|
+
try {
|
|
170
|
+
signature = decodeBase643(r.signature_base64);
|
|
171
|
+
} catch {
|
|
172
|
+
return { ok: false, reason: "manifest_signature_malformed" };
|
|
173
|
+
}
|
|
174
|
+
if (signature.length !== nacl2.sign.signatureLength) return { ok: false, reason: "manifest_signature_malformed" };
|
|
175
|
+
if (!isBase64Key32(r.signing_public_key_base64, decodeBase643)) return { ok: false, reason: "manifest_field_malformed" };
|
|
176
|
+
if (!isBase64Key32(r.encryption_public_key_base64, decodeBase643)) return { ok: false, reason: "manifest_field_malformed" };
|
|
177
|
+
if (!isBase64Key32(r.attestor_public_key_base64, decodeBase643)) return { ok: false, reason: "manifest_field_malformed" };
|
|
178
|
+
if (typeof r.provider_did != "string" || !DID_RE.test(r.provider_did)) return { ok: false, reason: "manifest_field_malformed" };
|
|
179
|
+
if (!isNonEmptyString(r.accept_url) || !isNonEmptyString(r.worker_base_url)) return { ok: false, reason: "manifest_field_malformed" };
|
|
180
|
+
let rawHintUrl = r.hint_url;
|
|
181
|
+
if (rawHintUrl !== void 0 && !isNonEmptyString(rawHintUrl)) return { ok: false, reason: "manifest_field_malformed" };
|
|
182
|
+
let hintUrl = rawHintUrl === void 0 ? void 0 : rawHintUrl, rawRelays = r.relays, relays;
|
|
183
|
+
if (rawRelays !== void 0) {
|
|
184
|
+
if (typeof rawRelays != "object" || rawRelays === null || Array.isArray(rawRelays)) return { ok: false, reason: "manifest_field_malformed" };
|
|
185
|
+
let rel = { ...rawRelays };
|
|
186
|
+
if (!hasOnlyKeys(rel, ["provider_submits", "accepted_entry_points"])) return { ok: false, reason: "manifest_field_malformed" };
|
|
187
|
+
if (typeof rel.provider_submits != "boolean") return { ok: false, reason: "manifest_field_malformed" };
|
|
188
|
+
let rawDoors = rel.accepted_entry_points;
|
|
189
|
+
if (!Array.isArray(rawDoors) || rawDoors.length === 0 || rawDoors.length > AUTHORIZATION_ENTRY_POINTS.length) return { ok: false, reason: "manifest_field_malformed" };
|
|
190
|
+
let doors = [];
|
|
191
|
+
for (let i = 0; i < rawDoors.length; i += 1) {
|
|
192
|
+
let door = rawDoors[i];
|
|
193
|
+
if (typeof door != "string" || !AUTHORIZATION_ENTRY_POINTS.includes(door)) return { ok: false, reason: "manifest_field_malformed" };
|
|
194
|
+
if (doors.includes(door)) return { ok: false, reason: "manifest_field_malformed" };
|
|
195
|
+
doors.push(door);
|
|
196
|
+
}
|
|
197
|
+
relays = { provider_submits: rel.provider_submits, accepted_entry_points: doors };
|
|
198
|
+
}
|
|
199
|
+
if (r.hire_message_schema !== SESSION_HIRE_SCHEMA) return { ok: false, reason: "manifest_field_malformed" };
|
|
200
|
+
if (r.payment_buys !== "an attempt, not an outcome") return { ok: false, reason: "manifest_field_malformed" };
|
|
201
|
+
if (!isPositiveInt(r.acceptance_ttl_ms)) return { ok: false, reason: "manifest_field_malformed" };
|
|
202
|
+
let ttl = r.grant_ttl_ms;
|
|
203
|
+
if (typeof ttl != "object" || ttl === null || Array.isArray(ttl)) return { ok: false, reason: "manifest_field_malformed" };
|
|
204
|
+
let ttlSnap = { ...ttl };
|
|
205
|
+
if (!hasOnlyKeys(ttlSnap, ["min", "max"])) return { ok: false, reason: "manifest_field_malformed" };
|
|
206
|
+
if (!isPositiveInt(ttlSnap.min) || !isPositiveInt(ttlSnap.max)) return { ok: false, reason: "manifest_field_malformed" };
|
|
207
|
+
if (ttlSnap.min > ttlSnap.max || ttlSnap.max > MAX_GRANT_TTL_MS) return { ok: false, reason: "manifest_field_malformed" };
|
|
208
|
+
let rawNotes = r.notes;
|
|
209
|
+
if (!Array.isArray(rawNotes)) return { ok: false, reason: "manifest_field_malformed" };
|
|
210
|
+
let notes = [];
|
|
211
|
+
for (let i = 0; i < rawNotes.length; i += 1) {
|
|
212
|
+
let note = rawNotes[i];
|
|
213
|
+
if (!isNonEmptyString(note, 4096)) return { ok: false, reason: "manifest_field_malformed" };
|
|
214
|
+
notes.push(note);
|
|
215
|
+
}
|
|
216
|
+
let rawServices = r.services;
|
|
217
|
+
if (!Array.isArray(rawServices) || rawServices.length === 0) return { ok: false, reason: "manifest_field_malformed" };
|
|
218
|
+
let services = [];
|
|
219
|
+
for (let i = 0; i < rawServices.length; i += 1) {
|
|
220
|
+
let entry = rawServices[i];
|
|
221
|
+
if (typeof entry != "object" || entry === null || Array.isArray(entry)) return { ok: false, reason: "manifest_field_malformed" };
|
|
222
|
+
let s = { ...entry };
|
|
223
|
+
if (!hasOnlyKeys(s, ["ref", "description", "price"])) return { ok: false, reason: "manifest_field_malformed" };
|
|
224
|
+
if (!isNonEmptyString(s.ref, MAX_SERVICE_REF_LENGTH) || typeof s.description != "string") return { ok: false, reason: "manifest_field_malformed" };
|
|
225
|
+
let price = s.price;
|
|
226
|
+
if (typeof price != "object" || price === null || Array.isArray(price)) return { ok: false, reason: "manifest_field_malformed" };
|
|
227
|
+
let p = { ...price };
|
|
228
|
+
if (!hasOnlyKeys(p, ["chain", "asset", "payee_account", "min_amount", "max_amount"])) return { ok: false, reason: "manifest_field_malformed" };
|
|
229
|
+
if (typeof p.chain != "string" || !isCaip2(p.chain)) return { ok: false, reason: "manifest_field_malformed" };
|
|
230
|
+
if (typeof p.asset != "string" || !isCaip19(p.asset)) return { ok: false, reason: "manifest_field_malformed" };
|
|
231
|
+
if (typeof p.payee_account != "string" || !isCaip10(p.payee_account)) return { ok: false, reason: "manifest_field_malformed" };
|
|
232
|
+
if (typeof p.min_amount != "string" || !isPositiveDecimalString(p.min_amount)) return { ok: false, reason: "manifest_field_malformed" };
|
|
233
|
+
if (typeof p.max_amount != "string" || !isPositiveDecimalString(p.max_amount)) return { ok: false, reason: "manifest_field_malformed" };
|
|
234
|
+
services.push({ ref: s.ref, description: s.description, price: { chain: p.chain, asset: p.asset, payee_account: p.payee_account, min_amount: p.min_amount, max_amount: p.max_amount } });
|
|
235
|
+
}
|
|
236
|
+
let signingKey = decodeBase643(r.signing_public_key_base64), derived = null;
|
|
237
|
+
try {
|
|
238
|
+
derived = deriveDidFromSigningKey(signingKey);
|
|
239
|
+
} catch {
|
|
240
|
+
derived = null;
|
|
241
|
+
}
|
|
242
|
+
if (derived === null || derived !== r.provider_did) return { ok: false, reason: "manifest_did_not_derived" };
|
|
243
|
+
let body = { schema: PROVIDER_MANIFEST_SCHEMA, provider_did: r.provider_did, signing_public_key_base64: r.signing_public_key_base64, encryption_public_key_base64: r.encryption_public_key_base64, attestor_public_key_base64: r.attestor_public_key_base64, accept_url: r.accept_url, hire_message_schema: SESSION_HIRE_SCHEMA, worker_base_url: r.worker_base_url, ...hintUrl !== void 0 ? { hint_url: hintUrl } : {}, ...relays !== void 0 ? { relays } : {}, grant_ttl_ms: { min: ttlSnap.min, max: ttlSnap.max }, acceptance_ttl_ms: r.acceptance_ttl_ms, services, payment_buys: "an attempt, not an outcome", notes }, verified = false;
|
|
244
|
+
try {
|
|
245
|
+
verified = nacl2.sign.detached.verify(manifestSigningBytes(body), signature, signingKey);
|
|
246
|
+
} catch {
|
|
247
|
+
verified = false;
|
|
248
|
+
}
|
|
249
|
+
return verified ? expectedProviderDid !== void 0 && expectedProviderDid !== body.provider_did ? { ok: false, reason: "manifest_did_not_pinned" } : { ok: true, manifest: { ...body, signature_base64: r.signature_base64 } } : { ok: false, reason: "manifest_signature_invalid" };
|
|
250
|
+
}
|
|
251
|
+
function verifyProvider(raw, expectedProviderDid) {
|
|
252
|
+
if (typeof expectedProviderDid != "string" || !DID_RE.test(expectedProviderDid)) return { ok: false, reason: "manifest_pin_not_a_did" };
|
|
253
|
+
let verdict = verifyManifest(raw, expectedProviderDid);
|
|
254
|
+
if (!verdict.ok) return { ok: false, reason: verdict.reason };
|
|
255
|
+
let refs = /* @__PURE__ */ new Set();
|
|
256
|
+
for (let offering of verdict.manifest.services) {
|
|
257
|
+
if (refs.has(offering.ref)) return { ok: false, reason: "manifest_service_ref_duplicated" };
|
|
258
|
+
refs.add(offering.ref);
|
|
259
|
+
}
|
|
260
|
+
return { ok: true, provider: mintVerifiedProvider(verdict.manifest) };
|
|
261
|
+
}
|
|
262
|
+
function automaticCanonicalJson(value) {
|
|
263
|
+
if (value === null || typeof value == "string" || typeof value == "boolean" || typeof value == "number" && Number.isSafeInteger(value)) return JSON.stringify(value);
|
|
264
|
+
if (Array.isArray(value)) return `[${value.map(automaticCanonicalJson).join(",")}]`;
|
|
265
|
+
if (value !== null && typeof value == "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
|
266
|
+
let record = value;
|
|
267
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${automaticCanonicalJson(record[key])}`).join(",")}}`;
|
|
268
|
+
}
|
|
269
|
+
throw new TypeError("Unsupported automatic collection JSON value");
|
|
270
|
+
}
|
|
271
|
+
var PROOFS_FIXTURE = { schema: "voidly.session.provider.manifest/v1", provider_did: "did:voidly:E41eeJuRTVxE7411zGGF9H", signing_public_key_base64: "abJvEa/votvZLFZzFKEhwNxyBoIqBRYgvL6mrNysGFI=", encryption_public_key_base64: "CwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCws=", attestor_public_key_base64: "ExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExM=", accept_url: "https://fixture.example.test/session/accept", hire_message_schema: "voidly-session-hire/v1", worker_base_url: "https://fixture.example.test", grant_ttl_ms: { min: 3e5, max: 216e5 }, acceptance_ttl_ms: 3e5, services: [{ ref: "sessions.public.fixture/v1", description: "Offline signature verification fixture only.", price: { chain: "eip155:8453", asset: "eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", payee_account: "eip155:8453:0x2222222222222222222222222222222222222222", min_amount: "1", max_amount: "1" } }], payment_buys: "an attempt, not an outcome", notes: ["Public offline fixture. Never contact these example endpoints or use this manifest for a payment."], signature_base64: "1gE/tewhX6oLaymE2i1w2vrR5gzZv3WTz5EzIECoetx4Dma+aVTAZDyzSLDHeHi/fc57mO7Sd6+3cR/WjfcJDA==" }, PROOFS_FIXTURE_DIGEST = "ccf7933183585166962b3244e19df341b6011895daa4fd43337011bc94b0846a";
|
|
272
|
+
var TITLE = "Your proof artwork", DESCRIPTION = "Code-generated artwork for a saved proof. Decorative, not a signed receipt or evidence of identity, payment, or impact.", PALETTES = [[[184, 166, 237], [237, 227, 206]], [[158, 190, 214], [224, 227, 238]], [[219, 185, 171], [194, 176, 230]]];
|
|
273
|
+
function generator(id) {
|
|
274
|
+
let a = parseInt(id.slice(0, 8), 16) | 0, b = parseInt(id.slice(8, 16), 16) | 0, c = parseInt(id.slice(16, 24), 16) | 0, d = parseInt(id.slice(24, 32), 16) | 0;
|
|
275
|
+
function next() {
|
|
276
|
+
let value = (a + b | 0) + d | 0;
|
|
277
|
+
return d = d + 1 | 0, a = b ^ b >>> 9, b = c + (c << 3) | 0, c = (c << 21 | c >>> 11) + value | 0, (value >>> 0) / 4294967296;
|
|
278
|
+
}
|
|
279
|
+
for (let index = 0; index < 16; index++) next();
|
|
280
|
+
return next;
|
|
281
|
+
}
|
|
282
|
+
function proofArtworkSvg(id) {
|
|
283
|
+
if (typeof id != "string" || !/^[0-9a-f]{32}$/.test(id)) throw new Error("Artwork requires a valid saved proof event ID.");
|
|
284
|
+
let random = generator(id), palette = PALETTES[Math.floor(random() * PALETTES.length)], rotation = random() * Math.PI * 2, phase = random() * Math.PI * 2, lobes = 2 + Math.floor(random() * 4), twist = (random() - 0.5) * 4.8, amplitude = 0.07 + random() * 0.12, stretch = 0.58 + random() * 0.4, driftX = (random() - 0.5) * 54, driftY = (random() - 0.5) * 54, aperture = 5 + random() * 23, power = 0.75 + random() * 0.75, paths = [];
|
|
285
|
+
for (let ring = 0; ring < 38; ring++) {
|
|
286
|
+
let progress = ring / 37, radius = aperture + progress * (102 - aperture), turn = rotation + twist * (1 - progress) ** 2, points = [];
|
|
287
|
+
for (let sample = 0; sample < 112; sample++) {
|
|
288
|
+
let angle = sample / 112 * Math.PI * 2, ripple = 1 + Math.sin(angle * lobes + phase + progress) * amplitude + Math.cos(angle * 2 - phase) * 0.025, cos = Math.cos(angle), sin = Math.sin(angle), x = Math.sign(cos) * Math.abs(cos) ** power * radius * ripple, y = Math.sign(sin) * Math.abs(sin) ** power * radius * ripple * stretch, px = 160 + Math.cos(turn) * x - Math.sin(turn) * y + driftX * (1 - progress), py = 160 + Math.sin(turn) * x + Math.cos(turn) * y + driftY * (1 - progress);
|
|
289
|
+
points.push(`${sample ? "L" : "M"}${px.toFixed(2)} ${py.toFixed(2)}`);
|
|
290
|
+
}
|
|
291
|
+
let color = palette[0].map((value, index) => Math.round(value + (palette[1][index] - value) * progress));
|
|
292
|
+
paths.push(`<path d="${points.join("")}Z" fill="none" stroke="rgb(${color.join(",")})" stroke-width="${ring % 6 === 0 ? 1.4 : 0.9}"/>`);
|
|
293
|
+
}
|
|
294
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" viewBox="0 0 320 320" role="img" aria-label="${TITLE}"><title>${TITLE}</title><desc>${DESCRIPTION}</desc><rect width="320" height="320" fill="#050506"/>${paths.join("")}</svg>`;
|
|
295
|
+
}
|
|
296
|
+
var PUBLIC_EXERCISE_SCHEMA = "voidpay.proof-collection.public-exercise/v3", PUBLIC_RESULT_SCHEMA = "voidpay.proof-collection.public-result/v3", PUBLIC_RESPONSE_SCHEMA = "voidpay.proof-collection.public-response/v3", SESSIONS_PROOFS_TASK = Object.freeze({ id: "sessions-public-check-01", version: 1, acceptance_rule_sha256: "1be9d67c2f8caffa45fd985b7454371838c29e4fadbd8dadc2a4e6663b0f7bc5" }), SESSIONS_PROOFS_PROVIDER = Object.freeze({ provider_did: "did:voidly:6rGTFa5apSnKNF14bGXZfu", manifest_url: "https://intelligence.voidly.ai:8443/.well-known/voidly-session-provider.json", index_url: "https://api.voidly.ai/v1/session/providers" }), PUBLIC_EXERCISE_MAX_BYTES = 8192, PUBLIC_EXERCISE_TTL_MS = 6e5, WRONG_PIN = "did:voidly:2222222222222222", HEX32 = /^[0-9a-f]{32}$/, HEX64 = /^[0-9a-f]{64}$/, PublicExerciseError = class extends Error {
|
|
297
|
+
constructor(code) {
|
|
298
|
+
super(code), this.name = "PublicExerciseError", this.code = code;
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
function requireCondition(value, code) {
|
|
302
|
+
if (!value) throw new PublicExerciseError(code);
|
|
303
|
+
}
|
|
304
|
+
function plain(value) {
|
|
305
|
+
return value !== null && typeof value == "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
306
|
+
}
|
|
307
|
+
function exact(value, keys) {
|
|
308
|
+
return plain(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
309
|
+
}
|
|
310
|
+
function same(value, expected) {
|
|
311
|
+
return exact(value, Object.keys(expected)) && Object.entries(expected).every(([key, field]) => value[key] === field);
|
|
312
|
+
}
|
|
313
|
+
function iso(value) {
|
|
314
|
+
return typeof value == "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
315
|
+
}
|
|
316
|
+
function parsePublicExercise(text, nowMs = Date.now()) {
|
|
317
|
+
requireCondition(typeof text == "string" && new TextEncoder().encode(text).length <= PUBLIC_EXERCISE_MAX_BYTES, "exercise_invalid");
|
|
318
|
+
let input;
|
|
319
|
+
try {
|
|
320
|
+
input = JSON.parse(text);
|
|
321
|
+
} catch {
|
|
322
|
+
throw new PublicExerciseError("exercise_invalid");
|
|
323
|
+
}
|
|
324
|
+
return requireCondition(exact(input, ["schema", "task", "challenge_id", "challenge", "issued_at", "expires_at", "provider"]), "exercise_invalid"), requireCondition(input.schema === PUBLIC_EXERCISE_SCHEMA && same(input.task, SESSIONS_PROOFS_TASK) && same(input.provider, SESSIONS_PROOFS_PROVIDER) && typeof input.challenge_id == "string" && HEX32.test(input.challenge_id) && typeof input.challenge == "string" && HEX64.test(input.challenge) && iso(input.issued_at) && iso(input.expires_at), "exercise_invalid"), requireCondition(Number.isFinite(nowMs) && Date.parse(input.expires_at) - Date.parse(input.issued_at) === PUBLIC_EXERCISE_TTL_MS && Date.parse(input.issued_at) <= nowMs + 3e4, "exercise_invalid"), requireCondition(Date.parse(input.expires_at) > nowMs, "exercise_expired"), { schema: PUBLIC_EXERCISE_SCHEMA, task: SESSIONS_PROOFS_TASK, challenge_id: input.challenge_id, challenge: input.challenge, issued_at: input.issued_at, expires_at: input.expires_at, provider: SESSIONS_PROOFS_PROVIDER };
|
|
325
|
+
}
|
|
326
|
+
async function runSessionsSelfTest() {
|
|
327
|
+
let found = verifyProvider(PROOFS_FIXTURE, PROOFS_FIXTURE.provider_did), wrong = verifyProvider(PROOFS_FIXTURE, WRONG_PIN), altered = verifyProvider({ ...PROOFS_FIXTURE, accept_url: "https://changed.example.test/accept" }, PROOFS_FIXTURE.provider_did), digest = found.ok ? await sha256Hex(canonicalBytes(found.provider.manifest)) : null, checks = { signed_manifest_verified: found.ok, wrong_pin_refused: !wrong.ok && wrong.reason === "manifest_did_not_pinned", changed_manifest_refused: !altered.ok && altered.reason === "manifest_signature_invalid", canonical_digest_matches: digest === PROOFS_FIXTURE_DIGEST };
|
|
328
|
+
return { schema: "voidly.session.self-test/v1", ok: Object.values(checks).every(Boolean), mode: "offline", checks, network_requests: 0, saved_proof: false };
|
|
329
|
+
}
|
|
330
|
+
async function verifyPublicExercise(exercise, index, manifest, nowMs = Date.now()) {
|
|
331
|
+
let parsed = parsePublicExercise(JSON.stringify(exercise), nowMs);
|
|
332
|
+
requireCondition(plain(index) && Array.isArray(index.providers) && index.providers.length <= 1e3 && index.providers.some((entry) => plain(entry) && entry.provider_did === SESSIONS_PROOFS_PROVIDER.provider_did && entry.manifest_url === SESSIONS_PROOFS_PROVIDER.manifest_url), "provider_not_listed");
|
|
333
|
+
let verified = verifyProvider(manifest, SESSIONS_PROOFS_PROVIDER.provider_did);
|
|
334
|
+
requireCondition(verified.ok, "manifest_invalid");
|
|
335
|
+
let wrong = verifyProvider(verified.provider.manifest, WRONG_PIN);
|
|
336
|
+
requireCondition(!wrong.ok && wrong.reason === "manifest_did_not_pinned", "wrong_pin_not_refused");
|
|
337
|
+
let manifest_digest_sha256 = await sha256Hex(canonicalBytes(verified.provider.manifest)), challenge_response_sha256 = await sha256Hex(canonicalBytes({ schema: PUBLIC_RESPONSE_SCHEMA, task: parsed.task, challenge_id: parsed.challenge_id, challenge: parsed.challenge, manifest_digest_sha256 }));
|
|
338
|
+
return { schema: PUBLIC_RESULT_SCHEMA, task: parsed.task, challenge_id: parsed.challenge_id, challenge: parsed.challenge, manifest_digest_sha256, challenge_response_sha256, checks: { provider_pair_listed: true, manifest_verified: true, wrong_pin_refused: true } };
|
|
339
|
+
}
|
|
340
|
+
async function readFixedJson(url, maxBytes, fetchImpl) {
|
|
341
|
+
let controller = new AbortController(), timer, reader, response, deadlineAt = performance.now() + 8e3, deadline = new Promise((_, reject) => {
|
|
342
|
+
timer = setTimeout(() => {
|
|
343
|
+
controller.abort(), reject(new PublicExerciseError("upstream_unavailable"));
|
|
344
|
+
}, 8e3);
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
let fetched = Promise.resolve(fetchImpl(url, { method: "GET", headers: { accept: "application/json" }, redirect: "error", credentials: "omit", cache: "no-store", referrerPolicy: "no-referrer", signal: controller.signal })).then((value) => {
|
|
348
|
+
if (controller.signal.aborted) throw value.body && !value.body.locked && value.body.cancel().catch(() => {
|
|
349
|
+
}), new PublicExerciseError("upstream_unavailable");
|
|
350
|
+
return value;
|
|
351
|
+
});
|
|
352
|
+
response = await Promise.race([fetched, deadline]), requireCondition(performance.now() < deadlineAt, "upstream_unavailable"), requireCondition(!response.redirected && (response.url === "" || response.url === url), "upstream_redirect"), requireCondition(response.ok && response.body, "upstream_unavailable"), requireCondition(/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") ?? ""), "upstream_not_json");
|
|
353
|
+
let contentLength = response.headers.get("content-length");
|
|
354
|
+
requireCondition(contentLength === null || /^\d+$/.test(contentLength) && Number(contentLength) <= maxBytes, "upstream_too_large"), reader = response.body.getReader();
|
|
355
|
+
let chunks = [], size = 0, reads = 0, emptyReads = 0;
|
|
356
|
+
for (; ; ) {
|
|
357
|
+
requireCondition(performance.now() < deadlineAt, "upstream_unavailable"), requireCondition(++reads <= 2048, "upstream_too_large");
|
|
358
|
+
let part = await Promise.race([reader.read(), deadline]);
|
|
359
|
+
if (requireCondition(performance.now() < deadlineAt, "upstream_unavailable"), part.done) break;
|
|
360
|
+
if (part.value.byteLength === 0) {
|
|
361
|
+
requireCondition(++emptyReads <= 128, "upstream_too_large");
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
size += part.value.byteLength, requireCondition(size <= maxBytes, "upstream_too_large"), chunks.push(part.value);
|
|
365
|
+
}
|
|
366
|
+
let bytes = new Uint8Array(size), offset = 0;
|
|
367
|
+
for (let chunk of chunks) bytes.set(chunk, offset), offset += chunk.byteLength;
|
|
368
|
+
try {
|
|
369
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
370
|
+
} catch {
|
|
371
|
+
throw new PublicExerciseError("upstream_not_json");
|
|
372
|
+
}
|
|
373
|
+
} catch (error) {
|
|
374
|
+
throw error instanceof PublicExerciseError ? error : new PublicExerciseError("upstream_unavailable");
|
|
375
|
+
} finally {
|
|
376
|
+
clearTimeout(timer), controller.abort(), reader ? (reader.cancel().catch(() => {
|
|
377
|
+
}), reader.releaseLock()) : response?.body && !response.body.locked && response.body.cancel().catch(() => {
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function runPublicExercise(text, options = {}) {
|
|
382
|
+
let nowMs = options.nowMs ?? Date.now(), exercise = parsePublicExercise(text, nowMs), fetchImpl = options.fetchImpl ?? fetch, index = await readFixedJson(SESSIONS_PROOFS_PROVIDER.index_url, 262144, fetchImpl), manifest = await readFixedJson(SESSIONS_PROOFS_PROVIDER.manifest_url, 65536, fetchImpl);
|
|
383
|
+
return verifyPublicExercise(exercise, index, manifest, options.nowMs ?? Date.now());
|
|
384
|
+
}
|
|
385
|
+
var AUTOMATIC_HANDOFF_SCHEMA = "voidpay.proof-collection.agent-handoff/v4", AUTOMATIC_TASK = Object.freeze({ id: "sessions-auto-save-01", version: 1, acceptance_rule_sha256: "b58e042e088740892856e96f4c8eee079bd94819c518e8e486ef250184ffb230" }), AUTOMATIC_BASE = "https://api.voidly.ai/v4/proofs/collections", AUTOMATIC_ISSUER = "https://api.voidly.ai/v2/proofs/collections/issuer", AUTOMATIC_RECEIPT_DOMAIN = `voidpay-proof-collection-receipt/v4
|
|
386
|
+
`, AUTOMATIC_RUN_BINDING_DOMAIN = `voidpay-proof-collection-run-binding/v4
|
|
387
|
+
`, AUTOMATIC_SCOPE = "public-provider-discovery-and-owner-preauthorized-private-save-only", AUTOMATIC_HANDOFF_MAX_BYTES = 1024, AUTOMATIC_BOUNDARY = Object.freeze({ decision: "STOP", wallet_required: false, payment_action_allowed: false, authorization_created: false, transaction_broadcast: false, settlement_observed: false, reward_offered: false, reward_amount: "0", reward_asset: null, customer_identity_verified: false, commercial_transaction: false, credited_to_traction_metrics: false, unique_person_verified: false, impact_verified: false }), SERVER_CHECKS = ["owner_preauthorized", "challenge_unexpired", "provider_pair_listed", "manifest_verified", "wrong_pin_refused"], CLAIMANT_CHECKS = ["manifest_digest_matches", "challenge_response_matches"], HEX322 = /^[0-9a-f]{32}$/, HEX642 = /^[0-9a-f]{64}$/, RETENTION_MS = 15552e6, MAX_RESPONSE_BYTES = 32768, MAX_ATTEMPTS = 3, AutomaticProofError = class extends Error {
|
|
388
|
+
constructor(code) {
|
|
389
|
+
super(code);
|
|
390
|
+
this.code = code;
|
|
391
|
+
this.name = "AutomaticProofError";
|
|
392
|
+
}
|
|
393
|
+
}, AutomaticHttpError = class extends AutomaticProofError {
|
|
394
|
+
constructor(code, status) {
|
|
395
|
+
super(code);
|
|
396
|
+
this.status = status;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
function requireCondition2(value, code) {
|
|
400
|
+
if (!value) throw new AutomaticProofError(code);
|
|
401
|
+
}
|
|
402
|
+
function exact2(value, keys) {
|
|
403
|
+
return value !== null && typeof value == "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null) && Object.keys(value).length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
404
|
+
}
|
|
405
|
+
function same2(value, expected) {
|
|
406
|
+
return exact2(value, Object.keys(expected)) && JSON.stringify(value, Object.keys(expected).sort()) === JSON.stringify(expected, Object.keys(expected).sort());
|
|
407
|
+
}
|
|
408
|
+
function iso2(value) {
|
|
409
|
+
return typeof value == "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
410
|
+
}
|
|
411
|
+
function unhex(value) {
|
|
412
|
+
return Uint8Array.from(value.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));
|
|
413
|
+
}
|
|
414
|
+
function parseAutomaticHandoff(text) {
|
|
415
|
+
requireCondition2(typeof text == "string" && new TextEncoder().encode(text).length <= AUTOMATIC_HANDOFF_MAX_BYTES, "handoff_invalid");
|
|
416
|
+
let value;
|
|
417
|
+
try {
|
|
418
|
+
value = JSON.parse(text);
|
|
419
|
+
} catch {
|
|
420
|
+
throw new AutomaticProofError("handoff_invalid");
|
|
421
|
+
}
|
|
422
|
+
return requireCondition2(exact2(value, ["schema", "run_id", "completion_capability"]) && value.schema === AUTOMATIC_HANDOFF_SCHEMA && typeof value.run_id == "string" && HEX322.test(value.run_id) && typeof value.completion_capability == "string" && HEX642.test(value.completion_capability), "handoff_invalid"), { schema: AUTOMATIC_HANDOFF_SCHEMA, run_id: value.run_id, completion_capability: value.completion_capability };
|
|
423
|
+
}
|
|
424
|
+
async function requestJson(kind, handoff, body, fetchImpl) {
|
|
425
|
+
let url = kind === "issuer" ? AUTOMATIC_ISSUER : `${AUTOMATIC_BASE}/${kind}`, controller = new AbortController(), timer, reader, response, deadlineAt = performance.now() + (kind === "submit" ? 2e4 : 1e4), deadline = new Promise((_, reject) => {
|
|
426
|
+
timer = setTimeout(() => {
|
|
427
|
+
controller.abort(), reject(new AutomaticProofError("completion_unavailable"));
|
|
428
|
+
}, kind === "submit" ? 2e4 : 1e4);
|
|
429
|
+
});
|
|
430
|
+
try {
|
|
431
|
+
let headers = { accept: "application/json" };
|
|
432
|
+
kind !== "issuer" && (headers["content-type"] = "application/json", headers.authorization = `Bearer ${handoff.completion_capability}`);
|
|
433
|
+
let fetched = Promise.resolve(fetchImpl(url, { method: kind === "issuer" ? "GET" : "POST", headers, ...kind === "issuer" ? {} : { body: JSON.stringify(body) }, redirect: "error", credentials: "omit", cache: "no-store", referrerPolicy: "no-referrer", signal: controller.signal })).then((value2) => {
|
|
434
|
+
if (controller.signal.aborted) throw value2.body && !value2.body.locked && value2.body.cancel().catch(() => {
|
|
435
|
+
}), new AutomaticProofError("completion_unavailable");
|
|
436
|
+
return value2;
|
|
437
|
+
});
|
|
438
|
+
response = await Promise.race([fetched, deadline]), requireCondition2(performance.now() < deadlineAt, "completion_unavailable"), requireCondition2(!response.redirected && response.type !== "opaqueredirect" && (response.url === "" || response.url === url) && response.status !== 0 && !(response.status >= 300 && response.status < 400), "response_invalid"), requireCondition2(/^application\/json(?:\s*;|$)/i.test(response.headers.get("content-type") ?? "") && response.body, "response_invalid");
|
|
439
|
+
let length = response.headers.get("content-length");
|
|
440
|
+
requireCondition2(length === null || /^\d+$/.test(length) && Number(length) <= MAX_RESPONSE_BYTES, "response_invalid"), reader = response.body.getReader();
|
|
441
|
+
let chunks = [], size = 0, reads = 0, emptyReads = 0;
|
|
442
|
+
for (; ; ) {
|
|
443
|
+
requireCondition2(performance.now() < deadlineAt, "completion_unavailable"), requireCondition2(++reads <= 2048, "response_invalid");
|
|
444
|
+
let next = await Promise.race([reader.read(), deadline]);
|
|
445
|
+
if (requireCondition2(performance.now() < deadlineAt, "completion_unavailable"), next.done) break;
|
|
446
|
+
if (next.value.byteLength === 0) {
|
|
447
|
+
requireCondition2(++emptyReads <= 128, "response_invalid");
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
size += next.value.byteLength, requireCondition2(size <= MAX_RESPONSE_BYTES, "response_invalid"), chunks.push(next.value);
|
|
451
|
+
}
|
|
452
|
+
let bytes = new Uint8Array(size), offset = 0;
|
|
453
|
+
for (let chunk of chunks) bytes.set(chunk, offset), offset += chunk.byteLength;
|
|
454
|
+
let value;
|
|
455
|
+
try {
|
|
456
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
457
|
+
} catch {
|
|
458
|
+
throw new AutomaticProofError("response_invalid");
|
|
459
|
+
}
|
|
460
|
+
if (requireCondition2(!JSON.stringify(value).includes(handoff.completion_capability), "response_invalid"), !response.ok) {
|
|
461
|
+
requireCondition2(exact2(value, ["schema", "ok", "error"]) && value.schema === "voidpay.proof-collection.error/v4" && value.ok === false && exact2(value.error, ["code"]) && typeof value.error.code == "string", "response_invalid");
|
|
462
|
+
let code = response.status === 401 || response.status === 403 ? "permission_refused" : response.status === 404 || response.status === 410 ? "run_expired_or_revoked" : response.status === 429 ? value.error.code === "verifier_attempts_exhausted" ? "attempts_exhausted" : "rate_limited" : "completion_unavailable";
|
|
463
|
+
throw new AutomaticHttpError(code, response.status);
|
|
464
|
+
}
|
|
465
|
+
return value;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
throw error instanceof AutomaticProofError ? error : new AutomaticProofError("completion_unavailable");
|
|
468
|
+
} finally {
|
|
469
|
+
clearTimeout(timer), controller.abort(), reader ? (reader.cancel().catch(() => {
|
|
470
|
+
}), reader.releaseLock()) : response?.body && !response.body.locked && response.body.cancel().catch(() => {
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function issuerKeys(handoff, fetchImpl) {
|
|
475
|
+
let value = await requestJson("issuer", handoff, void 0, fetchImpl);
|
|
476
|
+
requireCondition2(exact2(value, ["schema", "issuer", "keys", "unknown_key_policy"]) && value.schema === "voidpay.proof-collection.issuer/v2" && value.issuer === AUTOMATIC_ISSUER && value.unknown_key_policy === "untrusted; never trust an embedded receipt key" && Array.isArray(value.keys) && value.keys.length > 0 && value.keys.length <= 8, "issuer_invalid");
|
|
477
|
+
let keys = /* @__PURE__ */ new Map();
|
|
478
|
+
for (let key of value.keys) requireCondition2(exact2(key, ["issuer", "key_id", "algorithm", "public_key_hex", "status"]) && key.issuer === AUTOMATIC_ISSUER && key.algorithm === "Ed25519" && key.status === "active" && typeof key.key_id == "string" && HEX642.test(key.key_id) && typeof key.public_key_hex == "string" && HEX642.test(key.public_key_hex) && !keys.has(key.key_id) && await sha256Hex(unhex(key.public_key_hex)) === key.key_id, "issuer_invalid"), keys.set(key.key_id, key.public_key_hex);
|
|
479
|
+
return keys;
|
|
480
|
+
}
|
|
481
|
+
async function verifyComplete(value, handoff, fetchImpl, now) {
|
|
482
|
+
requireCondition2(exact2(value, ["schema", "ok", "status", "run_id", "exercise", "record", "already_completed"]) && value.schema === "voidpay.proof-collection.result/v4" && value.ok === true && value.status === "complete" && value.run_id === handoff.run_id && typeof value.already_completed == "boolean", "response_invalid");
|
|
483
|
+
let record = value.record;
|
|
484
|
+
requireCondition2(exact2(record, ["event_id", "visibility", "public_url", "expires_at", "receipt"]) && typeof record.event_id == "string" && HEX322.test(record.event_id) && iso2(record.expires_at) && (record.visibility === "private" || record.visibility === "public") && (value.already_completed || record.visibility === "private") && record.public_url === (record.visibility === "private" ? null : `https://api.voidly.ai/v2/proofs/collections/public/${record.event_id}`) && exact2(record.receipt, ["statement", "signature_hex"]), "receipt_invalid");
|
|
485
|
+
let receipt = record.receipt, s = receipt.statement;
|
|
486
|
+
requireCondition2(exact2(s, ["schema", "issuer", "key_id", "event_id", "run_binding_sha256", "exercise_binding_sha256", "artwork", "task", "computation_task", "checked_at", "completed_at", "outcome", "checks", "provider", "scope", "boundary"]) && s.schema === "voidpay.proof-collection.receipt/v4" && s.issuer === AUTOMATIC_ISSUER && s.event_id === record.event_id && typeof s.key_id == "string" && HEX642.test(s.key_id) && typeof s.run_binding_sha256 == "string" && HEX642.test(s.run_binding_sha256) && typeof s.exercise_binding_sha256 == "string" && HEX642.test(s.exercise_binding_sha256) && typeof receipt.signature_hex == "string" && /^[0-9a-f]{128}$/.test(receipt.signature_hex) && same2(s.task, AUTOMATIC_TASK) && same2(s.computation_task, SESSIONS_PROOFS_TASK) && s.scope === AUTOMATIC_SCOPE && same2(s.boundary, AUTOMATIC_BOUNDARY) && s.outcome === "PASS" && iso2(s.checked_at) && iso2(s.completed_at) && s.checked_at <= s.completed_at && Date.parse(s.completed_at) <= now + 3e4 && Date.parse(record.expires_at) === Date.parse(s.completed_at) + RETENTION_MS && Date.parse(record.expires_at) > now && same2(s.provider, { provider_did: SESSIONS_PROOFS_PROVIDER.provider_did, manifest_url: SESSIONS_PROOFS_PROVIDER.manifest_url }) && same2(s.artwork, { recipe: "contours-v1", seed: record.event_id }) && exact2(s.checks, ["server", "claimant"]) && exact2(s.checks.server, SERVER_CHECKS) && exact2(s.checks.claimant, CLAIMANT_CHECKS) && [...Object.values(s.checks.server), ...Object.values(s.checks.claimant)].every((check) => check === true), "receipt_invalid");
|
|
487
|
+
let exercise;
|
|
488
|
+
try {
|
|
489
|
+
exercise = parsePublicExercise(JSON.stringify(value.exercise), Date.parse(s.checked_at));
|
|
490
|
+
} catch {
|
|
491
|
+
throw new AutomaticProofError("receipt_invalid");
|
|
492
|
+
}
|
|
493
|
+
requireCondition2(exercise.challenge_id === handoff.run_id && Date.parse(exercise.issued_at) <= Date.parse(s.checked_at) && Date.parse(exercise.expires_at) > Date.parse(s.completed_at) && await sha256Hex(new TextEncoder().encode(AUTOMATIC_RUN_BINDING_DOMAIN + handoff.run_id)) === s.run_binding_sha256 && await sha256Hex(new TextEncoder().encode(automaticCanonicalJson(exercise))) === s.exercise_binding_sha256, "receipt_invalid");
|
|
494
|
+
let key = (await issuerKeys(handoff, fetchImpl)).get(s.key_id);
|
|
495
|
+
requireCondition2(key && nacl3.sign.detached.verify(new TextEncoder().encode(AUTOMATIC_RECEIPT_DOMAIN + automaticCanonicalJson(s)), unhex(receipt.signature_hex), unhex(key)), "receipt_invalid");
|
|
496
|
+
let svg = proofArtworkSvg(record.event_id), result = { schema: "voidly.session.automatic-proof/v1", ok: true, saved_proof: true, event_id: record.event_id, visibility: record.visibility, already_completed: value.already_completed, receipt: JSON.parse(JSON.stringify(receipt)), artwork: { recipe: "contours-v1", seed: record.event_id, svg, sha256: await sha256Hex(new TextEncoder().encode(svg)) }, return_url: "https://voidly.ai/pay/proofs", payment_boundary: "STOP" };
|
|
497
|
+
return requireCondition2(!JSON.stringify(result).includes(handoff.completion_capability), "response_invalid"), result;
|
|
498
|
+
}
|
|
499
|
+
function pending(value, runId) {
|
|
500
|
+
return exact2(value, ["schema", "ok", "status", "run_id", "record"]) && value.schema === "voidpay.proof-collection.result/v4" && value.ok === true && value.status === "pending" && value.run_id === runId && value.record === null;
|
|
501
|
+
}
|
|
502
|
+
function transient(error) {
|
|
503
|
+
return error instanceof AutomaticHttpError ? error.status === 409 || error.status >= 500 : error instanceof AutomaticProofError && error.code === "completion_unavailable";
|
|
504
|
+
}
|
|
505
|
+
async function completeAutomaticProof(text, options = {}) {
|
|
506
|
+
let handoff = parseAutomaticHandoff(text), fetchImpl = options.fetchImpl ?? fetch, now = () => options.nowMs ?? Date.now();
|
|
507
|
+
requireCondition2((await runSessionsSelfTest()).ok, "self_test_failed");
|
|
508
|
+
let mayHaveSaved = false;
|
|
509
|
+
try {
|
|
510
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) try {
|
|
511
|
+
let existing = await requestJson("result", handoff, { run_id: handoff.run_id }, fetchImpl);
|
|
512
|
+
if (!pending(existing, handoff.run_id)) return await verifyComplete(existing, handoff, fetchImpl, now());
|
|
513
|
+
let fresh = await requestJson("exercise", handoff, { run_id: handoff.run_id }, fetchImpl);
|
|
514
|
+
if (exact2(fresh, ["schema", "ok", "status", "run_id", "exercise", "record", "already_completed"])) return await verifyComplete(fresh, handoff, fetchImpl, now());
|
|
515
|
+
requireCondition2(exact2(fresh, ["schema", "ok", "action", "run_id", "exercise"]) && fresh.schema === "voidpay.proof-collection.response/v4" && fresh.ok === true && fresh.action === "exercise" && fresh.run_id === handoff.run_id, "response_invalid");
|
|
516
|
+
let exercise = parsePublicExercise(JSON.stringify(fresh.exercise), now());
|
|
517
|
+
requireCondition2(exercise.challenge_id === handoff.run_id, "response_invalid");
|
|
518
|
+
let result = await runPublicExercise(JSON.stringify(exercise), { fetchImpl, nowMs: options.nowMs });
|
|
519
|
+
mayHaveSaved = true;
|
|
520
|
+
let completed = await requestJson("submit", handoff, { run_id: handoff.run_id, result }, fetchImpl);
|
|
521
|
+
return await verifyComplete(completed, handoff, fetchImpl, now());
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error instanceof PublicExerciseError) {
|
|
524
|
+
if (error.code !== "upstream_unavailable") throw new AutomaticProofError("provider_check_failed");
|
|
525
|
+
error = new AutomaticProofError("completion_unavailable");
|
|
526
|
+
}
|
|
527
|
+
if (!transient(error)) throw error;
|
|
528
|
+
attempt < MAX_ATTEMPTS - 1 && await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
529
|
+
}
|
|
530
|
+
if (mayHaveSaved) try {
|
|
531
|
+
let existing = await requestJson("result", handoff, { run_id: handoff.run_id }, fetchImpl);
|
|
532
|
+
if (!pending(existing, handoff.run_id)) return await verifyComplete(existing, handoff, fetchImpl, now());
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (!transient(error)) throw error;
|
|
535
|
+
}
|
|
536
|
+
throw new AutomaticProofError(mayHaveSaved ? "completion_uncertain" : "completion_unavailable");
|
|
537
|
+
} finally {
|
|
538
|
+
handoff.completion_capability = "";
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
export {
|
|
542
|
+
AUTOMATIC_BASE,
|
|
543
|
+
AUTOMATIC_BOUNDARY,
|
|
544
|
+
AUTOMATIC_HANDOFF_MAX_BYTES,
|
|
545
|
+
AUTOMATIC_HANDOFF_SCHEMA,
|
|
546
|
+
AUTOMATIC_ISSUER,
|
|
547
|
+
AUTOMATIC_RECEIPT_DOMAIN,
|
|
548
|
+
AUTOMATIC_RUN_BINDING_DOMAIN,
|
|
549
|
+
AUTOMATIC_SCOPE,
|
|
550
|
+
AUTOMATIC_TASK,
|
|
551
|
+
AutomaticProofError,
|
|
552
|
+
completeAutomaticProof,
|
|
553
|
+
parseAutomaticHandoff
|
|
554
|
+
};
|