@voidly/session 1.1.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 +48 -6
- package/dist/proofs.mjs +17 -7
- package/dist/proofsAuto.d.ts +111 -0
- package/dist/proofsAuto.mjs +554 -0
- package/dist/proofsCli.mjs +232 -19
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -21,13 +21,13 @@ is not in this package.
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm install --ignore-scripts --save-exact @voidly/session@1.1
|
|
24
|
+
npm install --ignore-scripts --save-exact @voidly/session@1.2.1
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
The package name is public. Check the exact version's registry metadata and its
|
|
28
28
|
linked public source before installation; a source checkout does not prove that
|
|
29
|
-
version has been published. If
|
|
30
|
-
|
|
29
|
+
version has been published. If the requested version is unavailable, stop rather
|
|
30
|
+
than substituting another version. Locally reviewed tarballs are for development.
|
|
31
31
|
|
|
32
32
|
ESM only. Node ≥ 18 or any runtime with WebCrypto, `fetch` and `TextEncoder`.
|
|
33
33
|
Two exact runtime dependencies: `tweetnacl@1.0.3` and `tweetnacl-util@0.15.1`.
|
|
@@ -38,8 +38,8 @@ explicitly execute after installation.
|
|
|
38
38
|
> **Building from a checkout instead?** Pack it yourself:
|
|
39
39
|
>
|
|
40
40
|
> ```bash
|
|
41
|
-
> npm run build && npm pack # → voidly-session-1.1.
|
|
42
|
-
> npm install --ignore-scripts --save-exact /path/to/voidly-session-1.1.
|
|
41
|
+
> npm run build && npm pack # → voidly-session-1.2.1.tgz
|
|
42
|
+
> npm install --ignore-scripts --save-exact /path/to/voidly-session-1.2.1.tgz
|
|
43
43
|
> ```
|
|
44
44
|
>
|
|
45
45
|
> `npm run gate` inspects the actual packed bytes. A local build is not registry
|
|
@@ -48,7 +48,49 @@ explicitly execute after installation.
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
-
##
|
|
51
|
+
## One instruction: complete and save a private proof
|
|
52
|
+
|
|
53
|
+
Open [Voidpay Proofs](https://voidly.ai/pay/proofs), authorize one private result,
|
|
54
|
+
and copy its agent instruction. After reviewing and installing this exact package,
|
|
55
|
+
the agent runs one command with the small handoff supplied through standard input:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
node node_modules/@voidly/session/dist/proofsCli.mjs complete
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The command performs the offline self-test, obtains fresh public exercise data,
|
|
62
|
+
checks the provider, submits the result, verifies its signed receipt and generates
|
|
63
|
+
the exact saved event's artwork locally. It creates new `proof-artwork.svg` and
|
|
64
|
+
`proof-receipt.json` files inside a unique private directory in the current
|
|
65
|
+
workspace. Its compact JSON output includes absolute paths; if export fails it
|
|
66
|
+
includes the SVG itself, so the agent can display and attach the art. A failed local export does not undo a saved
|
|
67
|
+
proof: use the returned SVG instead of creating a replacement run. PNG conversion
|
|
68
|
+
is optional with an already available local converter, never an online service.
|
|
69
|
+
|
|
70
|
+
The handoff contains a **limited completion credential**. Anyone holding it can
|
|
71
|
+
finish that one approved proof and retrieve its result during the allowed window.
|
|
72
|
+
It cannot list, manage, publish or delete your collection, access a wallet, or pay.
|
|
73
|
+
Supply it only through a real subprocess standard-input channel—not arguments,
|
|
74
|
+
environment variables, URLs, npm commands, shell history or a temporary input file.
|
|
75
|
+
The CLI sends it only to three fixed first-party completion endpoints. Provider
|
|
76
|
+
and issuer requests receive no credential. It does not read ambient credentials.
|
|
77
|
+
|
|
78
|
+
The browser may close after copying succeeds. Authorization lasts one hour; first
|
|
79
|
+
activation must happen within 50 minutes to leave the full ten-minute exercise
|
|
80
|
+
window. Successful results can be recovered with the same handoff for one hour
|
|
81
|
+
after completion. Retry the **same instruction** after an uncertain response.
|
|
82
|
+
At most three server-verifier attempts are allowed; expiry, cancellation or
|
|
83
|
+
exhaustion requires a fresh approval on the website. The saved record remains
|
|
84
|
+
available in the owner's collection for its stated retention period.
|
|
85
|
+
|
|
86
|
+
Nothing is published by this command. Return to the website to share or join the
|
|
87
|
+
board explicitly. A previously published record can be recovered without changing
|
|
88
|
+
its visibility. This is participation, not remote proof of package installation,
|
|
89
|
+
agent identity, a unique person, payment, earnings or impact. The installation may
|
|
90
|
+
be temporary in a hosted agent workspace. Respect the agent's execution policy;
|
|
91
|
+
some environments cannot install packages, supply safe stdin or display SVG files.
|
|
92
|
+
|
|
93
|
+
## Credential-free SDK exercises (unchanged)
|
|
52
94
|
|
|
53
95
|
The website flow is released separately from this package. Follow the current
|
|
54
96
|
page's availability state; installing the SDK alone does not enable saving.
|
package/dist/proofs.mjs
CHANGED
|
@@ -328,20 +328,29 @@ async function verifyPublicExercise(exercise, index, manifest, nowMs = Date.now(
|
|
|
328
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
329
|
}
|
|
330
330
|
async function readFixedJson(url, maxBytes, fetchImpl) {
|
|
331
|
-
let controller = new AbortController(), timer, reader, deadline = new Promise((_, reject) => {
|
|
331
|
+
let controller = new AbortController(), timer, reader, response, deadlineAt = performance.now() + 8e3, deadline = new Promise((_, reject) => {
|
|
332
332
|
timer = setTimeout(() => {
|
|
333
333
|
controller.abort(), reject(new PublicExerciseError("upstream_unavailable"));
|
|
334
334
|
}, 8e3);
|
|
335
335
|
});
|
|
336
336
|
try {
|
|
337
|
-
let
|
|
338
|
-
|
|
337
|
+
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) => {
|
|
338
|
+
if (controller.signal.aborted) throw value.body && !value.body.locked && value.body.cancel().catch(() => {
|
|
339
|
+
}), new PublicExerciseError("upstream_unavailable");
|
|
340
|
+
return value;
|
|
341
|
+
});
|
|
342
|
+
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");
|
|
339
343
|
let contentLength = response.headers.get("content-length");
|
|
340
344
|
requireCondition(contentLength === null || /^\d+$/.test(contentLength) && Number(contentLength) <= maxBytes, "upstream_too_large"), reader = response.body.getReader();
|
|
341
|
-
let chunks = [], size = 0;
|
|
345
|
+
let chunks = [], size = 0, reads = 0, emptyReads = 0;
|
|
342
346
|
for (; ; ) {
|
|
347
|
+
requireCondition(performance.now() < deadlineAt, "upstream_unavailable"), requireCondition(++reads <= 2048, "upstream_too_large");
|
|
343
348
|
let part = await Promise.race([reader.read(), deadline]);
|
|
344
|
-
if (part.done) break;
|
|
349
|
+
if (requireCondition(performance.now() < deadlineAt, "upstream_unavailable"), part.done) break;
|
|
350
|
+
if (part.value.byteLength === 0) {
|
|
351
|
+
requireCondition(++emptyReads <= 128, "upstream_too_large");
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
345
354
|
size += part.value.byteLength, requireCondition(size <= maxBytes, "upstream_too_large"), chunks.push(part.value);
|
|
346
355
|
}
|
|
347
356
|
let bytes = new Uint8Array(size), offset = 0;
|
|
@@ -354,8 +363,9 @@ async function readFixedJson(url, maxBytes, fetchImpl) {
|
|
|
354
363
|
} catch (error) {
|
|
355
364
|
throw error instanceof PublicExerciseError ? error : new PublicExerciseError("upstream_unavailable");
|
|
356
365
|
} finally {
|
|
357
|
-
clearTimeout(timer), controller.abort(), reader
|
|
358
|
-
}), reader.releaseLock())
|
|
366
|
+
clearTimeout(timer), controller.abort(), reader ? (reader.cancel().catch(() => {
|
|
367
|
+
}), reader.releaseLock()) : response?.body && !response.body.locked && response.body.cancel().catch(() => {
|
|
368
|
+
});
|
|
359
369
|
}
|
|
360
370
|
}
|
|
361
371
|
async function runPublicExercise(text, options = {}) {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @voidly/session — GENERATED by scripts/build-types.mjs. Do not edit.
|
|
2
|
+
//
|
|
3
|
+
// The transitive closure of what src/proofsAuto.ts exports, flattened into one
|
|
4
|
+
// module. Server declarations are absent because nothing here reaches them.
|
|
5
|
+
|
|
6
|
+
export declare const AUTOMATIC_BASE = "https://api.voidly.ai/v4/proofs/collections";
|
|
7
|
+
export declare const AUTOMATIC_BOUNDARY: Readonly<{
|
|
8
|
+
decision: "STOP";
|
|
9
|
+
wallet_required: false;
|
|
10
|
+
payment_action_allowed: false;
|
|
11
|
+
authorization_created: false;
|
|
12
|
+
transaction_broadcast: false;
|
|
13
|
+
settlement_observed: false;
|
|
14
|
+
reward_offered: false;
|
|
15
|
+
reward_amount: "0";
|
|
16
|
+
reward_asset: null;
|
|
17
|
+
customer_identity_verified: false;
|
|
18
|
+
commercial_transaction: false;
|
|
19
|
+
credited_to_traction_metrics: false;
|
|
20
|
+
unique_person_verified: false;
|
|
21
|
+
impact_verified: false;
|
|
22
|
+
}>;
|
|
23
|
+
export declare const AUTOMATIC_HANDOFF_MAX_BYTES = 1024;
|
|
24
|
+
export declare const AUTOMATIC_HANDOFF_SCHEMA: "voidpay.proof-collection.agent-handoff/v4";
|
|
25
|
+
export declare const AUTOMATIC_ISSUER = "https://api.voidly.ai/v2/proofs/collections/issuer";
|
|
26
|
+
export declare const AUTOMATIC_RECEIPT_DOMAIN = "voidpay-proof-collection-receipt/v4\n";
|
|
27
|
+
export declare const AUTOMATIC_RUN_BINDING_DOMAIN = "voidpay-proof-collection-run-binding/v4\n";
|
|
28
|
+
export declare const AUTOMATIC_SCOPE = "public-provider-discovery-and-owner-preauthorized-private-save-only";
|
|
29
|
+
export declare const AUTOMATIC_TASK: Readonly<{
|
|
30
|
+
id: "sessions-auto-save-01";
|
|
31
|
+
version: 1;
|
|
32
|
+
acceptance_rule_sha256: "b58e042e088740892856e96f4c8eee079bd94819c518e8e486ef250184ffb230";
|
|
33
|
+
}>;
|
|
34
|
+
export type AutomaticHandoff = {
|
|
35
|
+
schema: typeof AUTOMATIC_HANDOFF_SCHEMA;
|
|
36
|
+
run_id: string;
|
|
37
|
+
completion_capability: string;
|
|
38
|
+
};
|
|
39
|
+
export declare class AutomaticProofError extends Error {
|
|
40
|
+
readonly code: AutomaticProofErrorCode;
|
|
41
|
+
constructor(code: AutomaticProofErrorCode);
|
|
42
|
+
}
|
|
43
|
+
export type AutomaticProofErrorCode = "handoff_invalid" | "self_test_failed" | "response_invalid" | "issuer_invalid" | "receipt_invalid" | "completion_unavailable" | "completion_uncertain" | "permission_refused" | "run_expired_or_revoked" | "rate_limited" | "attempts_exhausted" | "provider_check_failed";
|
|
44
|
+
export type AutomaticReceipt = {
|
|
45
|
+
statement: {
|
|
46
|
+
schema: "voidpay.proof-collection.receipt/v4";
|
|
47
|
+
issuer: string;
|
|
48
|
+
key_id: string;
|
|
49
|
+
event_id: string;
|
|
50
|
+
run_binding_sha256: string;
|
|
51
|
+
exercise_binding_sha256: string;
|
|
52
|
+
artwork: {
|
|
53
|
+
recipe: "contours-v1";
|
|
54
|
+
seed: string;
|
|
55
|
+
};
|
|
56
|
+
task: typeof AUTOMATIC_TASK;
|
|
57
|
+
computation_task: typeof SESSIONS_PROOFS_TASK;
|
|
58
|
+
checked_at: string;
|
|
59
|
+
completed_at: string;
|
|
60
|
+
outcome: "PASS";
|
|
61
|
+
checks: {
|
|
62
|
+
server: Record<typeof SERVER_CHECKS[number], true>;
|
|
63
|
+
claimant: Record<typeof CLAIMANT_CHECKS[number], true>;
|
|
64
|
+
};
|
|
65
|
+
provider: {
|
|
66
|
+
provider_did: string;
|
|
67
|
+
manifest_url: string;
|
|
68
|
+
};
|
|
69
|
+
scope: typeof AUTOMATIC_SCOPE;
|
|
70
|
+
boundary: typeof AUTOMATIC_BOUNDARY;
|
|
71
|
+
};
|
|
72
|
+
signature_hex: string;
|
|
73
|
+
};
|
|
74
|
+
declare const CLAIMANT_CHECKS: readonly [
|
|
75
|
+
"manifest_digest_matches",
|
|
76
|
+
"challenge_response_matches"
|
|
77
|
+
];
|
|
78
|
+
export declare function completeAutomaticProof(text: string, options?: {
|
|
79
|
+
readonly fetchImpl?: typeof fetch;
|
|
80
|
+
readonly nowMs?: number;
|
|
81
|
+
}): Promise<VerifiedAutomaticProof>;
|
|
82
|
+
export declare function parseAutomaticHandoff(text: string): AutomaticHandoff;
|
|
83
|
+
declare const SERVER_CHECKS: readonly [
|
|
84
|
+
"owner_preauthorized",
|
|
85
|
+
"challenge_unexpired",
|
|
86
|
+
"provider_pair_listed",
|
|
87
|
+
"manifest_verified",
|
|
88
|
+
"wrong_pin_refused"
|
|
89
|
+
];
|
|
90
|
+
declare const SESSIONS_PROOFS_TASK: Readonly<{
|
|
91
|
+
id: "sessions-public-check-01";
|
|
92
|
+
version: 1;
|
|
93
|
+
acceptance_rule_sha256: "1be9d67c2f8caffa45fd985b7454371838c29e4fadbd8dadc2a4e6663b0f7bc5";
|
|
94
|
+
}>;
|
|
95
|
+
export type VerifiedAutomaticProof = {
|
|
96
|
+
schema: "voidly.session.automatic-proof/v1";
|
|
97
|
+
ok: true;
|
|
98
|
+
saved_proof: true;
|
|
99
|
+
event_id: string;
|
|
100
|
+
visibility: "private" | "public";
|
|
101
|
+
already_completed: boolean;
|
|
102
|
+
receipt: AutomaticReceipt;
|
|
103
|
+
artwork: {
|
|
104
|
+
recipe: "contours-v1";
|
|
105
|
+
seed: string;
|
|
106
|
+
svg: string;
|
|
107
|
+
sha256: string;
|
|
108
|
+
};
|
|
109
|
+
return_url: "https://voidly.ai/pay/proofs";
|
|
110
|
+
payment_boundary: "STOP";
|
|
111
|
+
};
|
|
@@ -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
|
+
};
|
package/dist/proofsCli.mjs
CHANGED
|
@@ -109,9 +109,9 @@ function rebuildDocumentDeep(document) {
|
|
|
109
109
|
if (top === null) return document;
|
|
110
110
|
let made = /* @__PURE__ */ new Map();
|
|
111
111
|
made.set(document, top);
|
|
112
|
-
let
|
|
113
|
-
for (;
|
|
114
|
-
let frame =
|
|
112
|
+
let pending2 = [top];
|
|
113
|
+
for (; pending2.length > 0; ) {
|
|
114
|
+
let frame = pending2.pop();
|
|
115
115
|
if (frame === void 0) break;
|
|
116
116
|
for (let key of Object.keys(frame)) {
|
|
117
117
|
let child = frame[key];
|
|
@@ -122,15 +122,15 @@ function rebuildDocumentDeep(document) {
|
|
|
122
122
|
continue;
|
|
123
123
|
}
|
|
124
124
|
let copy = containerCopy(child);
|
|
125
|
-
copy !== null && (made.set(child, copy), frame[key] = copy,
|
|
125
|
+
copy !== null && (made.set(child, copy), frame[key] = copy, pending2.push(copy));
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
return top;
|
|
129
129
|
}
|
|
130
130
|
function freezeManifestDeep(m) {
|
|
131
|
-
let seen = /* @__PURE__ */ new Set(),
|
|
132
|
-
for (;
|
|
133
|
-
let node =
|
|
131
|
+
let seen = /* @__PURE__ */ new Set(), pending2 = [m];
|
|
132
|
+
for (; pending2.length > 0; ) {
|
|
133
|
+
let node = pending2.pop();
|
|
134
134
|
if (node === null || typeof node != "object") continue;
|
|
135
135
|
let container = node;
|
|
136
136
|
if (!seen.has(container)) {
|
|
@@ -138,7 +138,7 @@ function freezeManifestDeep(m) {
|
|
|
138
138
|
for (let key of Reflect.ownKeys(container)) {
|
|
139
139
|
if (typeof key == "symbol") continue;
|
|
140
140
|
let d = Object.getOwnPropertyDescriptor(container, key);
|
|
141
|
-
d !== void 0 && "value" in d &&
|
|
141
|
+
d !== void 0 && "value" in d && pending2.push(d.value);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}
|
|
@@ -329,20 +329,29 @@ async function verifyPublicExercise(exercise, index, manifest, nowMs = Date.now(
|
|
|
329
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
330
|
}
|
|
331
331
|
async function readFixedJson(url, maxBytes, fetchImpl) {
|
|
332
|
-
let controller = new AbortController(), timer, reader, deadline = new Promise((_, reject) => {
|
|
332
|
+
let controller = new AbortController(), timer, reader, response, deadlineAt = performance.now() + 8e3, deadline = new Promise((_, reject) => {
|
|
333
333
|
timer = setTimeout(() => {
|
|
334
334
|
controller.abort(), reject(new PublicExerciseError("upstream_unavailable"));
|
|
335
335
|
}, 8e3);
|
|
336
336
|
});
|
|
337
337
|
try {
|
|
338
|
-
let
|
|
339
|
-
|
|
338
|
+
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) => {
|
|
339
|
+
if (controller.signal.aborted) throw value.body && !value.body.locked && value.body.cancel().catch(() => {
|
|
340
|
+
}), new PublicExerciseError("upstream_unavailable");
|
|
341
|
+
return value;
|
|
342
|
+
});
|
|
343
|
+
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");
|
|
340
344
|
let contentLength = response.headers.get("content-length");
|
|
341
345
|
requireCondition(contentLength === null || /^\d+$/.test(contentLength) && Number(contentLength) <= maxBytes, "upstream_too_large"), reader = response.body.getReader();
|
|
342
|
-
let chunks = [], size = 0;
|
|
346
|
+
let chunks = [], size = 0, reads = 0, emptyReads = 0;
|
|
343
347
|
for (; ; ) {
|
|
348
|
+
requireCondition(performance.now() < deadlineAt, "upstream_unavailable"), requireCondition(++reads <= 2048, "upstream_too_large");
|
|
344
349
|
let part = await Promise.race([reader.read(), deadline]);
|
|
345
|
-
if (part.done) break;
|
|
350
|
+
if (requireCondition(performance.now() < deadlineAt, "upstream_unavailable"), part.done) break;
|
|
351
|
+
if (part.value.byteLength === 0) {
|
|
352
|
+
requireCondition(++emptyReads <= 128, "upstream_too_large");
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
346
355
|
size += part.value.byteLength, requireCondition(size <= maxBytes, "upstream_too_large"), chunks.push(part.value);
|
|
347
356
|
}
|
|
348
357
|
let bytes = new Uint8Array(size), offset = 0;
|
|
@@ -355,22 +364,214 @@ async function readFixedJson(url, maxBytes, fetchImpl) {
|
|
|
355
364
|
} catch (error) {
|
|
356
365
|
throw error instanceof PublicExerciseError ? error : new PublicExerciseError("upstream_unavailable");
|
|
357
366
|
} finally {
|
|
358
|
-
clearTimeout(timer), controller.abort(), reader
|
|
359
|
-
}), reader.releaseLock())
|
|
367
|
+
clearTimeout(timer), controller.abort(), reader ? (reader.cancel().catch(() => {
|
|
368
|
+
}), reader.releaseLock()) : response?.body && !response.body.locked && response.body.cancel().catch(() => {
|
|
369
|
+
});
|
|
360
370
|
}
|
|
361
371
|
}
|
|
362
372
|
async function runPublicExercise(text, options = {}) {
|
|
363
373
|
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
374
|
return verifyPublicExercise(exercise, index, manifest, options.nowMs ?? Date.now());
|
|
365
375
|
}
|
|
366
|
-
|
|
376
|
+
import nacl3 from "tweetnacl";
|
|
377
|
+
function automaticCanonicalJson(value) {
|
|
378
|
+
if (value === null || typeof value == "string" || typeof value == "boolean" || typeof value == "number" && Number.isSafeInteger(value)) return JSON.stringify(value);
|
|
379
|
+
if (Array.isArray(value)) return `[${value.map(automaticCanonicalJson).join(",")}]`;
|
|
380
|
+
if (value !== null && typeof value == "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
|
381
|
+
let record = value;
|
|
382
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${automaticCanonicalJson(record[key])}`).join(",")}}`;
|
|
383
|
+
}
|
|
384
|
+
throw new TypeError("Unsupported automatic collection JSON value");
|
|
385
|
+
}
|
|
386
|
+
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
|
|
387
|
+
`, AUTOMATIC_RUN_BINDING_DOMAIN = `voidpay-proof-collection-run-binding/v4
|
|
388
|
+
`, 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 {
|
|
389
|
+
constructor(code) {
|
|
390
|
+
super(code);
|
|
391
|
+
this.code = code;
|
|
392
|
+
this.name = "AutomaticProofError";
|
|
393
|
+
}
|
|
394
|
+
}, AutomaticHttpError = class extends AutomaticProofError {
|
|
395
|
+
constructor(code, status) {
|
|
396
|
+
super(code);
|
|
397
|
+
this.status = status;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
function requireCondition2(value, code) {
|
|
401
|
+
if (!value) throw new AutomaticProofError(code);
|
|
402
|
+
}
|
|
403
|
+
function exact2(value, keys) {
|
|
404
|
+
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));
|
|
405
|
+
}
|
|
406
|
+
function same2(value, expected) {
|
|
407
|
+
return exact2(value, Object.keys(expected)) && JSON.stringify(value, Object.keys(expected).sort()) === JSON.stringify(expected, Object.keys(expected).sort());
|
|
408
|
+
}
|
|
409
|
+
function iso2(value) {
|
|
410
|
+
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;
|
|
411
|
+
}
|
|
412
|
+
function unhex(value) {
|
|
413
|
+
return Uint8Array.from(value.match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));
|
|
414
|
+
}
|
|
415
|
+
function parseAutomaticHandoff(text) {
|
|
416
|
+
requireCondition2(typeof text == "string" && new TextEncoder().encode(text).length <= AUTOMATIC_HANDOFF_MAX_BYTES, "handoff_invalid");
|
|
417
|
+
let value;
|
|
418
|
+
try {
|
|
419
|
+
value = JSON.parse(text);
|
|
420
|
+
} catch {
|
|
421
|
+
throw new AutomaticProofError("handoff_invalid");
|
|
422
|
+
}
|
|
423
|
+
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 };
|
|
424
|
+
}
|
|
425
|
+
async function requestJson(kind, handoff, body, fetchImpl) {
|
|
426
|
+
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) => {
|
|
427
|
+
timer = setTimeout(() => {
|
|
428
|
+
controller.abort(), reject(new AutomaticProofError("completion_unavailable"));
|
|
429
|
+
}, kind === "submit" ? 2e4 : 1e4);
|
|
430
|
+
});
|
|
431
|
+
try {
|
|
432
|
+
let headers = { accept: "application/json" };
|
|
433
|
+
kind !== "issuer" && (headers["content-type"] = "application/json", headers.authorization = `Bearer ${handoff.completion_capability}`);
|
|
434
|
+
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) => {
|
|
435
|
+
if (controller.signal.aborted) throw value2.body && !value2.body.locked && value2.body.cancel().catch(() => {
|
|
436
|
+
}), new AutomaticProofError("completion_unavailable");
|
|
437
|
+
return value2;
|
|
438
|
+
});
|
|
439
|
+
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");
|
|
440
|
+
let length = response.headers.get("content-length");
|
|
441
|
+
requireCondition2(length === null || /^\d+$/.test(length) && Number(length) <= MAX_RESPONSE_BYTES, "response_invalid"), reader = response.body.getReader();
|
|
442
|
+
let chunks = [], size = 0, reads = 0, emptyReads = 0;
|
|
443
|
+
for (; ; ) {
|
|
444
|
+
requireCondition2(performance.now() < deadlineAt, "completion_unavailable"), requireCondition2(++reads <= 2048, "response_invalid");
|
|
445
|
+
let next = await Promise.race([reader.read(), deadline]);
|
|
446
|
+
if (requireCondition2(performance.now() < deadlineAt, "completion_unavailable"), next.done) break;
|
|
447
|
+
if (next.value.byteLength === 0) {
|
|
448
|
+
requireCondition2(++emptyReads <= 128, "response_invalid");
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
size += next.value.byteLength, requireCondition2(size <= MAX_RESPONSE_BYTES, "response_invalid"), chunks.push(next.value);
|
|
452
|
+
}
|
|
453
|
+
let bytes = new Uint8Array(size), offset = 0;
|
|
454
|
+
for (let chunk of chunks) bytes.set(chunk, offset), offset += chunk.byteLength;
|
|
455
|
+
let value;
|
|
456
|
+
try {
|
|
457
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
458
|
+
} catch {
|
|
459
|
+
throw new AutomaticProofError("response_invalid");
|
|
460
|
+
}
|
|
461
|
+
if (requireCondition2(!JSON.stringify(value).includes(handoff.completion_capability), "response_invalid"), !response.ok) {
|
|
462
|
+
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");
|
|
463
|
+
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";
|
|
464
|
+
throw new AutomaticHttpError(code, response.status);
|
|
465
|
+
}
|
|
466
|
+
return value;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
throw error instanceof AutomaticProofError ? error : new AutomaticProofError("completion_unavailable");
|
|
469
|
+
} finally {
|
|
470
|
+
clearTimeout(timer), controller.abort(), reader ? (reader.cancel().catch(() => {
|
|
471
|
+
}), reader.releaseLock()) : response?.body && !response.body.locked && response.body.cancel().catch(() => {
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function issuerKeys(handoff, fetchImpl) {
|
|
476
|
+
let value = await requestJson("issuer", handoff, void 0, fetchImpl);
|
|
477
|
+
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");
|
|
478
|
+
let keys = /* @__PURE__ */ new Map();
|
|
479
|
+
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);
|
|
480
|
+
return keys;
|
|
481
|
+
}
|
|
482
|
+
async function verifyComplete(value, handoff, fetchImpl, now) {
|
|
483
|
+
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");
|
|
484
|
+
let record = value.record;
|
|
485
|
+
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");
|
|
486
|
+
let receipt = record.receipt, s = receipt.statement;
|
|
487
|
+
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");
|
|
488
|
+
let exercise;
|
|
489
|
+
try {
|
|
490
|
+
exercise = parsePublicExercise(JSON.stringify(value.exercise), Date.parse(s.checked_at));
|
|
491
|
+
} catch {
|
|
492
|
+
throw new AutomaticProofError("receipt_invalid");
|
|
493
|
+
}
|
|
494
|
+
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");
|
|
495
|
+
let key = (await issuerKeys(handoff, fetchImpl)).get(s.key_id);
|
|
496
|
+
requireCondition2(key && nacl3.sign.detached.verify(new TextEncoder().encode(AUTOMATIC_RECEIPT_DOMAIN + automaticCanonicalJson(s)), unhex(receipt.signature_hex), unhex(key)), "receipt_invalid");
|
|
497
|
+
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" };
|
|
498
|
+
return requireCondition2(!JSON.stringify(result).includes(handoff.completion_capability), "response_invalid"), result;
|
|
499
|
+
}
|
|
500
|
+
function pending(value, runId) {
|
|
501
|
+
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;
|
|
502
|
+
}
|
|
503
|
+
function transient(error) {
|
|
504
|
+
return error instanceof AutomaticHttpError ? error.status === 409 || error.status >= 500 : error instanceof AutomaticProofError && error.code === "completion_unavailable";
|
|
505
|
+
}
|
|
506
|
+
async function completeAutomaticProof(text, options = {}) {
|
|
507
|
+
let handoff = parseAutomaticHandoff(text), fetchImpl = options.fetchImpl ?? fetch, now = () => options.nowMs ?? Date.now();
|
|
508
|
+
requireCondition2((await runSessionsSelfTest()).ok, "self_test_failed");
|
|
509
|
+
let mayHaveSaved = false;
|
|
510
|
+
try {
|
|
511
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) try {
|
|
512
|
+
let existing = await requestJson("result", handoff, { run_id: handoff.run_id }, fetchImpl);
|
|
513
|
+
if (!pending(existing, handoff.run_id)) return await verifyComplete(existing, handoff, fetchImpl, now());
|
|
514
|
+
let fresh = await requestJson("exercise", handoff, { run_id: handoff.run_id }, fetchImpl);
|
|
515
|
+
if (exact2(fresh, ["schema", "ok", "status", "run_id", "exercise", "record", "already_completed"])) return await verifyComplete(fresh, handoff, fetchImpl, now());
|
|
516
|
+
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");
|
|
517
|
+
let exercise = parsePublicExercise(JSON.stringify(fresh.exercise), now());
|
|
518
|
+
requireCondition2(exercise.challenge_id === handoff.run_id, "response_invalid");
|
|
519
|
+
let result = await runPublicExercise(JSON.stringify(exercise), { fetchImpl, nowMs: options.nowMs });
|
|
520
|
+
mayHaveSaved = true;
|
|
521
|
+
let completed = await requestJson("submit", handoff, { run_id: handoff.run_id, result }, fetchImpl);
|
|
522
|
+
return await verifyComplete(completed, handoff, fetchImpl, now());
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (error instanceof PublicExerciseError) {
|
|
525
|
+
if (error.code !== "upstream_unavailable") throw new AutomaticProofError("provider_check_failed");
|
|
526
|
+
error = new AutomaticProofError("completion_unavailable");
|
|
527
|
+
}
|
|
528
|
+
if (!transient(error)) throw error;
|
|
529
|
+
attempt < MAX_ATTEMPTS - 1 && await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
530
|
+
}
|
|
531
|
+
if (mayHaveSaved) try {
|
|
532
|
+
let existing = await requestJson("result", handoff, { run_id: handoff.run_id }, fetchImpl);
|
|
533
|
+
if (!pending(existing, handoff.run_id)) return await verifyComplete(existing, handoff, fetchImpl, now());
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (!transient(error)) throw error;
|
|
536
|
+
}
|
|
537
|
+
throw new AutomaticProofError(mayHaveSaved ? "completion_uncertain" : "completion_unavailable");
|
|
538
|
+
} finally {
|
|
539
|
+
handoff.completion_capability = "";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
async function exportProofArtifacts(eventId, verifiedReceipt) {
|
|
543
|
+
let artworkPath = null, receiptPath = null;
|
|
544
|
+
try {
|
|
545
|
+
if (!/^[0-9a-f]{32}$/.test(eventId)) throw new Error("event_invalid");
|
|
546
|
+
let svg = proofArtworkSvg(eventId), receipt = JSON.stringify(verifiedReceipt, null, 2) + `
|
|
547
|
+
`, fs = await import("node:fs/promises"), { constants } = await import("node:fs"), { join } = await import("node:path");
|
|
548
|
+
if (!Number.isInteger(constants.O_NOFOLLOW) || constants.O_NOFOLLOW === 0) throw new Error("no_follow_unavailable");
|
|
549
|
+
let directory = await fs.mkdtemp(join(process.cwd(), "voidpay-proof-"));
|
|
550
|
+
await fs.chmod(directory, 448);
|
|
551
|
+
let stat = await fs.lstat(directory);
|
|
552
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("directory_invalid");
|
|
553
|
+
let create = async (name, bytes) => {
|
|
554
|
+
let path = join(directory, name), handle = await fs.open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
555
|
+
try {
|
|
556
|
+
await handle.writeFile(bytes, "utf8"), await handle.sync();
|
|
557
|
+
} finally {
|
|
558
|
+
await handle.close();
|
|
559
|
+
}
|
|
560
|
+
return path;
|
|
561
|
+
};
|
|
562
|
+
return receiptPath = await create("proof-receipt.json", receipt), artworkPath = await create("proof-artwork.svg", svg), { ok: true, artwork_path: artworkPath, receipt_path: receiptPath, error: null };
|
|
563
|
+
} catch {
|
|
564
|
+
return { ok: false, artwork_path: artworkPath, receipt_path: receiptPath, error: "local_export_failed" };
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function input(maxBytes = PUBLIC_EXERCISE_MAX_BYTES) {
|
|
367
568
|
return new Promise((resolve, reject) => {
|
|
368
569
|
let chunks = [], size = 0, cleanup = () => {
|
|
369
570
|
clearTimeout(timer), process.stdin.removeListener("data", onData), process.stdin.removeListener("end", onEnd), process.stdin.removeListener("error", onError), process.stdin.pause();
|
|
370
571
|
}, fail = () => {
|
|
371
572
|
cleanup(), reject(new Error("input_invalid"));
|
|
372
573
|
}, onData = (chunk) => {
|
|
373
|
-
size += chunk.byteLength, size >
|
|
574
|
+
size += chunk.byteLength, size > maxBytes ? fail() : chunks.push(chunk);
|
|
374
575
|
}, onEnd = () => {
|
|
375
576
|
cleanup();
|
|
376
577
|
try {
|
|
@@ -387,15 +588,27 @@ async function main() {
|
|
|
387
588
|
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
589
|
`), 1;
|
|
389
590
|
let args = process.argv.slice(2);
|
|
591
|
+
if (args.length === 1 && args[0] === "complete") try {
|
|
592
|
+
let result = await completeAutomaticProof(await input(AUTOMATIC_HANDOFF_MAX_BYTES)), artifacts = await exportProofArtifacts(result.event_id, result.receipt), { artwork, receipt, ...summary } = result;
|
|
593
|
+
return process.stdout.write(JSON.stringify({ artifacts, outcome: "PASS", checks_passed: 7, receipt_verified: true, ...summary, receipt, artwork: { recipe: artwork.recipe, seed: artwork.seed, sha256: artwork.sha256, ...artifacts.ok ? {} : { svg: artwork.svg } } }, null, 2) + `
|
|
594
|
+
`), process.stderr.write(`PASS \u2014 seven checks verified. ${result.already_completed ? "Your existing proof was recovered" : "Your proof was saved"}. ${result.visibility === "private" ? "Saved privately; nothing published by this command." : "This record was already made public separately."}
|
|
595
|
+
`), artifacts.ok || process.stderr.write(`The proof is saved, but local export failed. Use the returned SVG to save or display the art; do not start another proof.
|
|
596
|
+
`), 0;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
let code = error instanceof AutomaticProofError ? error.code : "completion_unavailable", next = code === "run_expired_or_revoked" || code === "attempts_exhausted" || code === "permission_refused" ? "This instruction cannot continue. Check your records at https://voidly.ai/pay/proofs before approving a new run." : "A submission may already be saved; retry the SAME instruction or check https://voidly.ai/pay/proofs. Never create a replacement run just to retry.";
|
|
599
|
+
return process.stderr.write(`Automatic proof stopped: ${code}. Completion was not verified. ${next}
|
|
600
|
+
`), 1;
|
|
601
|
+
}
|
|
390
602
|
if (args.length === 2 && args[0] === "artwork" && /^[0-9a-f]{32}$/.test(args[1])) return process.stdout.write(proofArtworkSvg(args[1]) + `
|
|
391
603
|
`), process.stderr.write(`Decorative artwork only. The supplied identifier was not checked with a server.
|
|
392
604
|
`), 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
|
|
605
|
+
if (args.length !== 1 || !["self-test", "public-check", "--help"].includes(args[0])) return process.stderr.write(`Usage: voidly-session self-test | public-check | complete | artwork <saved-event-id> | --help
|
|
394
606
|
`), 1;
|
|
395
607
|
if (args[0] === "--help") return process.stdout.write(`self-test: offline SDK verification; no network.
|
|
396
608
|
public-check: public exercise JSON on stdin; two fixed public GETs; result JSON on stdout.
|
|
609
|
+
complete: one-proof completion credential on stdin; fixed first-party requests; saves one preauthorized private PASS, verifies its signed receipt and creates new local artwork/receipt files.
|
|
397
610
|
artwork <saved-event-id>: decorative SVG on stdout, generated locally; no savedness verification.
|
|
398
|
-
|
|
611
|
+
Only complete saves proofs. No command publishes or performs payment. Review the installed package before running it.
|
|
399
612
|
`), 0;
|
|
400
613
|
try {
|
|
401
614
|
if (args[0] === "self-test") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidly/session",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"agent-payments",
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
"import": "./dist/proofs.mjs",
|
|
47
47
|
"default": "./dist/proofs.mjs"
|
|
48
48
|
},
|
|
49
|
+
"./proofs-auto": {
|
|
50
|
+
"types": "./dist/proofsAuto.d.ts",
|
|
51
|
+
"import": "./dist/proofsAuto.mjs",
|
|
52
|
+
"default": "./dist/proofsAuto.mjs"
|
|
53
|
+
},
|
|
49
54
|
"./package.json": "./package.json"
|
|
50
55
|
},
|
|
51
56
|
"bin": {
|