@opendatalabs/vana-sdk 3.18.0 → 3.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -3
- package/dist/errors.cjs +56 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +98 -2
- package/dist/errors.js +48 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +542 -33
- package/dist/index.browser.js.map +4 -4
- package/dist/index.node.cjs +569 -33
- package/dist/index.node.cjs.map +4 -4
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +542 -33
- package/dist/index.node.js.map +4 -4
- package/dist/protocol/derivative-questions.cjs +501 -0
- package/dist/protocol/derivative-questions.cjs.map +1 -0
- package/dist/protocol/derivative-questions.d.ts +355 -0
- package/dist/protocol/derivative-questions.js +486 -0
- package/dist/protocol/derivative-questions.js.map +1 -0
- package/dist/protocol/derivative-questions.test.d.ts +1 -0
- package/dist/protocol/lineage.cjs +9 -4
- package/dist/protocol/lineage.cjs.map +1 -1
- package/dist/protocol/lineage.d.ts +21 -12
- package/dist/protocol/lineage.js +9 -4
- package/dist/protocol/lineage.js.map +1 -1
- package/dist/protocol/personal-server-write.cjs +12 -108
- package/dist/protocol/personal-server-write.cjs.map +1 -1
- package/dist/protocol/personal-server-write.d.ts +2 -17
- package/dist/protocol/personal-server-write.js +7 -98
- package/dist/protocol/personal-server-write.js.map +1 -1
- package/dist/protocol/write-request.cjs +142 -0
- package/dist/protocol/write-request.cjs.map +1 -0
- package/dist/protocol/write-request.d.ts +63 -0
- package/dist/protocol/write-request.js +111 -0
- package/dist/protocol/write-request.js.map +1 -0
- package/dist/tests/mock-personal-server.d.ts +51 -0
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var write_request_exports = {};
|
|
20
|
+
__export(write_request_exports, {
|
|
21
|
+
errorMessage: () => errorMessage,
|
|
22
|
+
finiteOr: () => finiteOr,
|
|
23
|
+
nextProofIat: () => nextProofIat,
|
|
24
|
+
normalizeBaseUrl: () => normalizeBaseUrl,
|
|
25
|
+
proofKeyFor: () => proofKeyFor,
|
|
26
|
+
resolveFetch: () => resolveFetch,
|
|
27
|
+
sendWithFreshProof: () => sendWithFreshProof,
|
|
28
|
+
sleep: () => sleep
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(write_request_exports);
|
|
31
|
+
var import_sha2 = require("@noble/hashes/sha2");
|
|
32
|
+
var import_viem = require("viem");
|
|
33
|
+
var import_errors = require("../errors");
|
|
34
|
+
function normalizeBaseUrl(url) {
|
|
35
|
+
return url.replace(/\/+$/, "");
|
|
36
|
+
}
|
|
37
|
+
function resolveFetch(fetchFn) {
|
|
38
|
+
const resolved = fetchFn ?? globalThis.fetch;
|
|
39
|
+
if (resolved === void 0) {
|
|
40
|
+
throw new import_errors.WriteRequestError("No fetch implementation available");
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
function errorMessage(err) {
|
|
45
|
+
return err instanceof Error ? err.message : String(err);
|
|
46
|
+
}
|
|
47
|
+
function finiteOr(value, fallback) {
|
|
48
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
49
|
+
}
|
|
50
|
+
function sleep(ms) {
|
|
51
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
}
|
|
53
|
+
const issuedProofIats = /* @__PURE__ */ new Map();
|
|
54
|
+
const issuedProofBuckets = /* @__PURE__ */ new Map();
|
|
55
|
+
let issuedProofIatsPrunedAtSec = 0;
|
|
56
|
+
const WEB3_SIGNED_PROOF_LIFETIME_SECONDS = 300;
|
|
57
|
+
const WEB3_SIGNED_CLOCK_SKEW_SECONDS = 60;
|
|
58
|
+
const PROOF_IAT_RETENTION_SECONDS = WEB3_SIGNED_PROOF_LIFETIME_SECONDS + WEB3_SIGNED_CLOCK_SKEW_SECONDS;
|
|
59
|
+
const PROOF_IAT_MAX_AHEAD_SECONDS = 30;
|
|
60
|
+
function pruneIssuedProofIats(nowSec) {
|
|
61
|
+
if (issuedProofIatsPrunedAtSec === nowSec) return;
|
|
62
|
+
issuedProofIatsPrunedAtSec = nowSec;
|
|
63
|
+
const cutoff = nowSec - PROOF_IAT_RETENTION_SECONDS;
|
|
64
|
+
for (const [sec, keys] of issuedProofBuckets) {
|
|
65
|
+
if (sec >= cutoff) continue;
|
|
66
|
+
for (const key of keys) issuedProofIats.delete(key);
|
|
67
|
+
issuedProofBuckets.delete(sec);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function setIssuedProofIat(key, iat, previous) {
|
|
71
|
+
if (previous !== void 0) {
|
|
72
|
+
const bucket2 = issuedProofBuckets.get(previous);
|
|
73
|
+
bucket2?.delete(key);
|
|
74
|
+
if (bucket2?.size === 0) issuedProofBuckets.delete(previous);
|
|
75
|
+
}
|
|
76
|
+
issuedProofIats.set(key, iat);
|
|
77
|
+
let bucket = issuedProofBuckets.get(iat);
|
|
78
|
+
if (bucket === void 0) {
|
|
79
|
+
bucket = /* @__PURE__ */ new Set();
|
|
80
|
+
issuedProofBuckets.set(iat, bucket);
|
|
81
|
+
}
|
|
82
|
+
bucket.add(key);
|
|
83
|
+
}
|
|
84
|
+
function nextProofIat(proofKey) {
|
|
85
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
86
|
+
pruneIssuedProofIats(nowSec);
|
|
87
|
+
const last = issuedProofIats.get(proofKey);
|
|
88
|
+
const iat = last === void 0 ? nowSec : Math.max(nowSec, last + 1);
|
|
89
|
+
setIssuedProofIat(proofKey, iat, last);
|
|
90
|
+
const waitSec = iat - nowSec - PROOF_IAT_MAX_AHEAD_SECONDS;
|
|
91
|
+
if (waitSec <= 0) return Promise.resolve(iat);
|
|
92
|
+
return sleep(waitSec * 1e3).then(() => iat);
|
|
93
|
+
}
|
|
94
|
+
function proofKeyFor(parts) {
|
|
95
|
+
return (0, import_viem.bytesToHex)(
|
|
96
|
+
(0, import_sha2.sha256)(
|
|
97
|
+
new TextEncoder().encode(
|
|
98
|
+
JSON.stringify([
|
|
99
|
+
parts.aud,
|
|
100
|
+
parts.method,
|
|
101
|
+
parts.uri,
|
|
102
|
+
parts.grantId,
|
|
103
|
+
parts.signedBytes ? (0, import_viem.bytesToHex)((0, import_sha2.sha256)(parts.signedBytes)) : ""
|
|
104
|
+
])
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
async function sendWithFreshProof(label, fetchFn, options, proofKey, build) {
|
|
110
|
+
const attempts = Math.max(1, Math.floor(finiteOr(options?.attempts, 3)));
|
|
111
|
+
let delayMs = Math.max(0, finiteOr(options?.initialDelayMs, 1e3));
|
|
112
|
+
let lastError;
|
|
113
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
114
|
+
const { url, init } = await build(await nextProofIat(proofKey));
|
|
115
|
+
try {
|
|
116
|
+
return await fetchFn(url, init);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
lastError = err;
|
|
119
|
+
}
|
|
120
|
+
if (attempt < attempts - 1) {
|
|
121
|
+
await sleep(delayMs);
|
|
122
|
+
delayMs *= 2;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new import_errors.WriteTransportError(
|
|
126
|
+
`${label} failed after ${attempts} attempt(s): ${errorMessage(lastError)}`,
|
|
127
|
+
attempts,
|
|
128
|
+
lastError
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
132
|
+
0 && (module.exports = {
|
|
133
|
+
errorMessage,
|
|
134
|
+
finiteOr,
|
|
135
|
+
nextProofIat,
|
|
136
|
+
normalizeBaseUrl,
|
|
137
|
+
proofKeyFor,
|
|
138
|
+
resolveFetch,
|
|
139
|
+
sendWithFreshProof,
|
|
140
|
+
sleep
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=write-request.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/write-request.ts"],"sourcesContent":["/**\n * Transport shared by every builder call that authenticates with the\n * Personal Server Write API: the data writes of\n * {@link ../protocol/personal-server-write} and the derivative question\n * routes of {@link ../protocol/derivative-questions}.\n *\n * @remarks\n * Both sign a single-use Web3Signed proof per request, so both need the same\n * two things: a `fetch` wrapper that re-signs on every transport attempt, and\n * one process-wide record of the `iat` seconds already issued, so two proofs\n * for the same request identity can never come out byte-identical (the server\n * would reject the second as a replay). The record must be shared, not\n * per-module: a builder that polls one question every few milliseconds signs\n * the same `{ aud, method, uri, bodyHash, grantId }` many times a second.\n *\n * @internal\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { WriteRequestError, WriteTransportError } from \"../errors\";\n\n/**\n * Transport-level retry knobs shared by every Write API call.\n *\n * @remarks\n * Applies only when `fetch` **throws** (connection reset, DNS, a relay drop).\n * Every attempt signs a fresh proof, because the Personal Server consumes a\n * proof the moment it accepts it. A received HTTP response is never retried:\n * a 4xx/5xx is surfaced as a typed error.\n * @category Protocol\n */\nexport interface WriteTransportRetryOptions {\n /** Total attempts including the first (default 3). `1` disables retries. */\n attempts?: number;\n /** Delay before the first retry (ms); doubles per retry (default 1_000). */\n initialDelayMs?: number;\n}\n\n/** Strip trailing slashes so a base URL concatenates with a path. */\nexport function normalizeBaseUrl(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\n/** The caller's `fetch`, else the global one. */\nexport function resolveFetch(fetchFn: typeof fetch | undefined): typeof fetch {\n const resolved = fetchFn ?? globalThis.fetch;\n if (resolved === undefined) {\n throw new WriteRequestError(\"No fetch implementation available\");\n }\n return resolved;\n}\n\nexport function errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport function finiteOr(value: number | undefined, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * The Personal Server consumes every proof it accepts, and a Web3Signed\n * payload is fully determined by `{ aud, method, uri, bodyHash, grantId, iat,\n * exp }`, so two proofs for the same request signed within one second would\n * be byte-identical and the second rejected as a replay. Remember the\n * highest `iat` issued per request identity in this process and bump past\n * it when a second proof for the same identity falls inside the same second.\n *\n * A mark is kept for as long as the server can still remember the proof it\n * guards (its lifetime plus the verifier's clock skew), so a proof is never\n * re-issued while it could still be rejected as a replay: not by a burst,\n * and not by a wall clock stepping backwards (an identical request after a\n * step back waits for the clock instead of reusing the mark). Marks are\n * bucketed by their `iat` second, so pruning (once per second) only touches\n * the buckets that fell out of the retention window: the work per proof is\n * amortised constant and the map is bounded by the distinct requests signed\n * in the window.\n */\nconst issuedProofIats = new Map<string, number>();\n/** `iat` second -> identities whose current mark is that second. */\nconst issuedProofBuckets = new Map<number, Set<string>>();\nlet issuedProofIatsPrunedAtSec = 0;\n/** `buildWeb3SignedHeader`'s default `exp - iat`. */\nconst WEB3_SIGNED_PROOF_LIFETIME_SECONDS = 300;\n/** The verifier's tolerated clock skew (`verifyWeb3Signed`). */\nconst WEB3_SIGNED_CLOCK_SKEW_SECONDS = 60;\nconst PROOF_IAT_RETENTION_SECONDS =\n WEB3_SIGNED_PROOF_LIFETIME_SECONDS + WEB3_SIGNED_CLOCK_SKEW_SECONDS;\n/**\n * How far ahead of the clock a bumped `iat` may run when the proof is sent.\n * The verifier tolerates 60 s of skew. A burst of identical requests that\n * would need to run further ahead waits for the clock instead, so a proof is\n * never repeated; sustained identical requests are throttled to one per\n * second after a burst of this many.\n */\nconst PROOF_IAT_MAX_AHEAD_SECONDS = 30;\n\nfunction pruneIssuedProofIats(nowSec: number): void {\n if (issuedProofIatsPrunedAtSec === nowSec) return;\n issuedProofIatsPrunedAtSec = nowSec;\n const cutoff = nowSec - PROOF_IAT_RETENTION_SECONDS;\n // There is at most one bucket per second in the window, so this walk is\n // bounded by the window length, not by the number of marks.\n for (const [sec, keys] of issuedProofBuckets) {\n if (sec >= cutoff) continue;\n for (const key of keys) issuedProofIats.delete(key);\n issuedProofBuckets.delete(sec);\n }\n}\n\nfunction setIssuedProofIat(key: string, iat: number, previous?: number): void {\n if (previous !== undefined) {\n const bucket = issuedProofBuckets.get(previous);\n bucket?.delete(key);\n if (bucket?.size === 0) issuedProofBuckets.delete(previous);\n }\n issuedProofIats.set(key, iat);\n let bucket = issuedProofBuckets.get(iat);\n if (bucket === undefined) {\n bucket = new Set();\n issuedProofBuckets.set(iat, bucket);\n }\n bucket.add(key);\n}\n\n/**\n * Reserve the next `iat` for a request identity. The reservation is made\n * synchronously so concurrent callers never share a value; the returned\n * promise only waits when the reserved `iat` is further ahead of the clock\n * than {@link PROOF_IAT_MAX_AHEAD_SECONDS}.\n */\nexport function nextProofIat(proofKey: string): Promise<number> {\n const nowSec = Math.floor(Date.now() / 1000);\n pruneIssuedProofIats(nowSec);\n const last = issuedProofIats.get(proofKey);\n const iat = last === undefined ? nowSec : Math.max(nowSec, last + 1);\n setIssuedProofIat(proofKey, iat, last);\n const waitSec = iat - nowSec - PROOF_IAT_MAX_AHEAD_SECONDS;\n if (waitSec <= 0) return Promise.resolve(iat);\n return sleep(waitSec * 1000).then(() => iat);\n}\n\n/** The identity a proof is deduplicated by. */\nexport function proofKeyFor(parts: {\n aud: string;\n method: string;\n uri: string;\n grantId: string;\n signedBytes?: Uint8Array;\n}): string {\n // A digest, so a retained mark costs a fixed amount of memory whatever the\n // request looked like.\n return bytesToHex(\n sha256(\n new TextEncoder().encode(\n JSON.stringify([\n parts.aud,\n parts.method,\n parts.uri,\n parts.grantId,\n parts.signedBytes ? bytesToHex(sha256(parts.signedBytes)) : \"\",\n ]),\n ),\n ),\n );\n}\n\n/**\n * Send a request, re-signing it on every attempt. Only a thrown `fetch` is\n * retried; the proof builder and any received response are never retried.\n */\nexport async function sendWithFreshProof(\n label: string,\n fetchFn: typeof fetch,\n options: WriteTransportRetryOptions | undefined,\n proofKey: string,\n build: (iat: number) => Promise<{ url: string; init: RequestInit }>,\n): Promise<Response> {\n const attempts = Math.max(1, Math.floor(finiteOr(options?.attempts, 3)));\n let delayMs = Math.max(0, finiteOr(options?.initialDelayMs, 1_000));\n let lastError: unknown;\n for (let attempt = 0; attempt < attempts; attempt++) {\n const { url, init } = await build(await nextProofIat(proofKey));\n try {\n return await fetchFn(url, init);\n } catch (err) {\n lastError = err;\n }\n if (attempt < attempts - 1) {\n await sleep(delayMs);\n delayMs *= 2;\n }\n }\n throw new WriteTransportError(\n `${label} failed after ${attempts} attempt(s): ${errorMessage(lastError)}`,\n attempts,\n lastError,\n );\n}\n"],"mappings":"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","names":["bucket"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport shared by every builder call that authenticates with the
|
|
3
|
+
* Personal Server Write API: the data writes of
|
|
4
|
+
* {@link ../protocol/personal-server-write} and the derivative question
|
|
5
|
+
* routes of {@link ../protocol/derivative-questions}.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Both sign a single-use Web3Signed proof per request, so both need the same
|
|
9
|
+
* two things: a `fetch` wrapper that re-signs on every transport attempt, and
|
|
10
|
+
* one process-wide record of the `iat` seconds already issued, so two proofs
|
|
11
|
+
* for the same request identity can never come out byte-identical (the server
|
|
12
|
+
* would reject the second as a replay). The record must be shared, not
|
|
13
|
+
* per-module: a builder that polls one question every few milliseconds signs
|
|
14
|
+
* the same `{ aud, method, uri, bodyHash, grantId }` many times a second.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Transport-level retry knobs shared by every Write API call.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* Applies only when `fetch` **throws** (connection reset, DNS, a relay drop).
|
|
23
|
+
* Every attempt signs a fresh proof, because the Personal Server consumes a
|
|
24
|
+
* proof the moment it accepts it. A received HTTP response is never retried:
|
|
25
|
+
* a 4xx/5xx is surfaced as a typed error.
|
|
26
|
+
* @category Protocol
|
|
27
|
+
*/
|
|
28
|
+
export interface WriteTransportRetryOptions {
|
|
29
|
+
/** Total attempts including the first (default 3). `1` disables retries. */
|
|
30
|
+
attempts?: number;
|
|
31
|
+
/** Delay before the first retry (ms); doubles per retry (default 1_000). */
|
|
32
|
+
initialDelayMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Strip trailing slashes so a base URL concatenates with a path. */
|
|
35
|
+
export declare function normalizeBaseUrl(url: string): string;
|
|
36
|
+
/** The caller's `fetch`, else the global one. */
|
|
37
|
+
export declare function resolveFetch(fetchFn: typeof fetch | undefined): typeof fetch;
|
|
38
|
+
export declare function errorMessage(err: unknown): string;
|
|
39
|
+
export declare function finiteOr(value: number | undefined, fallback: number): number;
|
|
40
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Reserve the next `iat` for a request identity. The reservation is made
|
|
43
|
+
* synchronously so concurrent callers never share a value; the returned
|
|
44
|
+
* promise only waits when the reserved `iat` is further ahead of the clock
|
|
45
|
+
* than {@link PROOF_IAT_MAX_AHEAD_SECONDS}.
|
|
46
|
+
*/
|
|
47
|
+
export declare function nextProofIat(proofKey: string): Promise<number>;
|
|
48
|
+
/** The identity a proof is deduplicated by. */
|
|
49
|
+
export declare function proofKeyFor(parts: {
|
|
50
|
+
aud: string;
|
|
51
|
+
method: string;
|
|
52
|
+
uri: string;
|
|
53
|
+
grantId: string;
|
|
54
|
+
signedBytes?: Uint8Array;
|
|
55
|
+
}): string;
|
|
56
|
+
/**
|
|
57
|
+
* Send a request, re-signing it on every attempt. Only a thrown `fetch` is
|
|
58
|
+
* retried; the proof builder and any received response are never retried.
|
|
59
|
+
*/
|
|
60
|
+
export declare function sendWithFreshProof(label: string, fetchFn: typeof fetch, options: WriteTransportRetryOptions | undefined, proofKey: string, build: (iat: number) => Promise<{
|
|
61
|
+
url: string;
|
|
62
|
+
init: RequestInit;
|
|
63
|
+
}>): Promise<Response>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2";
|
|
2
|
+
import { bytesToHex } from "viem";
|
|
3
|
+
import { WriteRequestError, WriteTransportError } from "../errors.js";
|
|
4
|
+
function normalizeBaseUrl(url) {
|
|
5
|
+
return url.replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
function resolveFetch(fetchFn) {
|
|
8
|
+
const resolved = fetchFn ?? globalThis.fetch;
|
|
9
|
+
if (resolved === void 0) {
|
|
10
|
+
throw new WriteRequestError("No fetch implementation available");
|
|
11
|
+
}
|
|
12
|
+
return resolved;
|
|
13
|
+
}
|
|
14
|
+
function errorMessage(err) {
|
|
15
|
+
return err instanceof Error ? err.message : String(err);
|
|
16
|
+
}
|
|
17
|
+
function finiteOr(value, fallback) {
|
|
18
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
19
|
+
}
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
const issuedProofIats = /* @__PURE__ */ new Map();
|
|
24
|
+
const issuedProofBuckets = /* @__PURE__ */ new Map();
|
|
25
|
+
let issuedProofIatsPrunedAtSec = 0;
|
|
26
|
+
const WEB3_SIGNED_PROOF_LIFETIME_SECONDS = 300;
|
|
27
|
+
const WEB3_SIGNED_CLOCK_SKEW_SECONDS = 60;
|
|
28
|
+
const PROOF_IAT_RETENTION_SECONDS = WEB3_SIGNED_PROOF_LIFETIME_SECONDS + WEB3_SIGNED_CLOCK_SKEW_SECONDS;
|
|
29
|
+
const PROOF_IAT_MAX_AHEAD_SECONDS = 30;
|
|
30
|
+
function pruneIssuedProofIats(nowSec) {
|
|
31
|
+
if (issuedProofIatsPrunedAtSec === nowSec) return;
|
|
32
|
+
issuedProofIatsPrunedAtSec = nowSec;
|
|
33
|
+
const cutoff = nowSec - PROOF_IAT_RETENTION_SECONDS;
|
|
34
|
+
for (const [sec, keys] of issuedProofBuckets) {
|
|
35
|
+
if (sec >= cutoff) continue;
|
|
36
|
+
for (const key of keys) issuedProofIats.delete(key);
|
|
37
|
+
issuedProofBuckets.delete(sec);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function setIssuedProofIat(key, iat, previous) {
|
|
41
|
+
if (previous !== void 0) {
|
|
42
|
+
const bucket2 = issuedProofBuckets.get(previous);
|
|
43
|
+
bucket2?.delete(key);
|
|
44
|
+
if (bucket2?.size === 0) issuedProofBuckets.delete(previous);
|
|
45
|
+
}
|
|
46
|
+
issuedProofIats.set(key, iat);
|
|
47
|
+
let bucket = issuedProofBuckets.get(iat);
|
|
48
|
+
if (bucket === void 0) {
|
|
49
|
+
bucket = /* @__PURE__ */ new Set();
|
|
50
|
+
issuedProofBuckets.set(iat, bucket);
|
|
51
|
+
}
|
|
52
|
+
bucket.add(key);
|
|
53
|
+
}
|
|
54
|
+
function nextProofIat(proofKey) {
|
|
55
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
56
|
+
pruneIssuedProofIats(nowSec);
|
|
57
|
+
const last = issuedProofIats.get(proofKey);
|
|
58
|
+
const iat = last === void 0 ? nowSec : Math.max(nowSec, last + 1);
|
|
59
|
+
setIssuedProofIat(proofKey, iat, last);
|
|
60
|
+
const waitSec = iat - nowSec - PROOF_IAT_MAX_AHEAD_SECONDS;
|
|
61
|
+
if (waitSec <= 0) return Promise.resolve(iat);
|
|
62
|
+
return sleep(waitSec * 1e3).then(() => iat);
|
|
63
|
+
}
|
|
64
|
+
function proofKeyFor(parts) {
|
|
65
|
+
return bytesToHex(
|
|
66
|
+
sha256(
|
|
67
|
+
new TextEncoder().encode(
|
|
68
|
+
JSON.stringify([
|
|
69
|
+
parts.aud,
|
|
70
|
+
parts.method,
|
|
71
|
+
parts.uri,
|
|
72
|
+
parts.grantId,
|
|
73
|
+
parts.signedBytes ? bytesToHex(sha256(parts.signedBytes)) : ""
|
|
74
|
+
])
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
async function sendWithFreshProof(label, fetchFn, options, proofKey, build) {
|
|
80
|
+
const attempts = Math.max(1, Math.floor(finiteOr(options?.attempts, 3)));
|
|
81
|
+
let delayMs = Math.max(0, finiteOr(options?.initialDelayMs, 1e3));
|
|
82
|
+
let lastError;
|
|
83
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
84
|
+
const { url, init } = await build(await nextProofIat(proofKey));
|
|
85
|
+
try {
|
|
86
|
+
return await fetchFn(url, init);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
lastError = err;
|
|
89
|
+
}
|
|
90
|
+
if (attempt < attempts - 1) {
|
|
91
|
+
await sleep(delayMs);
|
|
92
|
+
delayMs *= 2;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throw new WriteTransportError(
|
|
96
|
+
`${label} failed after ${attempts} attempt(s): ${errorMessage(lastError)}`,
|
|
97
|
+
attempts,
|
|
98
|
+
lastError
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
errorMessage,
|
|
103
|
+
finiteOr,
|
|
104
|
+
nextProofIat,
|
|
105
|
+
normalizeBaseUrl,
|
|
106
|
+
proofKeyFor,
|
|
107
|
+
resolveFetch,
|
|
108
|
+
sendWithFreshProof,
|
|
109
|
+
sleep
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=write-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/write-request.ts"],"sourcesContent":["/**\n * Transport shared by every builder call that authenticates with the\n * Personal Server Write API: the data writes of\n * {@link ../protocol/personal-server-write} and the derivative question\n * routes of {@link ../protocol/derivative-questions}.\n *\n * @remarks\n * Both sign a single-use Web3Signed proof per request, so both need the same\n * two things: a `fetch` wrapper that re-signs on every transport attempt, and\n * one process-wide record of the `iat` seconds already issued, so two proofs\n * for the same request identity can never come out byte-identical (the server\n * would reject the second as a replay). The record must be shared, not\n * per-module: a builder that polls one question every few milliseconds signs\n * the same `{ aud, method, uri, bodyHash, grantId }` many times a second.\n *\n * @internal\n */\n\nimport { sha256 } from \"@noble/hashes/sha2\";\nimport { bytesToHex } from \"viem\";\nimport { WriteRequestError, WriteTransportError } from \"../errors\";\n\n/**\n * Transport-level retry knobs shared by every Write API call.\n *\n * @remarks\n * Applies only when `fetch` **throws** (connection reset, DNS, a relay drop).\n * Every attempt signs a fresh proof, because the Personal Server consumes a\n * proof the moment it accepts it. A received HTTP response is never retried:\n * a 4xx/5xx is surfaced as a typed error.\n * @category Protocol\n */\nexport interface WriteTransportRetryOptions {\n /** Total attempts including the first (default 3). `1` disables retries. */\n attempts?: number;\n /** Delay before the first retry (ms); doubles per retry (default 1_000). */\n initialDelayMs?: number;\n}\n\n/** Strip trailing slashes so a base URL concatenates with a path. */\nexport function normalizeBaseUrl(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\n/** The caller's `fetch`, else the global one. */\nexport function resolveFetch(fetchFn: typeof fetch | undefined): typeof fetch {\n const resolved = fetchFn ?? globalThis.fetch;\n if (resolved === undefined) {\n throw new WriteRequestError(\"No fetch implementation available\");\n }\n return resolved;\n}\n\nexport function errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\nexport function finiteOr(value: number | undefined, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : fallback;\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * The Personal Server consumes every proof it accepts, and a Web3Signed\n * payload is fully determined by `{ aud, method, uri, bodyHash, grantId, iat,\n * exp }`, so two proofs for the same request signed within one second would\n * be byte-identical and the second rejected as a replay. Remember the\n * highest `iat` issued per request identity in this process and bump past\n * it when a second proof for the same identity falls inside the same second.\n *\n * A mark is kept for as long as the server can still remember the proof it\n * guards (its lifetime plus the verifier's clock skew), so a proof is never\n * re-issued while it could still be rejected as a replay: not by a burst,\n * and not by a wall clock stepping backwards (an identical request after a\n * step back waits for the clock instead of reusing the mark). Marks are\n * bucketed by their `iat` second, so pruning (once per second) only touches\n * the buckets that fell out of the retention window: the work per proof is\n * amortised constant and the map is bounded by the distinct requests signed\n * in the window.\n */\nconst issuedProofIats = new Map<string, number>();\n/** `iat` second -> identities whose current mark is that second. */\nconst issuedProofBuckets = new Map<number, Set<string>>();\nlet issuedProofIatsPrunedAtSec = 0;\n/** `buildWeb3SignedHeader`'s default `exp - iat`. */\nconst WEB3_SIGNED_PROOF_LIFETIME_SECONDS = 300;\n/** The verifier's tolerated clock skew (`verifyWeb3Signed`). */\nconst WEB3_SIGNED_CLOCK_SKEW_SECONDS = 60;\nconst PROOF_IAT_RETENTION_SECONDS =\n WEB3_SIGNED_PROOF_LIFETIME_SECONDS + WEB3_SIGNED_CLOCK_SKEW_SECONDS;\n/**\n * How far ahead of the clock a bumped `iat` may run when the proof is sent.\n * The verifier tolerates 60 s of skew. A burst of identical requests that\n * would need to run further ahead waits for the clock instead, so a proof is\n * never repeated; sustained identical requests are throttled to one per\n * second after a burst of this many.\n */\nconst PROOF_IAT_MAX_AHEAD_SECONDS = 30;\n\nfunction pruneIssuedProofIats(nowSec: number): void {\n if (issuedProofIatsPrunedAtSec === nowSec) return;\n issuedProofIatsPrunedAtSec = nowSec;\n const cutoff = nowSec - PROOF_IAT_RETENTION_SECONDS;\n // There is at most one bucket per second in the window, so this walk is\n // bounded by the window length, not by the number of marks.\n for (const [sec, keys] of issuedProofBuckets) {\n if (sec >= cutoff) continue;\n for (const key of keys) issuedProofIats.delete(key);\n issuedProofBuckets.delete(sec);\n }\n}\n\nfunction setIssuedProofIat(key: string, iat: number, previous?: number): void {\n if (previous !== undefined) {\n const bucket = issuedProofBuckets.get(previous);\n bucket?.delete(key);\n if (bucket?.size === 0) issuedProofBuckets.delete(previous);\n }\n issuedProofIats.set(key, iat);\n let bucket = issuedProofBuckets.get(iat);\n if (bucket === undefined) {\n bucket = new Set();\n issuedProofBuckets.set(iat, bucket);\n }\n bucket.add(key);\n}\n\n/**\n * Reserve the next `iat` for a request identity. The reservation is made\n * synchronously so concurrent callers never share a value; the returned\n * promise only waits when the reserved `iat` is further ahead of the clock\n * than {@link PROOF_IAT_MAX_AHEAD_SECONDS}.\n */\nexport function nextProofIat(proofKey: string): Promise<number> {\n const nowSec = Math.floor(Date.now() / 1000);\n pruneIssuedProofIats(nowSec);\n const last = issuedProofIats.get(proofKey);\n const iat = last === undefined ? nowSec : Math.max(nowSec, last + 1);\n setIssuedProofIat(proofKey, iat, last);\n const waitSec = iat - nowSec - PROOF_IAT_MAX_AHEAD_SECONDS;\n if (waitSec <= 0) return Promise.resolve(iat);\n return sleep(waitSec * 1000).then(() => iat);\n}\n\n/** The identity a proof is deduplicated by. */\nexport function proofKeyFor(parts: {\n aud: string;\n method: string;\n uri: string;\n grantId: string;\n signedBytes?: Uint8Array;\n}): string {\n // A digest, so a retained mark costs a fixed amount of memory whatever the\n // request looked like.\n return bytesToHex(\n sha256(\n new TextEncoder().encode(\n JSON.stringify([\n parts.aud,\n parts.method,\n parts.uri,\n parts.grantId,\n parts.signedBytes ? bytesToHex(sha256(parts.signedBytes)) : \"\",\n ]),\n ),\n ),\n );\n}\n\n/**\n * Send a request, re-signing it on every attempt. Only a thrown `fetch` is\n * retried; the proof builder and any received response are never retried.\n */\nexport async function sendWithFreshProof(\n label: string,\n fetchFn: typeof fetch,\n options: WriteTransportRetryOptions | undefined,\n proofKey: string,\n build: (iat: number) => Promise<{ url: string; init: RequestInit }>,\n): Promise<Response> {\n const attempts = Math.max(1, Math.floor(finiteOr(options?.attempts, 3)));\n let delayMs = Math.max(0, finiteOr(options?.initialDelayMs, 1_000));\n let lastError: unknown;\n for (let attempt = 0; attempt < attempts; attempt++) {\n const { url, init } = await build(await nextProofIat(proofKey));\n try {\n return await fetchFn(url, init);\n } catch (err) {\n lastError = err;\n }\n if (attempt < attempts - 1) {\n await sleep(delayMs);\n delayMs *= 2;\n }\n }\n throw new WriteTransportError(\n `${label} failed after ${attempts} attempt(s): ${errorMessage(lastError)}`,\n attempts,\n lastError,\n );\n}\n"],"mappings":"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","names":["bucket"]}
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* proof single-use; `lineage` is the body's top-level field (JSON) or
|
|
15
15
|
* the metadata object's field (binary), validated per
|
|
16
16
|
* docs/derivative-data-api.md and mirrored to `$lineage`
|
|
17
|
+
* - derivative questions (`/v1/derivatives/questions`): the same builder
|
|
18
|
+
* credential as a write (bearer + X-Vana-Write-Signature over the
|
|
19
|
+
* request PATH, query string excluded, as the server verifies it),
|
|
20
|
+
* authorized against `write:<derivedScope>`, compact JSON bodies, the
|
|
21
|
+
* consent rule (every source scope read-granted to the builder), the
|
|
22
|
+
* cycle guard, and the 404 a builder gets for a question it did not
|
|
23
|
+
* register
|
|
17
24
|
* - lineage reads on both the Personal Server and the gateway: Web3Signed
|
|
18
25
|
* over the bare path (`/lineage[/:version]`, the version is a path
|
|
19
26
|
* segment, any query is 400), grant view from the signed `grantId` claim
|
|
@@ -55,6 +62,30 @@ export interface MockPersonalServerOptions {
|
|
|
55
62
|
status?: "stored" | "syncing";
|
|
56
63
|
sessionTtlSeconds?: number;
|
|
57
64
|
now?: () => number;
|
|
65
|
+
/** Answer every `/v1/derivatives` route 503, as a server with no compute. */
|
|
66
|
+
computeUnavailable?: boolean;
|
|
67
|
+
}
|
|
68
|
+
/** A question registration the mock server holds. */
|
|
69
|
+
export interface MockQuestion {
|
|
70
|
+
questionId: string;
|
|
71
|
+
derivedScope: string;
|
|
72
|
+
sourceScopes: string[];
|
|
73
|
+
question: string;
|
|
74
|
+
model: string | null;
|
|
75
|
+
registeredBy: {
|
|
76
|
+
kind: "owner";
|
|
77
|
+
} | {
|
|
78
|
+
kind: "builder";
|
|
79
|
+
builder: Address;
|
|
80
|
+
grantId: string;
|
|
81
|
+
};
|
|
82
|
+
status: "pending" | "ready" | "failed" | "stale";
|
|
83
|
+
error: string | null;
|
|
84
|
+
createdAt: string;
|
|
85
|
+
updatedAt: string;
|
|
86
|
+
lastComputedAt: string | null;
|
|
87
|
+
derivedVersion: number | null;
|
|
88
|
+
derivedCollectedAt: string | null;
|
|
58
89
|
}
|
|
59
90
|
export interface MockRequestLog {
|
|
60
91
|
method: string;
|
|
@@ -75,6 +106,26 @@ export interface MockPersonalServer {
|
|
|
75
106
|
respondNextWith(status: number, body: unknown): void;
|
|
76
107
|
/** Sessions minted (token -> record). */
|
|
77
108
|
sessions: Map<string, MockSession>;
|
|
109
|
+
/** Question registrations (id -> row), in registration order. */
|
|
110
|
+
questions: Map<string, MockQuestion>;
|
|
111
|
+
/**
|
|
112
|
+
* Forget every minted session, the way a restarted Personal Server does:
|
|
113
|
+
* the next call with an old bearer answers 401.
|
|
114
|
+
*/
|
|
115
|
+
dropSessions(): void;
|
|
116
|
+
/**
|
|
117
|
+
* Settle a question the way a compute would: set its status and, for a
|
|
118
|
+
* `ready` one, store the derived record the builder then reads.
|
|
119
|
+
*/
|
|
120
|
+
settleQuestion(questionId: string, outcome: {
|
|
121
|
+
status: "ready";
|
|
122
|
+
data?: Record<string, unknown>;
|
|
123
|
+
} | {
|
|
124
|
+
status: "failed";
|
|
125
|
+
error: string;
|
|
126
|
+
} | {
|
|
127
|
+
status: "stale" | "pending";
|
|
128
|
+
}): void;
|
|
78
129
|
}
|
|
79
130
|
export interface MockSession {
|
|
80
131
|
token: string;
|