@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
package/dist/proofs.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @voidly/session — GENERATED by scripts/build-types.mjs. Do not edit.
|
|
2
|
+
//
|
|
3
|
+
// The transitive closure of what src/proofs.ts exports, flattened into one
|
|
4
|
+
// module. Server declarations are absent because nothing here reaches them.
|
|
5
|
+
|
|
6
|
+
export declare function parsePublicExercise(text: string, nowMs?: number): PublicExercise;
|
|
7
|
+
export declare function proofArtworkSvg(id: string): string;
|
|
8
|
+
export declare const PUBLIC_EXERCISE_MAX_BYTES = 8192;
|
|
9
|
+
export declare const PUBLIC_EXERCISE_SCHEMA: "voidpay.proof-collection.public-exercise/v3";
|
|
10
|
+
export declare const PUBLIC_EXERCISE_TTL_MS = 600000;
|
|
11
|
+
export declare const PUBLIC_RESPONSE_SCHEMA: "voidpay.proof-collection.public-response/v3";
|
|
12
|
+
export declare const PUBLIC_RESULT_SCHEMA: "voidpay.proof-collection.public-result/v3";
|
|
13
|
+
export interface PublicExercise {
|
|
14
|
+
readonly schema: typeof PUBLIC_EXERCISE_SCHEMA;
|
|
15
|
+
readonly task: typeof SESSIONS_PROOFS_TASK;
|
|
16
|
+
readonly challenge_id: string;
|
|
17
|
+
readonly challenge: string;
|
|
18
|
+
readonly issued_at: string;
|
|
19
|
+
readonly expires_at: string;
|
|
20
|
+
readonly provider: typeof SESSIONS_PROOFS_PROVIDER;
|
|
21
|
+
}
|
|
22
|
+
export declare class PublicExerciseError extends Error {
|
|
23
|
+
readonly code: PublicExerciseRefusal;
|
|
24
|
+
constructor(code: PublicExerciseRefusal);
|
|
25
|
+
}
|
|
26
|
+
export type PublicExerciseRefusal = "exercise_invalid" | "exercise_expired" | "provider_not_listed" | "manifest_invalid" | "wrong_pin_not_refused" | "upstream_unavailable" | "upstream_redirect" | "upstream_too_large" | "upstream_not_json";
|
|
27
|
+
export interface PublicExerciseResult {
|
|
28
|
+
readonly schema: typeof PUBLIC_RESULT_SCHEMA;
|
|
29
|
+
readonly task: typeof SESSIONS_PROOFS_TASK;
|
|
30
|
+
readonly challenge_id: string;
|
|
31
|
+
readonly challenge: string;
|
|
32
|
+
readonly manifest_digest_sha256: string;
|
|
33
|
+
readonly challenge_response_sha256: string;
|
|
34
|
+
readonly checks: {
|
|
35
|
+
readonly provider_pair_listed: true;
|
|
36
|
+
readonly manifest_verified: true;
|
|
37
|
+
readonly wrong_pin_refused: true;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export declare function runPublicExercise(text: string, options?: {
|
|
41
|
+
readonly fetchImpl?: typeof fetch;
|
|
42
|
+
readonly nowMs?: number;
|
|
43
|
+
}): Promise<PublicExerciseResult>;
|
|
44
|
+
export declare function runSessionsSelfTest(): Promise<{
|
|
45
|
+
schema: "voidly.session.self-test/v1";
|
|
46
|
+
ok: boolean;
|
|
47
|
+
mode: "offline";
|
|
48
|
+
checks: {
|
|
49
|
+
signed_manifest_verified: boolean;
|
|
50
|
+
wrong_pin_refused: boolean;
|
|
51
|
+
changed_manifest_refused: boolean;
|
|
52
|
+
canonical_digest_matches: boolean;
|
|
53
|
+
};
|
|
54
|
+
network_requests: 0;
|
|
55
|
+
saved_proof: false;
|
|
56
|
+
}>;
|
|
57
|
+
export declare const SESSIONS_PROOFS_PROVIDER: Readonly<{
|
|
58
|
+
provider_did: "did:voidly:6rGTFa5apSnKNF14bGXZfu";
|
|
59
|
+
manifest_url: "https://intelligence.voidly.ai:8443/.well-known/voidly-session-provider.json";
|
|
60
|
+
index_url: "https://api.voidly.ai/v1/session/providers";
|
|
61
|
+
}>;
|
|
62
|
+
export declare const SESSIONS_PROOFS_TASK: Readonly<{
|
|
63
|
+
id: "sessions-public-check-01";
|
|
64
|
+
version: 1;
|
|
65
|
+
acceptance_rule_sha256: "1be9d67c2f8caffa45fd985b7454371838c29e4fadbd8dadc2a4e6663b0f7bc5";
|
|
66
|
+
}>;
|
|
67
|
+
export declare function verifyPublicExercise(exercise: PublicExercise, index: unknown, manifest: unknown, nowMs?: number): Promise<PublicExerciseResult>;
|
package/dist/proofs.mjs
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import nacl from "tweetnacl";
|
|
2
|
+
import __naclUtil0 from "tweetnacl-util";
|
|
3
|
+
const { decodeBase64 } = __naclUtil0;
|
|
4
|
+
var MAX_WINDOW_MS = 3600 * 1e3, MAX_CLOCK_SKEW_MS = 30 * 1e3;
|
|
5
|
+
function canonicalize(value) {
|
|
6
|
+
if (value == null) return "null";
|
|
7
|
+
if (typeof value == "boolean") return value ? "true" : "false";
|
|
8
|
+
if (typeof value == "number") {
|
|
9
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) throw new Error("canonicalize: only finite integers supported");
|
|
10
|
+
return value.toString(10);
|
|
11
|
+
}
|
|
12
|
+
if (typeof value == "bigint") return value.toString(10);
|
|
13
|
+
if (typeof value == "string") return JSON.stringify(value);
|
|
14
|
+
if (Array.isArray(value)) return "[" + value.map(canonicalize).join(",") + "]";
|
|
15
|
+
if (typeof value == "object") {
|
|
16
|
+
let obj = value;
|
|
17
|
+
return "{" + Object.keys(obj).filter((k) => obj[k] !== null && obj[k] !== void 0).sort().map((k) => JSON.stringify(k) + ":" + canonicalize(obj[k])).join(",") + "}";
|
|
18
|
+
}
|
|
19
|
+
throw new Error(`canonicalize: unsupported type ${typeof value}`);
|
|
20
|
+
}
|
|
21
|
+
function canonicalBytes(value) {
|
|
22
|
+
return new TextEncoder().encode(canonicalize(value));
|
|
23
|
+
}
|
|
24
|
+
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
25
|
+
function toBase58(bytes) {
|
|
26
|
+
let result = "", num = BigInt("0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join(""));
|
|
27
|
+
for (; num > 0n; ) {
|
|
28
|
+
let remainder = num % 58n;
|
|
29
|
+
num = num / 58n, result = BASE58_ALPHABET[Number(remainder)] + result;
|
|
30
|
+
}
|
|
31
|
+
for (let byte of bytes) if (byte === 0) result = "1" + result;
|
|
32
|
+
else break;
|
|
33
|
+
return result || "1";
|
|
34
|
+
}
|
|
35
|
+
function deriveDidFromSigningKey(signingPublicKey) {
|
|
36
|
+
return `did:voidly:${toBase58(signingPublicKey.slice(0, 16))}`;
|
|
37
|
+
}
|
|
38
|
+
async function sha256Bytes(bytes) {
|
|
39
|
+
let buf = await crypto.subtle.digest("SHA-256", bytes);
|
|
40
|
+
return new Uint8Array(buf);
|
|
41
|
+
}
|
|
42
|
+
async function sha256Hex(bytes) {
|
|
43
|
+
let digest = await sha256Bytes(bytes);
|
|
44
|
+
return Array.from(digest).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
45
|
+
}
|
|
46
|
+
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;
|
|
47
|
+
var SECRETBOX_TAG_BYTES = 16, CAPSULE_SALT_BYTES = 32;
|
|
48
|
+
function base64Length(bytes) {
|
|
49
|
+
return 4 * Math.ceil(bytes / 3);
|
|
50
|
+
}
|
|
51
|
+
var SESSION_MAX_BODY_BYTES = 32 * 1024, SESSION_WORST_CASE_ENVELOPE_BYTES = 4972, MAX_FRAME_BUCKET_BYTES = (() => {
|
|
52
|
+
let best = 0;
|
|
53
|
+
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);
|
|
54
|
+
if (best === 0) throw new Error("wireBudget: the transport ceiling cannot carry the minimum frame bucket");
|
|
55
|
+
return best;
|
|
56
|
+
})(), 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;
|
|
57
|
+
var TASK_BRIEF_SCHEMA = "voidly-task-brief/v1";
|
|
58
|
+
var TASK_RESULT_SCHEMA = "voidly-task-result/v1";
|
|
59
|
+
var MAX_OFFER_TTL_MS = 1440 * 60 * 1e3, MAX_GRANT_TTL_MS = 1440 * 60 * 1e3;
|
|
60
|
+
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;
|
|
61
|
+
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"));
|
|
62
|
+
var MAX_RECOVERY_TTL_MS = 10080 * 60 * 1e3;
|
|
63
|
+
var DID_RE = /^did:voidly:[A-Za-z0-9._-]{1,64}$/;
|
|
64
|
+
function hasOnlyKeys(raw, allowed) {
|
|
65
|
+
for (let k of Object.keys(raw)) if (!allowed.includes(k)) return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
function isBase64Key32(value, decode) {
|
|
69
|
+
if (typeof value != "string" || value.length === 0) return false;
|
|
70
|
+
try {
|
|
71
|
+
return decode(value).length === 32;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
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}$/;
|
|
77
|
+
function isCaip2(s) {
|
|
78
|
+
return typeof s == "string" && CAIP2_RE.test(s);
|
|
79
|
+
}
|
|
80
|
+
function isCaip10(s) {
|
|
81
|
+
return typeof s == "string" && CAIP10_RE.test(s);
|
|
82
|
+
}
|
|
83
|
+
function isCaip19(s) {
|
|
84
|
+
return typeof s == "string" && CAIP19_RE.test(s);
|
|
85
|
+
}
|
|
86
|
+
function isPositiveDecimalString(s) {
|
|
87
|
+
return typeof s == "string" && POSITIVE_DECIMAL_RE.test(s);
|
|
88
|
+
}
|
|
89
|
+
import __naclUtil1 from "tweetnacl-util";
|
|
90
|
+
const { decodeBase64: decodeBase642 } = __naclUtil1;
|
|
91
|
+
var SESSION_HIRE_SCHEMA = "voidly-session-hire/v1";
|
|
92
|
+
var AUTHORIZATION_ENTRY_POINTS = ["transfer_with_authorization", "receive_with_authorization"];
|
|
93
|
+
import nacl2 from "tweetnacl";
|
|
94
|
+
import __naclUtil2 from "tweetnacl-util";
|
|
95
|
+
const { decodeBase64: decodeBase643 } = __naclUtil2;
|
|
96
|
+
var MINTED = /* @__PURE__ */ new WeakMap();
|
|
97
|
+
function containerCopy(value) {
|
|
98
|
+
if (value === null || typeof value != "object") return null;
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
let source = value, sink = [];
|
|
101
|
+
for (let key of Object.keys(source)) sink[key] = source[key];
|
|
102
|
+
return sink;
|
|
103
|
+
}
|
|
104
|
+
return { ...value };
|
|
105
|
+
}
|
|
106
|
+
function rebuildDocumentDeep(document) {
|
|
107
|
+
let top = containerCopy(document);
|
|
108
|
+
if (top === null) return document;
|
|
109
|
+
let made = /* @__PURE__ */ new Map();
|
|
110
|
+
made.set(document, top);
|
|
111
|
+
let pending = [top];
|
|
112
|
+
for (; pending.length > 0; ) {
|
|
113
|
+
let frame = pending.pop();
|
|
114
|
+
if (frame === void 0) break;
|
|
115
|
+
for (let key of Object.keys(frame)) {
|
|
116
|
+
let child = frame[key];
|
|
117
|
+
if (child === null || typeof child != "object") continue;
|
|
118
|
+
let already = made.get(child);
|
|
119
|
+
if (already !== void 0) {
|
|
120
|
+
frame[key] = already;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
let copy = containerCopy(child);
|
|
124
|
+
copy !== null && (made.set(child, copy), frame[key] = copy, pending.push(copy));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return top;
|
|
128
|
+
}
|
|
129
|
+
function freezeManifestDeep(m) {
|
|
130
|
+
let seen = /* @__PURE__ */ new Set(), pending = [m];
|
|
131
|
+
for (; pending.length > 0; ) {
|
|
132
|
+
let node = pending.pop();
|
|
133
|
+
if (node === null || typeof node != "object") continue;
|
|
134
|
+
let container = node;
|
|
135
|
+
if (!seen.has(container)) {
|
|
136
|
+
seen.add(container), Object.freeze(container);
|
|
137
|
+
for (let key of Reflect.ownKeys(container)) {
|
|
138
|
+
if (typeof key == "symbol") continue;
|
|
139
|
+
let d = Object.getOwnPropertyDescriptor(container, key);
|
|
140
|
+
d !== void 0 && "value" in d && pending.push(d.value);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return m;
|
|
145
|
+
}
|
|
146
|
+
function mintVerifiedProvider(manifest) {
|
|
147
|
+
let document = freezeManifestDeep(rebuildDocumentDeep(manifest)), provider = Object.freeze({ manifest: document });
|
|
148
|
+
return MINTED.set(provider, document), provider;
|
|
149
|
+
}
|
|
150
|
+
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"];
|
|
151
|
+
function manifestSigningBytes(m) {
|
|
152
|
+
return canonicalBytes(m);
|
|
153
|
+
}
|
|
154
|
+
function isNonEmptyString(v, max = 2048) {
|
|
155
|
+
return typeof v == "string" && v.length > 0 && v.length <= max;
|
|
156
|
+
}
|
|
157
|
+
function isPositiveInt(v) {
|
|
158
|
+
return typeof v == "number" && Number.isInteger(v) && v > 0;
|
|
159
|
+
}
|
|
160
|
+
function verifyManifest(raw, expectedProviderDid) {
|
|
161
|
+
if (typeof raw != "object" || raw === null || Array.isArray(raw)) return { ok: false, reason: "manifest_not_object" };
|
|
162
|
+
let r = { ...raw };
|
|
163
|
+
if (r.schema !== PROVIDER_MANIFEST_SCHEMA) return { ok: false, reason: "manifest_schema_mismatch" };
|
|
164
|
+
if (!hasOnlyKeys(r, PROVIDER_MANIFEST_KEYS)) return { ok: false, reason: "manifest_unexpected_field" };
|
|
165
|
+
if (r.signature_base64 === void 0 || r.signature_base64 === null) return { ok: false, reason: "manifest_signature_missing" };
|
|
166
|
+
if (typeof r.signature_base64 != "string" || r.signature_base64.length === 0) return { ok: false, reason: "manifest_signature_malformed" };
|
|
167
|
+
let signature;
|
|
168
|
+
try {
|
|
169
|
+
signature = decodeBase643(r.signature_base64);
|
|
170
|
+
} catch {
|
|
171
|
+
return { ok: false, reason: "manifest_signature_malformed" };
|
|
172
|
+
}
|
|
173
|
+
if (signature.length !== nacl2.sign.signatureLength) return { ok: false, reason: "manifest_signature_malformed" };
|
|
174
|
+
if (!isBase64Key32(r.signing_public_key_base64, decodeBase643)) return { ok: false, reason: "manifest_field_malformed" };
|
|
175
|
+
if (!isBase64Key32(r.encryption_public_key_base64, decodeBase643)) return { ok: false, reason: "manifest_field_malformed" };
|
|
176
|
+
if (!isBase64Key32(r.attestor_public_key_base64, decodeBase643)) return { ok: false, reason: "manifest_field_malformed" };
|
|
177
|
+
if (typeof r.provider_did != "string" || !DID_RE.test(r.provider_did)) return { ok: false, reason: "manifest_field_malformed" };
|
|
178
|
+
if (!isNonEmptyString(r.accept_url) || !isNonEmptyString(r.worker_base_url)) return { ok: false, reason: "manifest_field_malformed" };
|
|
179
|
+
let rawHintUrl = r.hint_url;
|
|
180
|
+
if (rawHintUrl !== void 0 && !isNonEmptyString(rawHintUrl)) return { ok: false, reason: "manifest_field_malformed" };
|
|
181
|
+
let hintUrl = rawHintUrl === void 0 ? void 0 : rawHintUrl, rawRelays = r.relays, relays;
|
|
182
|
+
if (rawRelays !== void 0) {
|
|
183
|
+
if (typeof rawRelays != "object" || rawRelays === null || Array.isArray(rawRelays)) return { ok: false, reason: "manifest_field_malformed" };
|
|
184
|
+
let rel = { ...rawRelays };
|
|
185
|
+
if (!hasOnlyKeys(rel, ["provider_submits", "accepted_entry_points"])) return { ok: false, reason: "manifest_field_malformed" };
|
|
186
|
+
if (typeof rel.provider_submits != "boolean") return { ok: false, reason: "manifest_field_malformed" };
|
|
187
|
+
let rawDoors = rel.accepted_entry_points;
|
|
188
|
+
if (!Array.isArray(rawDoors) || rawDoors.length === 0 || rawDoors.length > AUTHORIZATION_ENTRY_POINTS.length) return { ok: false, reason: "manifest_field_malformed" };
|
|
189
|
+
let doors = [];
|
|
190
|
+
for (let i = 0; i < rawDoors.length; i += 1) {
|
|
191
|
+
let door = rawDoors[i];
|
|
192
|
+
if (typeof door != "string" || !AUTHORIZATION_ENTRY_POINTS.includes(door)) return { ok: false, reason: "manifest_field_malformed" };
|
|
193
|
+
if (doors.includes(door)) return { ok: false, reason: "manifest_field_malformed" };
|
|
194
|
+
doors.push(door);
|
|
195
|
+
}
|
|
196
|
+
relays = { provider_submits: rel.provider_submits, accepted_entry_points: doors };
|
|
197
|
+
}
|
|
198
|
+
if (r.hire_message_schema !== SESSION_HIRE_SCHEMA) return { ok: false, reason: "manifest_field_malformed" };
|
|
199
|
+
if (r.payment_buys !== "an attempt, not an outcome") return { ok: false, reason: "manifest_field_malformed" };
|
|
200
|
+
if (!isPositiveInt(r.acceptance_ttl_ms)) return { ok: false, reason: "manifest_field_malformed" };
|
|
201
|
+
let ttl = r.grant_ttl_ms;
|
|
202
|
+
if (typeof ttl != "object" || ttl === null || Array.isArray(ttl)) return { ok: false, reason: "manifest_field_malformed" };
|
|
203
|
+
let ttlSnap = { ...ttl };
|
|
204
|
+
if (!hasOnlyKeys(ttlSnap, ["min", "max"])) return { ok: false, reason: "manifest_field_malformed" };
|
|
205
|
+
if (!isPositiveInt(ttlSnap.min) || !isPositiveInt(ttlSnap.max)) return { ok: false, reason: "manifest_field_malformed" };
|
|
206
|
+
if (ttlSnap.min > ttlSnap.max || ttlSnap.max > MAX_GRANT_TTL_MS) return { ok: false, reason: "manifest_field_malformed" };
|
|
207
|
+
let rawNotes = r.notes;
|
|
208
|
+
if (!Array.isArray(rawNotes)) return { ok: false, reason: "manifest_field_malformed" };
|
|
209
|
+
let notes = [];
|
|
210
|
+
for (let i = 0; i < rawNotes.length; i += 1) {
|
|
211
|
+
let note = rawNotes[i];
|
|
212
|
+
if (!isNonEmptyString(note, 4096)) return { ok: false, reason: "manifest_field_malformed" };
|
|
213
|
+
notes.push(note);
|
|
214
|
+
}
|
|
215
|
+
let rawServices = r.services;
|
|
216
|
+
if (!Array.isArray(rawServices) || rawServices.length === 0) return { ok: false, reason: "manifest_field_malformed" };
|
|
217
|
+
let services = [];
|
|
218
|
+
for (let i = 0; i < rawServices.length; i += 1) {
|
|
219
|
+
let entry = rawServices[i];
|
|
220
|
+
if (typeof entry != "object" || entry === null || Array.isArray(entry)) return { ok: false, reason: "manifest_field_malformed" };
|
|
221
|
+
let s = { ...entry };
|
|
222
|
+
if (!hasOnlyKeys(s, ["ref", "description", "price"])) return { ok: false, reason: "manifest_field_malformed" };
|
|
223
|
+
if (!isNonEmptyString(s.ref, MAX_SERVICE_REF_LENGTH) || typeof s.description != "string") return { ok: false, reason: "manifest_field_malformed" };
|
|
224
|
+
let price = s.price;
|
|
225
|
+
if (typeof price != "object" || price === null || Array.isArray(price)) return { ok: false, reason: "manifest_field_malformed" };
|
|
226
|
+
let p = { ...price };
|
|
227
|
+
if (!hasOnlyKeys(p, ["chain", "asset", "payee_account", "min_amount", "max_amount"])) return { ok: false, reason: "manifest_field_malformed" };
|
|
228
|
+
if (typeof p.chain != "string" || !isCaip2(p.chain)) return { ok: false, reason: "manifest_field_malformed" };
|
|
229
|
+
if (typeof p.asset != "string" || !isCaip19(p.asset)) return { ok: false, reason: "manifest_field_malformed" };
|
|
230
|
+
if (typeof p.payee_account != "string" || !isCaip10(p.payee_account)) return { ok: false, reason: "manifest_field_malformed" };
|
|
231
|
+
if (typeof p.min_amount != "string" || !isPositiveDecimalString(p.min_amount)) return { ok: false, reason: "manifest_field_malformed" };
|
|
232
|
+
if (typeof p.max_amount != "string" || !isPositiveDecimalString(p.max_amount)) return { ok: false, reason: "manifest_field_malformed" };
|
|
233
|
+
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 } });
|
|
234
|
+
}
|
|
235
|
+
let signingKey = decodeBase643(r.signing_public_key_base64), derived = null;
|
|
236
|
+
try {
|
|
237
|
+
derived = deriveDidFromSigningKey(signingKey);
|
|
238
|
+
} catch {
|
|
239
|
+
derived = null;
|
|
240
|
+
}
|
|
241
|
+
if (derived === null || derived !== r.provider_did) return { ok: false, reason: "manifest_did_not_derived" };
|
|
242
|
+
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;
|
|
243
|
+
try {
|
|
244
|
+
verified = nacl2.sign.detached.verify(manifestSigningBytes(body), signature, signingKey);
|
|
245
|
+
} catch {
|
|
246
|
+
verified = false;
|
|
247
|
+
}
|
|
248
|
+
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" };
|
|
249
|
+
}
|
|
250
|
+
function verifyProvider(raw, expectedProviderDid) {
|
|
251
|
+
if (typeof expectedProviderDid != "string" || !DID_RE.test(expectedProviderDid)) return { ok: false, reason: "manifest_pin_not_a_did" };
|
|
252
|
+
let verdict = verifyManifest(raw, expectedProviderDid);
|
|
253
|
+
if (!verdict.ok) return { ok: false, reason: verdict.reason };
|
|
254
|
+
let refs = /* @__PURE__ */ new Set();
|
|
255
|
+
for (let offering of verdict.manifest.services) {
|
|
256
|
+
if (refs.has(offering.ref)) return { ok: false, reason: "manifest_service_ref_duplicated" };
|
|
257
|
+
refs.add(offering.ref);
|
|
258
|
+
}
|
|
259
|
+
return { ok: true, provider: mintVerifiedProvider(verdict.manifest) };
|
|
260
|
+
}
|
|
261
|
+
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";
|
|
262
|
+
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]]];
|
|
263
|
+
function generator(id) {
|
|
264
|
+
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;
|
|
265
|
+
function next() {
|
|
266
|
+
let value = (a + b | 0) + d | 0;
|
|
267
|
+
return d = d + 1 | 0, a = b ^ b >>> 9, b = c + (c << 3) | 0, c = (c << 21 | c >>> 11) + value | 0, (value >>> 0) / 4294967296;
|
|
268
|
+
}
|
|
269
|
+
for (let index = 0; index < 16; index++) next();
|
|
270
|
+
return next;
|
|
271
|
+
}
|
|
272
|
+
function proofArtworkSvg(id) {
|
|
273
|
+
if (typeof id != "string" || !/^[0-9a-f]{32}$/.test(id)) throw new Error("Artwork requires a valid saved proof event ID.");
|
|
274
|
+
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 = [];
|
|
275
|
+
for (let ring = 0; ring < 38; ring++) {
|
|
276
|
+
let progress = ring / 37, radius = aperture + progress * (102 - aperture), turn = rotation + twist * (1 - progress) ** 2, points = [];
|
|
277
|
+
for (let sample = 0; sample < 112; sample++) {
|
|
278
|
+
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);
|
|
279
|
+
points.push(`${sample ? "L" : "M"}${px.toFixed(2)} ${py.toFixed(2)}`);
|
|
280
|
+
}
|
|
281
|
+
let color = palette[0].map((value, index) => Math.round(value + (palette[1][index] - value) * progress));
|
|
282
|
+
paths.push(`<path d="${points.join("")}Z" fill="none" stroke="rgb(${color.join(",")})" stroke-width="${ring % 6 === 0 ? 1.4 : 0.9}"/>`);
|
|
283
|
+
}
|
|
284
|
+
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>`;
|
|
285
|
+
}
|
|
286
|
+
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 {
|
|
287
|
+
constructor(code) {
|
|
288
|
+
super(code), this.name = "PublicExerciseError", this.code = code;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
function requireCondition(value, code) {
|
|
292
|
+
if (!value) throw new PublicExerciseError(code);
|
|
293
|
+
}
|
|
294
|
+
function plain(value) {
|
|
295
|
+
return value !== null && typeof value == "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
296
|
+
}
|
|
297
|
+
function exact(value, keys) {
|
|
298
|
+
return plain(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
299
|
+
}
|
|
300
|
+
function same(value, expected) {
|
|
301
|
+
return exact(value, Object.keys(expected)) && Object.entries(expected).every(([key, field]) => value[key] === field);
|
|
302
|
+
}
|
|
303
|
+
function iso(value) {
|
|
304
|
+
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;
|
|
305
|
+
}
|
|
306
|
+
function parsePublicExercise(text, nowMs = Date.now()) {
|
|
307
|
+
requireCondition(typeof text == "string" && new TextEncoder().encode(text).length <= PUBLIC_EXERCISE_MAX_BYTES, "exercise_invalid");
|
|
308
|
+
let input;
|
|
309
|
+
try {
|
|
310
|
+
input = JSON.parse(text);
|
|
311
|
+
} catch {
|
|
312
|
+
throw new PublicExerciseError("exercise_invalid");
|
|
313
|
+
}
|
|
314
|
+
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 };
|
|
315
|
+
}
|
|
316
|
+
async function runSessionsSelfTest() {
|
|
317
|
+
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 };
|
|
318
|
+
return { schema: "voidly.session.self-test/v1", ok: Object.values(checks).every(Boolean), mode: "offline", checks, network_requests: 0, saved_proof: false };
|
|
319
|
+
}
|
|
320
|
+
async function verifyPublicExercise(exercise, index, manifest, nowMs = Date.now()) {
|
|
321
|
+
let parsed = parsePublicExercise(JSON.stringify(exercise), nowMs);
|
|
322
|
+
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");
|
|
323
|
+
let verified = verifyProvider(manifest, SESSIONS_PROOFS_PROVIDER.provider_did);
|
|
324
|
+
requireCondition(verified.ok, "manifest_invalid");
|
|
325
|
+
let wrong = verifyProvider(verified.provider.manifest, WRONG_PIN);
|
|
326
|
+
requireCondition(!wrong.ok && wrong.reason === "manifest_did_not_pinned", "wrong_pin_not_refused");
|
|
327
|
+
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 }));
|
|
328
|
+
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 } };
|
|
329
|
+
}
|
|
330
|
+
async function readFixedJson(url, maxBytes, fetchImpl) {
|
|
331
|
+
let controller = new AbortController(), timer, reader, deadline = new Promise((_, reject) => {
|
|
332
|
+
timer = setTimeout(() => {
|
|
333
|
+
controller.abort(), reject(new PublicExerciseError("upstream_unavailable"));
|
|
334
|
+
}, 8e3);
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
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]);
|
|
338
|
+
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");
|
|
339
|
+
let contentLength = response.headers.get("content-length");
|
|
340
|
+
requireCondition(contentLength === null || /^\d+$/.test(contentLength) && Number(contentLength) <= maxBytes, "upstream_too_large"), reader = response.body.getReader();
|
|
341
|
+
let chunks = [], size = 0;
|
|
342
|
+
for (; ; ) {
|
|
343
|
+
let part = await Promise.race([reader.read(), deadline]);
|
|
344
|
+
if (part.done) break;
|
|
345
|
+
size += part.value.byteLength, requireCondition(size <= maxBytes, "upstream_too_large"), chunks.push(part.value);
|
|
346
|
+
}
|
|
347
|
+
let bytes = new Uint8Array(size), offset = 0;
|
|
348
|
+
for (let chunk of chunks) bytes.set(chunk, offset), offset += chunk.byteLength;
|
|
349
|
+
try {
|
|
350
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
351
|
+
} catch {
|
|
352
|
+
throw new PublicExerciseError("upstream_not_json");
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw error instanceof PublicExerciseError ? error : new PublicExerciseError("upstream_unavailable");
|
|
356
|
+
} finally {
|
|
357
|
+
clearTimeout(timer), controller.abort(), reader && (reader.cancel().catch(() => {
|
|
358
|
+
}), reader.releaseLock());
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function runPublicExercise(text, options = {}) {
|
|
362
|
+
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);
|
|
363
|
+
return verifyPublicExercise(exercise, index, manifest, options.nowMs ?? Date.now());
|
|
364
|
+
}
|
|
365
|
+
export {
|
|
366
|
+
PUBLIC_EXERCISE_MAX_BYTES,
|
|
367
|
+
PUBLIC_EXERCISE_SCHEMA,
|
|
368
|
+
PUBLIC_EXERCISE_TTL_MS,
|
|
369
|
+
PUBLIC_RESPONSE_SCHEMA,
|
|
370
|
+
PUBLIC_RESULT_SCHEMA,
|
|
371
|
+
PublicExerciseError,
|
|
372
|
+
SESSIONS_PROOFS_PROVIDER,
|
|
373
|
+
SESSIONS_PROOFS_TASK,
|
|
374
|
+
parsePublicExercise,
|
|
375
|
+
proofArtworkSvg,
|
|
376
|
+
runPublicExercise,
|
|
377
|
+
runSessionsSelfTest,
|
|
378
|
+
verifyPublicExercise
|
|
379
|
+
};
|