@voidly/session 1.0.0 → 1.1.0
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 +196 -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 +379 -0
- package/dist/proofsCli.mjs +417 -0
- package/package.json +28 -4
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
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 pending = [top];
|
|
113
|
+
for (; pending.length > 0; ) {
|
|
114
|
+
let frame = pending.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, pending.push(copy));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return top;
|
|
129
|
+
}
|
|
130
|
+
function freezeManifestDeep(m) {
|
|
131
|
+
let seen = /* @__PURE__ */ new Set(), pending = [m];
|
|
132
|
+
for (; pending.length > 0; ) {
|
|
133
|
+
let node = pending.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 && pending.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
|
+
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";
|
|
263
|
+
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]]];
|
|
264
|
+
function generator(id) {
|
|
265
|
+
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;
|
|
266
|
+
function next() {
|
|
267
|
+
let value = (a + b | 0) + d | 0;
|
|
268
|
+
return d = d + 1 | 0, a = b ^ b >>> 9, b = c + (c << 3) | 0, c = (c << 21 | c >>> 11) + value | 0, (value >>> 0) / 4294967296;
|
|
269
|
+
}
|
|
270
|
+
for (let index = 0; index < 16; index++) next();
|
|
271
|
+
return next;
|
|
272
|
+
}
|
|
273
|
+
function proofArtworkSvg(id) {
|
|
274
|
+
if (typeof id != "string" || !/^[0-9a-f]{32}$/.test(id)) throw new Error("Artwork requires a valid saved proof event ID.");
|
|
275
|
+
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 = [];
|
|
276
|
+
for (let ring = 0; ring < 38; ring++) {
|
|
277
|
+
let progress = ring / 37, radius = aperture + progress * (102 - aperture), turn = rotation + twist * (1 - progress) ** 2, points = [];
|
|
278
|
+
for (let sample = 0; sample < 112; sample++) {
|
|
279
|
+
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);
|
|
280
|
+
points.push(`${sample ? "L" : "M"}${px.toFixed(2)} ${py.toFixed(2)}`);
|
|
281
|
+
}
|
|
282
|
+
let color = palette[0].map((value, index) => Math.round(value + (palette[1][index] - value) * progress));
|
|
283
|
+
paths.push(`<path d="${points.join("")}Z" fill="none" stroke="rgb(${color.join(",")})" stroke-width="${ring % 6 === 0 ? 1.4 : 0.9}"/>`);
|
|
284
|
+
}
|
|
285
|
+
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>`;
|
|
286
|
+
}
|
|
287
|
+
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 {
|
|
288
|
+
constructor(code) {
|
|
289
|
+
super(code), this.name = "PublicExerciseError", this.code = code;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
function requireCondition(value, code) {
|
|
293
|
+
if (!value) throw new PublicExerciseError(code);
|
|
294
|
+
}
|
|
295
|
+
function plain(value) {
|
|
296
|
+
return value !== null && typeof value == "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
297
|
+
}
|
|
298
|
+
function exact(value, keys) {
|
|
299
|
+
return plain(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
300
|
+
}
|
|
301
|
+
function same(value, expected) {
|
|
302
|
+
return exact(value, Object.keys(expected)) && Object.entries(expected).every(([key, field]) => value[key] === field);
|
|
303
|
+
}
|
|
304
|
+
function iso(value) {
|
|
305
|
+
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;
|
|
306
|
+
}
|
|
307
|
+
function parsePublicExercise(text, nowMs = Date.now()) {
|
|
308
|
+
requireCondition(typeof text == "string" && new TextEncoder().encode(text).length <= PUBLIC_EXERCISE_MAX_BYTES, "exercise_invalid");
|
|
309
|
+
let input2;
|
|
310
|
+
try {
|
|
311
|
+
input2 = JSON.parse(text);
|
|
312
|
+
} catch {
|
|
313
|
+
throw new PublicExerciseError("exercise_invalid");
|
|
314
|
+
}
|
|
315
|
+
return requireCondition(exact(input2, ["schema", "task", "challenge_id", "challenge", "issued_at", "expires_at", "provider"]), "exercise_invalid"), requireCondition(input2.schema === PUBLIC_EXERCISE_SCHEMA && same(input2.task, SESSIONS_PROOFS_TASK) && same(input2.provider, SESSIONS_PROOFS_PROVIDER) && typeof input2.challenge_id == "string" && HEX32.test(input2.challenge_id) && typeof input2.challenge == "string" && HEX64.test(input2.challenge) && iso(input2.issued_at) && iso(input2.expires_at), "exercise_invalid"), requireCondition(Number.isFinite(nowMs) && Date.parse(input2.expires_at) - Date.parse(input2.issued_at) === PUBLIC_EXERCISE_TTL_MS && Date.parse(input2.issued_at) <= nowMs + 3e4, "exercise_invalid"), requireCondition(Date.parse(input2.expires_at) > nowMs, "exercise_expired"), { schema: PUBLIC_EXERCISE_SCHEMA, task: SESSIONS_PROOFS_TASK, challenge_id: input2.challenge_id, challenge: input2.challenge, issued_at: input2.issued_at, expires_at: input2.expires_at, provider: SESSIONS_PROOFS_PROVIDER };
|
|
316
|
+
}
|
|
317
|
+
async function runSessionsSelfTest() {
|
|
318
|
+
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 };
|
|
319
|
+
return { schema: "voidly.session.self-test/v1", ok: Object.values(checks).every(Boolean), mode: "offline", checks, network_requests: 0, saved_proof: false };
|
|
320
|
+
}
|
|
321
|
+
async function verifyPublicExercise(exercise, index, manifest, nowMs = Date.now()) {
|
|
322
|
+
let parsed = parsePublicExercise(JSON.stringify(exercise), nowMs);
|
|
323
|
+
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");
|
|
324
|
+
let verified = verifyProvider(manifest, SESSIONS_PROOFS_PROVIDER.provider_did);
|
|
325
|
+
requireCondition(verified.ok, "manifest_invalid");
|
|
326
|
+
let wrong = verifyProvider(verified.provider.manifest, WRONG_PIN);
|
|
327
|
+
requireCondition(!wrong.ok && wrong.reason === "manifest_did_not_pinned", "wrong_pin_not_refused");
|
|
328
|
+
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 }));
|
|
329
|
+
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 } };
|
|
330
|
+
}
|
|
331
|
+
async function readFixedJson(url, maxBytes, fetchImpl) {
|
|
332
|
+
let controller = new AbortController(), timer, reader, deadline = new Promise((_, reject) => {
|
|
333
|
+
timer = setTimeout(() => {
|
|
334
|
+
controller.abort(), reject(new PublicExerciseError("upstream_unavailable"));
|
|
335
|
+
}, 8e3);
|
|
336
|
+
});
|
|
337
|
+
try {
|
|
338
|
+
let response = await Promise.race([fetchImpl(url, { method: "GET", headers: { accept: "application/json" }, redirect: "error", credentials: "omit", cache: "no-store", referrerPolicy: "no-referrer", signal: controller.signal }), deadline]);
|
|
339
|
+
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");
|
|
340
|
+
let contentLength = response.headers.get("content-length");
|
|
341
|
+
requireCondition(contentLength === null || /^\d+$/.test(contentLength) && Number(contentLength) <= maxBytes, "upstream_too_large"), reader = response.body.getReader();
|
|
342
|
+
let chunks = [], size = 0;
|
|
343
|
+
for (; ; ) {
|
|
344
|
+
let part = await Promise.race([reader.read(), deadline]);
|
|
345
|
+
if (part.done) break;
|
|
346
|
+
size += part.value.byteLength, requireCondition(size <= maxBytes, "upstream_too_large"), chunks.push(part.value);
|
|
347
|
+
}
|
|
348
|
+
let bytes = new Uint8Array(size), offset = 0;
|
|
349
|
+
for (let chunk of chunks) bytes.set(chunk, offset), offset += chunk.byteLength;
|
|
350
|
+
try {
|
|
351
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
352
|
+
} catch {
|
|
353
|
+
throw new PublicExerciseError("upstream_not_json");
|
|
354
|
+
}
|
|
355
|
+
} catch (error) {
|
|
356
|
+
throw error instanceof PublicExerciseError ? error : new PublicExerciseError("upstream_unavailable");
|
|
357
|
+
} finally {
|
|
358
|
+
clearTimeout(timer), controller.abort(), reader && (reader.cancel().catch(() => {
|
|
359
|
+
}), reader.releaseLock());
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
async function runPublicExercise(text, options = {}) {
|
|
363
|
+
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);
|
|
364
|
+
return verifyPublicExercise(exercise, index, manifest, options.nowMs ?? Date.now());
|
|
365
|
+
}
|
|
366
|
+
async function input() {
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
let chunks = [], size = 0, cleanup = () => {
|
|
369
|
+
clearTimeout(timer), process.stdin.removeListener("data", onData), process.stdin.removeListener("end", onEnd), process.stdin.removeListener("error", onError), process.stdin.pause();
|
|
370
|
+
}, fail = () => {
|
|
371
|
+
cleanup(), reject(new Error("input_invalid"));
|
|
372
|
+
}, onData = (chunk) => {
|
|
373
|
+
size += chunk.byteLength, size > PUBLIC_EXERCISE_MAX_BYTES ? fail() : chunks.push(chunk);
|
|
374
|
+
}, onEnd = () => {
|
|
375
|
+
cleanup();
|
|
376
|
+
try {
|
|
377
|
+
resolve(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
|
|
378
|
+
} catch {
|
|
379
|
+
reject(new Error("input_invalid"));
|
|
380
|
+
}
|
|
381
|
+
}, onError = () => fail(), timer = setTimeout(fail, 3e4);
|
|
382
|
+
process.stdin.on("data", onData), process.stdin.once("end", onEnd), process.stdin.once("error", onError);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async function main() {
|
|
386
|
+
let [major, minor] = process.versions.node.split(".").map(Number);
|
|
387
|
+
if (major < 20 || major === 20 && minor < 3) return process.stderr.write(`This CLI requires Node.js 20.3 or newer. No check was run.
|
|
388
|
+
`), 1;
|
|
389
|
+
let args = process.argv.slice(2);
|
|
390
|
+
if (args.length === 2 && args[0] === "artwork" && /^[0-9a-f]{32}$/.test(args[1])) return process.stdout.write(proofArtworkSvg(args[1]) + `
|
|
391
|
+
`), process.stderr.write(`Decorative artwork only. The supplied identifier was not checked with a server.
|
|
392
|
+
`), 0;
|
|
393
|
+
if (args.length !== 1 || !["self-test", "public-check", "--help"].includes(args[0])) return process.stderr.write(`Usage: voidly-session self-test | public-check | artwork <saved-event-id> | --help
|
|
394
|
+
`), 1;
|
|
395
|
+
if (args[0] === "--help") return process.stdout.write(`self-test: offline SDK verification; no network.
|
|
396
|
+
public-check: public exercise JSON on stdin; two fixed public GETs; result JSON on stdout.
|
|
397
|
+
artwork <saved-event-id>: decorative SVG on stdout, generated locally; no savedness verification.
|
|
398
|
+
These commands do not save proofs, publish or perform payment. Review the installed package before running it.
|
|
399
|
+
`), 0;
|
|
400
|
+
try {
|
|
401
|
+
if (args[0] === "self-test") {
|
|
402
|
+
let result2 = await runSessionsSelfTest();
|
|
403
|
+
return process.stdout.write(JSON.stringify(result2, null, 2) + `
|
|
404
|
+
`), result2.ok ? 0 : 3;
|
|
405
|
+
}
|
|
406
|
+
let result = await runPublicExercise(await input());
|
|
407
|
+
return process.stdout.write(JSON.stringify(result, null, 2) + `
|
|
408
|
+
`), process.stderr.write(`Public check complete. Import this JSON at https://voidly.ai/pay/proofs and choose Save proof. Nothing has been saved or published by this command.
|
|
409
|
+
`), 0;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
return process.stderr.write(`Check stopped: ${error instanceof PublicExerciseError ? error.code : "check_failed"}. No proof was saved.
|
|
412
|
+
`), 1;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
main().then((code) => {
|
|
416
|
+
process.exitCode = code;
|
|
417
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidly/session",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "The voidpay session client. Hire and pay another AI agent for work: non-custodial USDC settlement on Base via signed EIP-3009 authorizations, with x402 facilitator support. The rail never holds, custodies or submits funds — it only verifies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent-payments",
|
|
7
|
+
"agent-to-agent",
|
|
8
|
+
"ai-agents",
|
|
9
|
+
"autonomous-agents",
|
|
10
|
+
"machine-payments",
|
|
11
|
+
"eip-3009",
|
|
12
|
+
"usdc",
|
|
13
|
+
"stablecoin",
|
|
14
|
+
"base",
|
|
15
|
+
"non-custodial",
|
|
16
|
+
"micropayments",
|
|
17
|
+
"pay-per-call",
|
|
18
|
+
"did",
|
|
19
|
+
"voidly"
|
|
20
|
+
],
|
|
5
21
|
"type": "module",
|
|
6
22
|
"repository": {
|
|
7
23
|
"type": "git",
|
|
@@ -25,8 +41,16 @@
|
|
|
25
41
|
"import": "./dist/breakEven.mjs",
|
|
26
42
|
"default": "./dist/breakEven.mjs"
|
|
27
43
|
},
|
|
44
|
+
"./proofs": {
|
|
45
|
+
"types": "./dist/proofs.d.ts",
|
|
46
|
+
"import": "./dist/proofs.mjs",
|
|
47
|
+
"default": "./dist/proofs.mjs"
|
|
48
|
+
},
|
|
28
49
|
"./package.json": "./package.json"
|
|
29
50
|
},
|
|
51
|
+
"bin": {
|
|
52
|
+
"voidly-session": "./dist/proofsCli.mjs"
|
|
53
|
+
},
|
|
30
54
|
"files": [
|
|
31
55
|
"dist",
|
|
32
56
|
"README.md",
|
|
@@ -45,8 +69,8 @@
|
|
|
45
69
|
"dry-run": "node scripts/dry-run-base-mainnet-payment.mjs"
|
|
46
70
|
},
|
|
47
71
|
"dependencies": {
|
|
48
|
-
"tweetnacl": "
|
|
49
|
-
"tweetnacl-util": "
|
|
72
|
+
"tweetnacl": "1.0.3",
|
|
73
|
+
"tweetnacl-util": "0.15.1"
|
|
50
74
|
},
|
|
51
75
|
"devDependencies": {
|
|
52
76
|
"@types/node": "^20.10.0",
|