@opendatalabs/vana-sdk 3.19.0 → 3.20.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 +82 -15
- package/dist/auth/web3-signed-builder.cjs +3 -0
- package/dist/auth/web3-signed-builder.cjs.map +1 -1
- package/dist/auth/web3-signed-builder.d.ts +14 -0
- package/dist/auth/web3-signed-builder.js +3 -0
- package/dist/auth/web3-signed-builder.js.map +1 -1
- package/dist/direct/controller.cjs +3 -2
- package/dist/direct/controller.cjs.map +1 -1
- package/dist/direct/controller.d.ts +19 -2
- package/dist/direct/controller.js +3 -2
- package/dist/direct/controller.js.map +1 -1
- package/dist/errors.cjs +13 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +21 -2
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.browser.js +69 -25
- package/dist/index.browser.js.map +2 -2
- package/dist/index.node.cjs +70 -25
- package/dist/index.node.cjs.map +2 -2
- package/dist/index.node.js +69 -25
- package/dist/index.node.js.map +2 -2
- package/dist/protocol/derivative-questions.cjs +42 -25
- package/dist/protocol/derivative-questions.cjs.map +1 -1
- package/dist/protocol/derivative-questions.d.ts +51 -10
- package/dist/protocol/derivative-questions.js +47 -26
- package/dist/protocol/derivative-questions.js.map +1 -1
- package/dist/protocol/write-request.cjs +14 -0
- package/dist/protocol/write-request.cjs.map +1 -1
- package/dist/protocol/write-request.d.ts +12 -0
- package/dist/protocol/write-request.js +13 -0
- package/dist/protocol/write-request.js.map +1 -1
- package/dist/server.cjs +28 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +29 -1
- package/dist/server.js.map +1 -1
- package/dist/tests/mock-personal-server.d.ts +9 -6
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { buildWeb3SignedHeader } from "../auth/web3-signed-builder.js";
|
|
|
3
3
|
import {
|
|
4
4
|
DerivativeComputeUnavailableError,
|
|
5
5
|
DerivativeCycleError,
|
|
6
|
+
DerivativeDerivedScopeRequiredError,
|
|
6
7
|
DerivativeQuestionFailedError,
|
|
7
8
|
DerivativeQuestionInvalidError,
|
|
8
9
|
DerivativeQuestionNotFoundError,
|
|
@@ -15,7 +16,9 @@ import {
|
|
|
15
16
|
WriteUnauthorizedError
|
|
16
17
|
} from "../errors.js";
|
|
17
18
|
import { assertDerivedScopeNaming } from "./lineage.js";
|
|
18
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
readPersonalServerErrorBody
|
|
21
|
+
} from "./personal-server-error-body.js";
|
|
19
22
|
import {
|
|
20
23
|
readPersonalServerData
|
|
21
24
|
} from "./personal-server-data.js";
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
} from "./personal-server-write.js";
|
|
26
29
|
import {
|
|
27
30
|
errorMessage,
|
|
31
|
+
freshProofNonce,
|
|
28
32
|
normalizeBaseUrl,
|
|
29
33
|
proofKeyFor,
|
|
30
34
|
resolveFetch,
|
|
@@ -80,12 +84,7 @@ const DerivativeQuestionSchema = z.object({
|
|
|
80
84
|
const QuestionListSchema = z.object({
|
|
81
85
|
questions: z.array(DerivativeQuestionSchema)
|
|
82
86
|
});
|
|
83
|
-
const QuestionRecomputeResultSchema =
|
|
84
|
-
questionId: z.string().min(1),
|
|
85
|
-
derivedScope: z.string().min(1),
|
|
86
|
-
/** `pending` when the question was never computed, else `stale`. */
|
|
87
|
-
status: QuestionStatusSchema
|
|
88
|
-
});
|
|
87
|
+
const QuestionRecomputeResultSchema = DerivativeQuestionSchema;
|
|
89
88
|
const QuestionDeleteResultSchema = z.object({
|
|
90
89
|
questionId: z.string().min(1),
|
|
91
90
|
deleted: z.literal(true)
|
|
@@ -153,8 +152,18 @@ async function resolveSession(params, resolved, force) {
|
|
|
153
152
|
cache.set(resolved.cacheKey, session);
|
|
154
153
|
return session;
|
|
155
154
|
}
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
const PROOF_FAILURE_CODES = /* @__PURE__ */ new Set([
|
|
156
|
+
"WRITE_ATTRIBUTION_REQUIRED",
|
|
157
|
+
"WRITE_ATTRIBUTION_INVALID",
|
|
158
|
+
"WRITE_ATTRIBUTION_SIGNER_MISMATCH",
|
|
159
|
+
"WRITE_ATTRIBUTION_GRANT_MISMATCH",
|
|
160
|
+
"WRITE_ATTRIBUTION_REPLAY"
|
|
161
|
+
]);
|
|
162
|
+
function isStaleSession(errorCode) {
|
|
163
|
+
return errorCode === null || !PROOF_FAILURE_CODES.has(errorCode);
|
|
164
|
+
}
|
|
165
|
+
async function questionErrorFromResponse(response, body) {
|
|
166
|
+
const { errorCode, message, details } = body ?? await readPersonalServerErrorBody(response);
|
|
158
167
|
const text = message ?? `Derivative question request failed: ${response.status} ${response.statusText}`;
|
|
159
168
|
switch (errorCode) {
|
|
160
169
|
case "DERIVATIVE_SOURCE_NOT_GRANTED":
|
|
@@ -173,6 +182,8 @@ async function questionErrorFromResponse(response) {
|
|
|
173
182
|
);
|
|
174
183
|
case "DERIVATIVE_QUESTION_NOT_FOUND":
|
|
175
184
|
return new DerivativeQuestionNotFoundError(text, errorCode, details);
|
|
185
|
+
case "DERIVATIVE_DERIVED_SCOPE_REQUIRED":
|
|
186
|
+
return new DerivativeDerivedScopeRequiredError(text, errorCode, details);
|
|
176
187
|
default:
|
|
177
188
|
break;
|
|
178
189
|
}
|
|
@@ -202,7 +213,7 @@ async function sendOnce(params, resolved, session, spec, bodyBytes) {
|
|
|
202
213
|
proofKeyFor({
|
|
203
214
|
aud: session.audience,
|
|
204
215
|
method: spec.method,
|
|
205
|
-
uri: spec.
|
|
216
|
+
uri: spec.target,
|
|
206
217
|
grantId: session.grantId,
|
|
207
218
|
signedBytes: bodyBytes
|
|
208
219
|
}),
|
|
@@ -218,17 +229,22 @@ async function sendOnce(params, resolved, session, spec, bodyBytes) {
|
|
|
218
229
|
await buildWeb3SignedHeader({
|
|
219
230
|
signMessage: session.signer.signMessage,
|
|
220
231
|
aud: session.audience,
|
|
221
|
-
// The
|
|
222
|
-
//
|
|
223
|
-
|
|
232
|
+
// The proof commits to the whole request target, query included:
|
|
233
|
+
// `?derivedScope=` is what the list route authorizes against, and a
|
|
234
|
+
// proof that did not cover it would authorize any other scope.
|
|
235
|
+
uri: spec.target,
|
|
224
236
|
method: spec.method,
|
|
225
237
|
body: bodyBytes,
|
|
226
238
|
grantId: session.grantId,
|
|
239
|
+
// Fresh per attempt, so a retry after a thrown `fetch` is never the
|
|
240
|
+
// proof the server may already have consumed, and so two identical
|
|
241
|
+
// polls inside one second stay distinct.
|
|
242
|
+
nonce: freshProofNonce(),
|
|
227
243
|
iat
|
|
228
244
|
})
|
|
229
245
|
);
|
|
230
246
|
return {
|
|
231
|
-
url: `${resolved.baseUrl}${spec.
|
|
247
|
+
url: `${resolved.baseUrl}${spec.target}`,
|
|
232
248
|
init: {
|
|
233
249
|
method: spec.method,
|
|
234
250
|
headers,
|
|
@@ -244,12 +260,17 @@ async function sendQuestionRequest(params, spec, schema) {
|
|
|
244
260
|
const bodyBytes = spec.body === void 0 ? void 0 : new TextEncoder().encode(JSON.stringify(spec.body));
|
|
245
261
|
let session = await resolveSession(params, resolved, false);
|
|
246
262
|
let response = await sendOnce(params, resolved, session, spec, bodyBytes);
|
|
263
|
+
let errorBody;
|
|
247
264
|
if (response.status === 401) {
|
|
248
|
-
|
|
249
|
-
|
|
265
|
+
errorBody = await readPersonalServerErrorBody(response);
|
|
266
|
+
if (isStaleSession(errorBody.errorCode)) {
|
|
267
|
+
session = await resolveSession(params, resolved, true);
|
|
268
|
+
response = await sendOnce(params, resolved, session, spec, bodyBytes);
|
|
269
|
+
errorBody = void 0;
|
|
270
|
+
}
|
|
250
271
|
}
|
|
251
272
|
if (!response.ok) {
|
|
252
|
-
throw await questionErrorFromResponse(response);
|
|
273
|
+
throw await questionErrorFromResponse(response, errorBody);
|
|
253
274
|
}
|
|
254
275
|
let body;
|
|
255
276
|
try {
|
|
@@ -343,7 +364,7 @@ async function registerQuestion(params) {
|
|
|
343
364
|
params,
|
|
344
365
|
{
|
|
345
366
|
method: "POST",
|
|
346
|
-
|
|
367
|
+
target: DERIVATIVE_QUESTIONS_PATH,
|
|
347
368
|
body,
|
|
348
369
|
label: "Register derivative question"
|
|
349
370
|
},
|
|
@@ -351,10 +372,10 @@ async function registerQuestion(params) {
|
|
|
351
372
|
);
|
|
352
373
|
}
|
|
353
374
|
async function getQuestion(params) {
|
|
354
|
-
const
|
|
375
|
+
const target = assertQuestionId(params.questionId);
|
|
355
376
|
return sendQuestionRequest(
|
|
356
377
|
params,
|
|
357
|
-
{ method: "GET",
|
|
378
|
+
{ method: "GET", target, label: "Read derivative question" },
|
|
358
379
|
DerivativeQuestionSchema
|
|
359
380
|
);
|
|
360
381
|
}
|
|
@@ -364,12 +385,12 @@ async function listQuestions(params) {
|
|
|
364
385
|
"derivedScope is required; a builder may only list its own questions on a scope it may write"
|
|
365
386
|
);
|
|
366
387
|
}
|
|
388
|
+
const target = `${DERIVATIVE_QUESTIONS_PATH}?derivedScope=${encodeURIComponent(params.derivedScope)}`;
|
|
367
389
|
const { questions } = await sendQuestionRequest(
|
|
368
390
|
params,
|
|
369
391
|
{
|
|
370
392
|
method: "GET",
|
|
371
|
-
|
|
372
|
-
query: `?derivedScope=${encodeURIComponent(params.derivedScope)}`,
|
|
393
|
+
target,
|
|
373
394
|
label: "List derivative questions"
|
|
374
395
|
},
|
|
375
396
|
QuestionListSchema
|
|
@@ -377,18 +398,18 @@ async function listQuestions(params) {
|
|
|
377
398
|
return questions;
|
|
378
399
|
}
|
|
379
400
|
async function recomputeQuestion(params) {
|
|
380
|
-
const
|
|
401
|
+
const target = `${assertQuestionId(params.questionId)}/recompute`;
|
|
381
402
|
return sendQuestionRequest(
|
|
382
403
|
params,
|
|
383
|
-
{ method: "POST",
|
|
404
|
+
{ method: "POST", target, label: "Recompute derivative question" },
|
|
384
405
|
QuestionRecomputeResultSchema
|
|
385
406
|
);
|
|
386
407
|
}
|
|
387
408
|
async function deleteQuestion(params) {
|
|
388
|
-
const
|
|
409
|
+
const target = assertQuestionId(params.questionId);
|
|
389
410
|
return sendQuestionRequest(
|
|
390
411
|
params,
|
|
391
|
-
{ method: "DELETE",
|
|
412
|
+
{ method: "DELETE", target, label: "Delete derivative question" },
|
|
392
413
|
QuestionDeleteResultSchema
|
|
393
414
|
);
|
|
394
415
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/protocol/derivative-questions.ts"],"sourcesContent":["/**\n * Builder-side client for the Personal Server derivative question API.\n *\n * @remarks\n * A question is a standing prompt over the owner's source scopes. The\n * Personal Server answers it locally (the raw sources never leave the\n * machine except through its inference call) and writes the answer into the\n * derived scope as an ordinary derivative record, with lineage pointing at\n * the sources. The builder then reads the derived scope with its normal read\n * grant. Every source change re-runs the question, so a builder registers it\n * once and keeps reading a scope that stays up to date.\n *\n * One grant carries the whole pipeline, and it needs all three of:\n *\n * - a bare read entry for every source scope (the answer exposes them, so\n * the server refuses the registration otherwise:\n * `DERIVATIVE_SOURCE_NOT_GRANTED`),\n * - a bare read entry for the derived scope (to read the answer back),\n * - `write:<derivedScope>` (the credential the question routes authorize\n * against).\n *\n * Authentication is the Write API's, with no new credential: the write\n * session bearer from {@link openWriteSession} plus a fresh, single-use\n * `X-Vana-Write-Signature` Web3Signed proof over every request, carrying the\n * grant id as a signed claim. These helpers own that: they open one session\n * per `{ signer, Personal Server, grant }`, reuse it across calls, sign a new\n * proof per request, and re-open the session once when a call comes back 401\n * (the Personal Server keeps sessions in memory and forgets them when it\n * restarts).\n *\n * @category Protocol\n */\n\nimport { z } from \"zod\";\nimport { buildWeb3SignedHeader } from \"../auth/web3-signed-builder\";\nimport {\n DerivativeComputeUnavailableError,\n DerivativeCycleError,\n DerivativeQuestionFailedError,\n DerivativeQuestionInvalidError,\n DerivativeQuestionNotFoundError,\n DerivativeQuestionRejectedError,\n DerivativeQuestionTimeoutError,\n DerivativeSourceNotGrantedError,\n WriteConflictError,\n WriteForbiddenError,\n WriteRequestError,\n WriteUnauthorizedError,\n type PersonalServerWriteError,\n} from \"../errors\";\nimport { assertDerivedScopeNaming } from \"./lineage\";\nimport { readPersonalServerErrorBody } from \"./personal-server-error-body\";\nimport {\n readPersonalServerData,\n type ReadPersonalServerDataParams,\n} from \"./personal-server-data\";\nimport type { DataFileEnvelope } from \"./data-file\";\nimport {\n openWriteSession,\n WRITE_SIGNATURE_HEADER,\n type WriteSession,\n} from \"./personal-server-write\";\nimport {\n errorMessage,\n normalizeBaseUrl,\n proofKeyFor,\n resolveFetch,\n sendWithFreshProof,\n sleep,\n type WriteTransportRetryOptions,\n} from \"./write-request\";\nimport {\n resolveWriteSigner,\n type ResolveWriteSignerOptions,\n type WriteSignerSource,\n} from \"./write-signer\";\n\n/** Path the question routes are mounted at. */\nexport const DERIVATIVE_QUESTIONS_PATH = \"/v1/derivatives/questions\";\n/** The most source scopes one question may read. */\nexport const MAX_QUESTION_SOURCE_SCOPES = 16;\n/** The longest question text the Personal Server accepts. */\nexport const MAX_QUESTION_CHARS = 8_000;\n/** The longest model id the Personal Server accepts. */\nexport const MAX_QUESTION_MODEL_CHARS = 128;\n/** Model ids as providers spell them (`z-ai/glm-5.2`, `gpt-4o-mini`, ...). */\nconst MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;\n/** How long {@link waitForQuestion} polls before giving up. */\nexport const DEFAULT_QUESTION_TIMEOUT_MS = 120_000;\n/** How long {@link waitForQuestion} waits between polls. */\nexport const DEFAULT_QUESTION_POLL_INTERVAL_MS = 2_000;\n/** Re-open a session this long before its token expires. */\nconst SESSION_REFRESH_SKEW_MS = 30_000;\n\n/** Every state a question can be in. */\nexport const QUESTION_STATUSES = [\n \"pending\",\n \"ready\",\n \"failed\",\n \"stale\",\n] as const;\n\n/**\n * `pending` (never computed) -> `ready` | `failed`; a source change or an\n * explicit recompute puts a computed question back to `stale`, which\n * settles as `ready` or `failed` again.\n */\nexport const QuestionStatusSchema = z.enum(QUESTION_STATUSES);\n\n/** @see {@link QuestionStatusSchema} */\nexport type QuestionStatus = z.infer<typeof QuestionStatusSchema>;\n\n/** Who registered the question: the owner, or a builder under a grant. */\nexport const QuestionRegisteredBySchema = z.union([\n z.object({ kind: z.literal(\"owner\") }),\n z.object({\n kind: z.literal(\"builder\"),\n builder: z.string(),\n grantId: z.string(),\n }),\n]);\n\n/** @see {@link QuestionRegisteredBySchema} */\nexport type QuestionRegisteredBy = z.infer<typeof QuestionRegisteredBySchema>;\n\n// The server always sends these; `nullish` keeps a Personal Server that\n// omits one readable rather than failing the whole call on a missing field.\nconst nullableString = z\n .string()\n .nullish()\n .transform((value) => value ?? null);\n\n/**\n * A question registration as the Personal Server reports it (the answer of\n * register, get and list).\n */\nexport const DerivativeQuestionSchema = z.object({\n questionId: z.string().min(1),\n derivedScope: z.string().min(1),\n sourceScopes: z.array(z.string()),\n question: z.string(),\n /** The model override, or `null` for the server's default. */\n model: nullableString,\n registeredBy: QuestionRegisteredBySchema,\n status: QuestionStatusSchema,\n /** A short reason, set only while `status` is `failed`. */\n error: nullableString,\n createdAt: z.string(),\n updatedAt: nullableString,\n /** When the last compute finished, or `null` while `pending`. */\n lastComputedAt: nullableString,\n /** Local version of the derived record the last compute wrote. */\n derivedVersion: z\n .number()\n .nullish()\n .transform((value) => value ?? null),\n derivedCollectedAt: nullableString,\n});\n\n/** @see {@link DerivativeQuestionSchema} */\nexport type DerivativeQuestion = z.infer<typeof DerivativeQuestionSchema>;\n\nconst QuestionListSchema = z.object({\n questions: z.array(DerivativeQuestionSchema),\n});\n\n/** The 202 answer of a recompute request. */\nexport const QuestionRecomputeResultSchema = z.object({\n questionId: z.string().min(1),\n derivedScope: z.string().min(1),\n /** `pending` when the question was never computed, else `stale`. */\n status: QuestionStatusSchema,\n});\n\n/** @see {@link QuestionRecomputeResultSchema} */\nexport type QuestionRecomputeResult = z.infer<\n typeof QuestionRecomputeResultSchema\n>;\n\n/** The answer of a delete request. */\nexport const QuestionDeleteResultSchema = z.object({\n questionId: z.string().min(1),\n deleted: z.literal(true),\n});\n\n/** @see {@link QuestionDeleteResultSchema} */\nexport type QuestionDeleteResult = z.infer<typeof QuestionDeleteResultSchema>;\n\n/**\n * Connection, credential and transport shared by every question call.\n *\n * @remarks\n * The write session is opened on demand and reused for every later call\n * made with the same `signer` object, Personal Server, audience, grant and\n * `fetch`; a 401 re-opens it once and replays the call.\n */\nexport interface DerivativeQuestionAuthParams extends ResolveWriteSignerOptions {\n /** Personal Server origin, e.g. `https://ps.example.com`. */\n personalServerUrl: string;\n /** Builder key: a viem `LocalAccount`, `WalletClient`, or `{ signMessage }`. */\n signer: WriteSignerSource;\n /**\n * The grant the call runs under. It must carry `write:<derivedScope>`, a\n * bare read entry for the derived scope, and a bare read entry for every\n * source scope.\n */\n grantId: string;\n /** Web3Signed audience; defaults to `personalServerUrl`. */\n audience?: string;\n /** `fetch` to use; defaults to `globalThis.fetch`. */\n fetch?: typeof fetch;\n /** Extra request headers. */\n headers?: HeadersInit;\n retry?: WriteTransportRetryOptions;\n /** Aborts the request (and, for {@link waitForQuestion}, the polling). */\n signal?: AbortSignal;\n}\n\nexport interface RegisterQuestionParams extends DerivativeQuestionAuthParams {\n /**\n * The scope the answer is written into. Must not share its first\n * dot-segment with any source scope, so put derivatives in the app's own\n * namespace.\n */\n derivedScope: string;\n /**\n * The scopes the question reads: 1 to\n * {@link MAX_QUESTION_SOURCE_SCOPES} distinct scopes, none of them the\n * derived scope. They do not have to hold data yet: the question computes\n * once they do.\n */\n sourceScopes: readonly string[];\n /** The prompt, 1 to {@link MAX_QUESTION_CHARS} characters. */\n question: string;\n /** Model id override; omitted = the Personal Server's default model. */\n model?: string;\n}\n\nexport interface GetQuestionParams extends DerivativeQuestionAuthParams {\n questionId: string;\n}\n\nexport interface ListQuestionsParams extends DerivativeQuestionAuthParams {\n /**\n * The derived scope to list. A builder must name one (it may only see its\n * own questions on a scope it may write); the unfiltered list is the\n * owner's.\n */\n derivedScope: string;\n}\n\nexport interface RecomputeQuestionParams extends DerivativeQuestionAuthParams {\n questionId: string;\n}\n\nexport interface DeleteQuestionParams extends DerivativeQuestionAuthParams {\n questionId: string;\n}\n\nexport interface WaitForQuestionParams extends DerivativeQuestionAuthParams {\n questionId: string;\n /** Give up after this long (default {@link DEFAULT_QUESTION_TIMEOUT_MS}). */\n timeoutMs?: number;\n /** Wait between polls (default {@link DEFAULT_QUESTION_POLL_INTERVAL_MS}). */\n pollIntervalMs?: number;\n}\n\nexport interface AskPersonalServerParams extends RegisterQuestionParams {\n timeoutMs?: number;\n pollIntervalMs?: number;\n}\n\n/** {@link askPersonalServer}'s answer. */\nexport interface AskPersonalServerResult {\n /** The settled registration (`status` is `ready`). */\n registration: DerivativeQuestion;\n /** The derived record the Personal Server wrote and the builder just read. */\n record: DataFileEnvelope;\n}\n\n/**\n * Open write sessions, keyed by the signer object so a session is never\n * shared between builder keys and nothing is retained once the caller drops\n * its signer.\n */\nconst sessionsBySigner = new WeakMap<object, Map<string, WriteSession>>();\n\nfunction sessionCacheKey(\n personalServerUrl: string,\n audience: string,\n grantId: string,\n fetchFn: typeof fetch,\n): string {\n // The token is only valid on the server that minted it, and `fetch` is\n // what decides which server that is (a test double, a proxy, the global).\n return (\n JSON.stringify([personalServerUrl, audience, grantId]) + fetchIdOf(fetchFn)\n );\n}\n\nconst fetchIds = new WeakMap<object, number>();\nlet nextFetchId = 0;\n\nfunction fetchIdOf(fetchFn: typeof fetch): string {\n let id = fetchIds.get(fetchFn);\n if (id === undefined) {\n id = ++nextFetchId;\n fetchIds.set(fetchFn, id);\n }\n return `#${id}`;\n}\n\ninterface ResolvedQuestionRequest {\n baseUrl: string;\n audience: string;\n fetchFn: typeof fetch;\n cacheKey: string;\n signerKey: object;\n}\n\nfunction resolveRequest(\n params: DerivativeQuestionAuthParams,\n): ResolvedQuestionRequest {\n if (\n typeof params.personalServerUrl !== \"string\" ||\n params.personalServerUrl.length === 0\n ) {\n throw new WriteRequestError(\"personalServerUrl is required\");\n }\n // Checked before anything is signed or sent: without a grant the Personal\n // Server has nothing to authorize the call against.\n if (typeof params.grantId !== \"string\" || params.grantId.length === 0) {\n throw new WriteRequestError(\n \"grantId is required; a question call runs under the grant carrying write:<derivedScope>\",\n );\n }\n if (params.signer === null || typeof params.signer !== \"object\") {\n throw new WriteRequestError(\n \"signer must be a viem LocalAccount, a viem WalletClient, or a { signMessage } object\",\n );\n }\n const fetchFn = resolveFetch(params.fetch);\n const baseUrl = normalizeBaseUrl(params.personalServerUrl);\n const audience = params.audience ?? baseUrl;\n return {\n baseUrl,\n audience,\n fetchFn,\n cacheKey: sessionCacheKey(baseUrl, audience, params.grantId, fetchFn),\n signerKey: params.signer,\n };\n}\n\n/**\n * The session to use: the cached one while it is comfortably live, else a\n * fresh handshake. `force` drops the cached one first (the 401 path).\n */\nasync function resolveSession(\n params: DerivativeQuestionAuthParams,\n resolved: ResolvedQuestionRequest,\n force: boolean,\n): Promise<WriteSession> {\n let cache = sessionsBySigner.get(resolved.signerKey);\n if (cache === undefined) {\n cache = new Map();\n sessionsBySigner.set(resolved.signerKey, cache);\n }\n const cached = cache.get(resolved.cacheKey);\n if (\n !force &&\n cached !== undefined &&\n cached.expiresAt > Date.now() + SESSION_REFRESH_SKEW_MS\n ) {\n return cached;\n }\n if (force) cache.delete(resolved.cacheKey);\n const session = await openWriteSession({\n personalServerUrl: resolved.baseUrl,\n signer: params.signer,\n grantId: params.grantId,\n account: params.account,\n audience: resolved.audience,\n fetch: resolved.fetchFn,\n headers: params.headers,\n retry: params.retry,\n });\n cache.set(resolved.cacheKey, session);\n return session;\n}\n\n/** Map a non-2xx question answer onto the SDK's typed errors. */\nasync function questionErrorFromResponse(\n response: Response,\n): Promise<PersonalServerWriteError> {\n const { errorCode, message, details } =\n await readPersonalServerErrorBody(response);\n const text =\n message ??\n `Derivative question request failed: ${response.status} ${response.statusText}`;\n switch (errorCode) {\n case \"DERIVATIVE_SOURCE_NOT_GRANTED\":\n return new DerivativeSourceNotGrantedError(text, errorCode, details);\n case \"DERIVATIVE_CYCLE\":\n return new DerivativeCycleError(text, errorCode, details);\n case \"DERIVATIVE_COMPUTE_UNAVAILABLE\":\n return new DerivativeComputeUnavailableError(text, errorCode, details);\n case \"DERIVATIVE_QUESTION_INVALID\":\n case \"LINEAGE_SCOPE_UNDER_SOURCE_PREFIX\":\n return new DerivativeQuestionInvalidError(\n text,\n response.status,\n errorCode,\n details,\n );\n case \"DERIVATIVE_QUESTION_NOT_FOUND\":\n return new DerivativeQuestionNotFoundError(text, errorCode, details);\n default:\n break;\n }\n switch (response.status) {\n case 401:\n return new WriteUnauthorizedError(text, errorCode, details);\n case 403:\n return new WriteForbiddenError(text, errorCode, details);\n case 404:\n return new DerivativeQuestionNotFoundError(text, errorCode, details);\n case 409:\n return new WriteConflictError(text, errorCode, details);\n default:\n return new DerivativeQuestionRejectedError(\n text,\n response.status,\n errorCode,\n details,\n );\n }\n}\n\ninterface QuestionRequestSpec {\n method: \"GET\" | \"POST\" | \"DELETE\";\n /** Path without the query string: the proof only covers this. */\n path: string;\n /** Query string including the leading `?`, or `\"\"`. */\n query?: string;\n /** JSON body; sent (and signed) as compact JSON. */\n body?: Record<string, unknown>;\n label: string;\n}\n\nasync function sendOnce(\n params: DerivativeQuestionAuthParams,\n resolved: ResolvedQuestionRequest,\n session: WriteSession,\n spec: QuestionRequestSpec,\n bodyBytes: Uint8Array | undefined,\n): Promise<Response> {\n return sendWithFreshProof(\n spec.label,\n resolved.fetchFn,\n params.retry,\n proofKeyFor({\n aud: session.audience,\n method: spec.method,\n uri: spec.path,\n grantId: session.grantId,\n signedBytes: bodyBytes,\n }),\n async (iat) => {\n const headers = new Headers(params.headers);\n headers.set(\"Accept\", \"application/json\");\n headers.set(\"Authorization\", `Bearer ${session.accessToken}`);\n if (bodyBytes !== undefined) {\n headers.set(\"Content-Type\", \"application/json\");\n }\n headers.set(\n WRITE_SIGNATURE_HEADER,\n await buildWeb3SignedHeader({\n signMessage: session.signer.signMessage,\n aud: session.audience,\n // The Personal Server verifies the proof against the request's\n // path only, so the signed `uri` must not carry the query string.\n uri: spec.path,\n method: spec.method,\n body: bodyBytes,\n grantId: session.grantId,\n iat,\n }),\n );\n return {\n url: `${resolved.baseUrl}${spec.path}${spec.query ?? \"\"}`,\n init: {\n method: spec.method,\n headers,\n ...(bodyBytes === undefined\n ? {}\n : { body: bodyBytes as unknown as BodyInit }),\n ...(params.signal ? { signal: params.signal } : {}),\n },\n };\n },\n );\n}\n\n/**\n * Run one question call under a reused write session: fresh proof, and one\n * re-handshake when the Personal Server no longer knows the session.\n */\nasync function sendQuestionRequest<T>(\n params: DerivativeQuestionAuthParams,\n spec: QuestionRequestSpec,\n schema: z.ZodType<T>,\n): Promise<T> {\n const resolved = resolveRequest(params);\n // Compact JSON is the contract: the server re-serializes what it parsed\n // and refuses anything else with WRITE_BODY_NOT_CANONICAL.\n const bodyBytes =\n spec.body === undefined\n ? undefined\n : new TextEncoder().encode(JSON.stringify(spec.body));\n\n let session = await resolveSession(params, resolved, false);\n let response = await sendOnce(params, resolved, session, spec, bodyBytes);\n if (response.status === 401) {\n // The Personal Server keeps write sessions in memory: a restart (or an\n // expiry the client did not see) invalidates the bearer, not the grant.\n // Open a new session once and replay the call with a fresh proof.\n session = await resolveSession(params, resolved, true);\n response = await sendOnce(params, resolved, session, spec, bodyBytes);\n }\n\n if (!response.ok) {\n throw await questionErrorFromResponse(response);\n }\n let body: unknown;\n try {\n body = await response.json();\n } catch (err) {\n throw new DerivativeQuestionRejectedError(\n `${spec.label} response is not JSON`,\n response.status,\n null,\n { cause: errorMessage(err) },\n );\n }\n const parsed = schema.safeParse(body);\n if (!parsed.success) {\n throw new DerivativeQuestionRejectedError(\n `${spec.label} response is not a derivative question answer`,\n response.status,\n null,\n { issues: parsed.error.issues },\n );\n }\n return parsed.data;\n}\n\nfunction assertQuestionId(questionId: string): string {\n if (typeof questionId !== \"string\" || questionId.length === 0) {\n throw new WriteRequestError(\"questionId is required\");\n }\n return `${DERIVATIVE_QUESTIONS_PATH}/${encodeURIComponent(questionId)}`;\n}\n\n/**\n * Validate a registration the way the Personal Server does, so a builder\n * gets a typed error before a proof is signed rather than a 400 after.\n */\nfunction registrationBody(params: RegisterQuestionParams): {\n derivedScope: string;\n sourceScopes: string[];\n question: string;\n model?: string;\n} {\n const { derivedScope, question } = params;\n if (typeof derivedScope !== \"string\" || derivedScope.length === 0) {\n throw new WriteRequestError(\"derivedScope is required\");\n }\n if (!Array.isArray(params.sourceScopes) || params.sourceScopes.length === 0) {\n throw new WriteRequestError(\n \"sourceScopes must be a non-empty array of scopes\",\n );\n }\n if (params.sourceScopes.length > MAX_QUESTION_SOURCE_SCOPES) {\n throw new WriteRequestError(\n `sourceScopes lists ${params.sourceScopes.length} scopes; the maximum is ${MAX_QUESTION_SOURCE_SCOPES}`,\n { max: MAX_QUESTION_SOURCE_SCOPES, count: params.sourceScopes.length },\n );\n }\n const sourceScopes: string[] = [];\n for (const scope of params.sourceScopes) {\n if (typeof scope !== \"string\" || scope.length === 0) {\n throw new WriteRequestError(\"sourceScopes entries must be scope strings\");\n }\n if (sourceScopes.includes(scope)) {\n throw new WriteRequestError(\"sourceScopes must not repeat a scope\", {\n duplicate: scope,\n });\n }\n if (scope === derivedScope) {\n throw new WriteRequestError(\n \"derivedScope cannot be one of its own sources\",\n { scope },\n );\n }\n sourceScopes.push(scope);\n }\n if (typeof question !== \"string\" || question.trim() === \"\") {\n throw new WriteRequestError(\"question must be a non-empty string\");\n }\n if (question.length > MAX_QUESTION_CHARS) {\n throw new WriteRequestError(\n `question is ${question.length} characters; the maximum is ${MAX_QUESTION_CHARS}`,\n { max: MAX_QUESTION_CHARS, length: question.length },\n );\n }\n if (params.model !== undefined) {\n if (\n typeof params.model !== \"string\" ||\n params.model.length > MAX_QUESTION_MODEL_CHARS ||\n !MODEL_ID_PATTERN.test(params.model)\n ) {\n throw new WriteRequestError(\"model must be a provider model id\", {\n model: params.model,\n });\n }\n }\n // The lineage naming rule, applied before signing: the server would refuse\n // the registration with LINEAGE_SCOPE_UNDER_SOURCE_PREFIX.\n assertDerivedScopeNaming(derivedScope, sourceScopes);\n return {\n derivedScope,\n sourceScopes,\n question,\n ...(params.model === undefined ? {} : { model: params.model }),\n };\n}\n\n/**\n * Register a standing question over the owner's source scopes.\n *\n * @remarks\n * Sends `POST /v1/derivatives/questions`. The registration comes back\n * `pending` and the first compute is scheduled immediately; poll it with\n * {@link waitForQuestion}, then read `derivedScope`.\n *\n * @example\n * ```typescript\n * const registered = await registerQuestion({\n * personalServerUrl: \"https://ps.example.com\",\n * signer,\n * grantId,\n * derivedScope: \"coach.weekly\",\n * sourceScopes: [\"oura.sleep\", \"chatgpt.conversations\"],\n * question: \"How did my sleep relate to my mood this week?\",\n * });\n * ```\n * @returns The registration, `status: \"pending\"`.\n * @throws {WriteRequestError} Before sending: a missing grant, a bad scope\n * list, an over-long question, a derived scope under a source's namespace.\n * @throws {DerivativeSourceNotGrantedError} 403: a source scope is not\n * read-granted to the builder (`details.scopes`).\n * @throws {DerivativeCycleError} 409: the question would make the derived\n * scope a transitive source of itself.\n * @throws {DerivativeQuestionInvalidError} 400 from the server.\n * @throws {DerivativeComputeUnavailableError} 503: no compute layer.\n * @throws {WriteForbiddenError} 403: the grant does not authorize writing\n * the derived scope.\n */\nexport async function registerQuestion(\n params: RegisterQuestionParams,\n): Promise<DerivativeQuestion> {\n const body = registrationBody(params);\n return sendQuestionRequest(\n params,\n {\n method: \"POST\",\n path: DERIVATIVE_QUESTIONS_PATH,\n body,\n label: \"Register derivative question\",\n },\n DerivativeQuestionSchema,\n );\n}\n\n/**\n * Read one question's current state.\n *\n * @remarks\n * Sends `GET /v1/derivatives/questions/:id`. A builder only sees questions\n * it registered itself; anything else is a 404.\n *\n * @returns The registration, including `status`, `lastComputedAt`,\n * `derivedVersion` and (when it failed) `error`.\n * @throws {DerivativeQuestionNotFoundError} 404: unknown id, or not this\n * builder's question.\n */\nexport async function getQuestion(\n params: GetQuestionParams,\n): Promise<DerivativeQuestion> {\n const path = assertQuestionId(params.questionId);\n return sendQuestionRequest(\n params,\n { method: \"GET\", path, label: \"Read derivative question\" },\n DerivativeQuestionSchema,\n );\n}\n\n/**\n * List the questions this builder registered on a derived scope.\n *\n * @remarks\n * Sends `GET /v1/derivatives/questions?derivedScope=...`. The scope is\n * required for a builder: it is what the call is authorized against. Note\n * that the query string is outside the signed proof, which covers the path.\n *\n * @returns The registrations, newest state included.\n */\nexport async function listQuestions(\n params: ListQuestionsParams,\n): Promise<DerivativeQuestion[]> {\n if (\n typeof params.derivedScope !== \"string\" ||\n params.derivedScope.length === 0\n ) {\n throw new WriteRequestError(\n \"derivedScope is required; a builder may only list its own questions on a scope it may write\",\n );\n }\n const { questions } = await sendQuestionRequest(\n params,\n {\n method: \"GET\",\n path: DERIVATIVE_QUESTIONS_PATH,\n query: `?derivedScope=${encodeURIComponent(params.derivedScope)}`,\n label: \"List derivative questions\",\n },\n QuestionListSchema,\n );\n return questions;\n}\n\n/**\n * Ask the Personal Server to recompute a question now.\n *\n * @remarks\n * Sends `POST /v1/derivatives/questions/:id/recompute`, which answers 202\n * and schedules the compute immediately instead of after the usual quiet\n * period. Use it to retry a `failed` question; a source change recomputes on\n * its own.\n *\n * @returns `{ questionId, derivedScope, status }` with the status the\n * question was put into (`pending` when it had never computed, else\n * `stale`).\n */\nexport async function recomputeQuestion(\n params: RecomputeQuestionParams,\n): Promise<QuestionRecomputeResult> {\n const path = `${assertQuestionId(params.questionId)}/recompute`;\n return sendQuestionRequest(\n params,\n { method: \"POST\", path, label: \"Recompute derivative question\" },\n QuestionRecomputeResultSchema,\n );\n}\n\n/**\n * Delete a question registration.\n *\n * @remarks\n * Sends `DELETE /v1/derivatives/questions/:id`. The question stops\n * recomputing; the derived records it already wrote are left alone (delete\n * those through the data-point deletion path).\n *\n * @returns `{ questionId, deleted: true }`.\n */\nexport async function deleteQuestion(\n params: DeleteQuestionParams,\n): Promise<QuestionDeleteResult> {\n const path = assertQuestionId(params.questionId);\n return sendQuestionRequest(\n params,\n { method: \"DELETE\", path, label: \"Delete derivative question\" },\n QuestionDeleteResultSchema,\n );\n}\n\n/** `true` once the question has settled: nothing more to wait for. */\nfunction isSettled(status: QuestionStatus): boolean {\n return status === \"ready\" || status === \"failed\";\n}\n\nfunction abortError(signal: AbortSignal): Error {\n const reason: unknown = signal.reason;\n if (reason instanceof Error) return reason;\n const error = new Error(\"The operation was aborted\");\n error.name = \"AbortError\";\n return error;\n}\n\n/**\n * Poll a question until it settles.\n *\n * @remarks\n * Calls {@link getQuestion} every `pollIntervalMs` until `status` is `ready`\n * or `failed` and returns that state; a `failed` question is returned, not\n * thrown, so the caller can read `error` and decide whether to\n * {@link recomputeQuestion}. All polls share the one write session and each\n * signs its own proof.\n *\n * @example\n * ```typescript\n * const settled = await waitForQuestion({\n * personalServerUrl,\n * signer,\n * grantId,\n * questionId: registered.questionId,\n * timeoutMs: 60_000,\n * });\n * if (settled.status === \"ready\") {\n * // read derivedScope\n * }\n * ```\n * @returns The settled registration (`ready` or `failed`).\n * @throws {DerivativeQuestionTimeoutError} The question had not settled\n * within `timeoutMs`; it keeps computing on the server.\n * @throws Whatever {@link getQuestion} throws, and the `signal`'s abort\n * reason when the caller aborts.\n */\nexport async function waitForQuestion(\n params: WaitForQuestionParams,\n): Promise<DerivativeQuestion> {\n const timeoutMs = Math.max(\n 0,\n params.timeoutMs ?? DEFAULT_QUESTION_TIMEOUT_MS,\n );\n const pollIntervalMs = Math.max(\n 0,\n params.pollIntervalMs ?? DEFAULT_QUESTION_POLL_INTERVAL_MS,\n );\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n if (params.signal?.aborted) throw abortError(params.signal);\n const latest = await getQuestion(params);\n if (isSettled(latest.status)) return latest;\n const remaining = deadline - Date.now();\n if (remaining <= 0) {\n throw new DerivativeQuestionTimeoutError(\n `Derivative question ${latest.questionId} was still ${latest.status} after ${timeoutMs}ms`,\n {\n questionId: latest.questionId,\n derivedScope: latest.derivedScope,\n status: latest.status,\n timeoutMs,\n },\n );\n }\n await sleep(Math.min(pollIntervalMs, remaining));\n }\n}\n\n/**\n * Register a question, wait for it, and read the answer: the whole builder\n * loop in one call.\n *\n * @remarks\n * {@link registerQuestion} + {@link waitForQuestion} +\n * {@link readPersonalServerData} on the derived scope, which is why the\n * grant needs a bare read entry for `derivedScope` on top of\n * `write:<derivedScope>` and the source reads. The read is the plain\n * Web3Signed one; when the grant is priced, settle the 402 yourself with the\n * escrow-aware read from `@opendatalabs/vana-sdk/server` and use\n * {@link registerQuestion} and {@link waitForQuestion} directly.\n *\n * A question registered this way keeps recomputing after the call returns:\n * every later change to a source scope refreshes the derived record, and the\n * builder can read it again without registering anything.\n *\n * @example\n * ```typescript\n * const { registration, record } = await askPersonalServer({\n * personalServerUrl: \"https://ps.example.com\",\n * signer,\n * grantId,\n * derivedScope: \"coach.weekly\",\n * sourceScopes: [\"oura.sleep\"],\n * question: \"How did my sleep trend this week?\",\n * });\n * console.log(record.data.answer, registration.questionId);\n * ```\n * @returns The settled registration and the derived record.\n * @throws {DerivativeQuestionFailedError} The question settled as `failed`\n * (`details.error` is the server's reason).\n * @throws Everything {@link registerQuestion}, {@link waitForQuestion} and\n * the read path throw.\n */\nexport async function askPersonalServer(\n params: AskPersonalServerParams,\n): Promise<AskPersonalServerResult> {\n const registered = await registerQuestion(params);\n const registration = await waitForQuestion({\n ...params,\n questionId: registered.questionId,\n });\n if (registration.status !== \"ready\") {\n throw new DerivativeQuestionFailedError(\n `Derivative question ${registration.questionId} failed: ${registration.error ?? \"no reason given\"}`,\n {\n questionId: registration.questionId,\n derivedScope: registration.derivedScope,\n error: registration.error,\n },\n );\n }\n const signer = resolveWriteSigner(params.signer, {\n account: params.account,\n });\n const readParams: ReadPersonalServerDataParams = {\n personalServerUrl: normalizeBaseUrl(params.personalServerUrl),\n scope: params.derivedScope,\n grantId: params.grantId,\n signMessage: signer.signMessage,\n ...(params.audience === undefined ? {} : { audience: params.audience }),\n ...(params.headers === undefined ? {} : { headers: params.headers }),\n ...(params.fetch === undefined ? {} : { fetch: params.fetch }),\n };\n const record = await readPersonalServerData(readParams);\n return { registration, record };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/protocol/derivative-questions.ts"],"sourcesContent":["/**\n * Builder-side client for the Personal Server derivative question API.\n *\n * @remarks\n * A question is a standing prompt over the owner's source scopes. The\n * Personal Server answers it locally (the raw sources never leave the\n * machine except through its inference call) and writes the answer into the\n * derived scope as an ordinary derivative record, with lineage pointing at\n * the sources. The builder then reads the derived scope with its normal read\n * grant. Every source change re-runs the question, so a builder registers it\n * once and keeps reading a scope that stays up to date.\n *\n * One grant carries the whole pipeline, and it needs all three of:\n *\n * - a bare read entry for every source scope (the answer exposes them, so\n * the server refuses the registration otherwise:\n * `DERIVATIVE_SOURCE_NOT_GRANTED`),\n * - a bare read entry for the derived scope (to read the answer back),\n * - `write:<derivedScope>` (the credential the question routes authorize\n * against).\n *\n * Authentication is the Write API's, with no new credential: the write\n * session bearer from {@link openWriteSession} plus a fresh, single-use\n * `X-Vana-Write-Signature` Web3Signed proof over every request, carrying the\n * grant id as a signed claim. These helpers own that: they open one session\n * per `{ signer, Personal Server, grant }`, reuse it across calls, sign a new\n * proof per request, and re-open the session once when a call comes back a\n * 401 the session is responsible for (the Personal Server keeps sessions in\n * memory and forgets them when it restarts; a 401 about the PROOF is\n * surfaced as it is, since a new session would not change it).\n *\n * Two rules govern the proof on these routes, and both are the server's\n * (`personal-server-ts` d91124d and later):\n *\n * - the signed `uri` claim covers the query string, not just the path,\n * because `?derivedScope=` is what the list route authorizes against;\n * - every call carries a fresh `nonce` claim, which becomes the server's\n * replay key. Without one the whole proof is the key, so two identical\n * polls signed inside the same second are refused as a replay.\n *\n * @category Protocol\n */\n\nimport { z } from \"zod\";\nimport { buildWeb3SignedHeader } from \"../auth/web3-signed-builder\";\nimport {\n DerivativeComputeUnavailableError,\n DerivativeCycleError,\n DerivativeDerivedScopeRequiredError,\n DerivativeQuestionFailedError,\n DerivativeQuestionInvalidError,\n DerivativeQuestionNotFoundError,\n DerivativeQuestionRejectedError,\n DerivativeQuestionTimeoutError,\n DerivativeSourceNotGrantedError,\n WriteConflictError,\n WriteForbiddenError,\n WriteRequestError,\n WriteUnauthorizedError,\n type PersonalServerWriteError,\n} from \"../errors\";\nimport { assertDerivedScopeNaming } from \"./lineage\";\nimport {\n readPersonalServerErrorBody,\n type PersonalServerErrorBody,\n} from \"./personal-server-error-body\";\nimport {\n readPersonalServerData,\n type ReadPersonalServerDataParams,\n} from \"./personal-server-data\";\nimport type { DataFileEnvelope } from \"./data-file\";\nimport {\n openWriteSession,\n WRITE_SIGNATURE_HEADER,\n type WriteSession,\n} from \"./personal-server-write\";\nimport {\n errorMessage,\n freshProofNonce,\n normalizeBaseUrl,\n proofKeyFor,\n resolveFetch,\n sendWithFreshProof,\n sleep,\n type WriteTransportRetryOptions,\n} from \"./write-request\";\nimport {\n resolveWriteSigner,\n type ResolveWriteSignerOptions,\n type WriteSignerSource,\n} from \"./write-signer\";\n\n/** Path the question routes are mounted at. */\nexport const DERIVATIVE_QUESTIONS_PATH = \"/v1/derivatives/questions\";\n/** The most source scopes one question may read. */\nexport const MAX_QUESTION_SOURCE_SCOPES = 16;\n/** The longest question text the Personal Server accepts. */\nexport const MAX_QUESTION_CHARS = 8_000;\n/** The longest model id the Personal Server accepts. */\nexport const MAX_QUESTION_MODEL_CHARS = 128;\n/** Model ids as providers spell them (`z-ai/glm-5.2`, `gpt-4o-mini`, ...). */\nconst MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]*$/;\n/** How long {@link waitForQuestion} polls before giving up. */\nexport const DEFAULT_QUESTION_TIMEOUT_MS = 120_000;\n/** How long {@link waitForQuestion} waits between polls. */\nexport const DEFAULT_QUESTION_POLL_INTERVAL_MS = 2_000;\n/** Re-open a session this long before its token expires. */\nconst SESSION_REFRESH_SKEW_MS = 30_000;\n\n/** Every state a question can be in. */\nexport const QUESTION_STATUSES = [\n \"pending\",\n \"ready\",\n \"failed\",\n \"stale\",\n] as const;\n\n/**\n * `pending` (never computed) -> `ready` | `failed`; a source change or an\n * explicit recompute puts a computed question back to `stale`, which\n * settles as `ready` or `failed` again.\n */\nexport const QuestionStatusSchema = z.enum(QUESTION_STATUSES);\n\n/** @see {@link QuestionStatusSchema} */\nexport type QuestionStatus = z.infer<typeof QuestionStatusSchema>;\n\n/** Who registered the question: the owner, or a builder under a grant. */\nexport const QuestionRegisteredBySchema = z.union([\n z.object({ kind: z.literal(\"owner\") }),\n z.object({\n kind: z.literal(\"builder\"),\n builder: z.string(),\n grantId: z.string(),\n }),\n]);\n\n/** @see {@link QuestionRegisteredBySchema} */\nexport type QuestionRegisteredBy = z.infer<typeof QuestionRegisteredBySchema>;\n\n// The server always sends these; `nullish` keeps a Personal Server that\n// omits one readable rather than failing the whole call on a missing field.\nconst nullableString = z\n .string()\n .nullish()\n .transform((value) => value ?? null);\n\n/**\n * A question registration as the Personal Server reports it (the answer of\n * register, get and list).\n */\nexport const DerivativeQuestionSchema = z.object({\n questionId: z.string().min(1),\n derivedScope: z.string().min(1),\n sourceScopes: z.array(z.string()),\n question: z.string(),\n /** The model override, or `null` for the server's default. */\n model: nullableString,\n registeredBy: QuestionRegisteredBySchema,\n status: QuestionStatusSchema,\n /** A short reason, set only while `status` is `failed`. */\n error: nullableString,\n createdAt: z.string(),\n updatedAt: nullableString,\n /** When the last compute finished, or `null` while `pending`. */\n lastComputedAt: nullableString,\n /** Local version of the derived record the last compute wrote. */\n derivedVersion: z\n .number()\n .nullish()\n .transform((value) => value ?? null),\n derivedCollectedAt: nullableString,\n});\n\n/** @see {@link DerivativeQuestionSchema} */\nexport type DerivativeQuestion = z.infer<typeof DerivativeQuestionSchema>;\n\nconst QuestionListSchema = z.object({\n questions: z.array(DerivativeQuestionSchema),\n});\n\n/**\n * The 202 answer of a recompute request: the same registration view every\n * other question route answers, so a client needs one schema.\n *\n * @remarks\n * Older Personal Servers answered only\n * `{ questionId, derivedScope, status }` here. The full view is a superset\n * of those three fields, so code reading them is unaffected, but the answer\n * of a server before `personal-server-ts` d91124d no longer parses.\n */\nexport const QuestionRecomputeResultSchema = DerivativeQuestionSchema;\n\n/** @see {@link QuestionRecomputeResultSchema} */\nexport type QuestionRecomputeResult = DerivativeQuestion;\n\n/** The answer of a delete request. */\nexport const QuestionDeleteResultSchema = z.object({\n questionId: z.string().min(1),\n deleted: z.literal(true),\n});\n\n/** @see {@link QuestionDeleteResultSchema} */\nexport type QuestionDeleteResult = z.infer<typeof QuestionDeleteResultSchema>;\n\n/**\n * Connection, credential and transport shared by every question call.\n *\n * @remarks\n * The write session is opened on demand and reused for every later call\n * made with the same `signer` object, Personal Server, audience, grant and\n * `fetch`; a 401 re-opens it once and replays the call.\n */\nexport interface DerivativeQuestionAuthParams extends ResolveWriteSignerOptions {\n /** Personal Server origin, e.g. `https://ps.example.com`. */\n personalServerUrl: string;\n /** Builder key: a viem `LocalAccount`, `WalletClient`, or `{ signMessage }`. */\n signer: WriteSignerSource;\n /**\n * The grant the call runs under. It must carry `write:<derivedScope>`, a\n * bare read entry for the derived scope, and a bare read entry for every\n * source scope.\n */\n grantId: string;\n /** Web3Signed audience; defaults to `personalServerUrl`. */\n audience?: string;\n /** `fetch` to use; defaults to `globalThis.fetch`. */\n fetch?: typeof fetch;\n /** Extra request headers. */\n headers?: HeadersInit;\n retry?: WriteTransportRetryOptions;\n /** Aborts the request (and, for {@link waitForQuestion}, the polling). */\n signal?: AbortSignal;\n}\n\nexport interface RegisterQuestionParams extends DerivativeQuestionAuthParams {\n /**\n * The scope the answer is written into. Must not share its first\n * dot-segment with any source scope, so put derivatives in the app's own\n * namespace.\n */\n derivedScope: string;\n /**\n * The scopes the question reads: 1 to\n * {@link MAX_QUESTION_SOURCE_SCOPES} distinct scopes, none of them the\n * derived scope. They do not have to hold data yet: the question computes\n * once they do.\n */\n sourceScopes: readonly string[];\n /** The prompt, 1 to {@link MAX_QUESTION_CHARS} characters. */\n question: string;\n /** Model id override; omitted = the Personal Server's default model. */\n model?: string;\n}\n\nexport interface GetQuestionParams extends DerivativeQuestionAuthParams {\n questionId: string;\n}\n\nexport interface ListQuestionsParams extends DerivativeQuestionAuthParams {\n /**\n * The derived scope to list. A builder must name one (it may only see its\n * own questions on a scope it may write); the unfiltered list is the\n * owner's.\n */\n derivedScope: string;\n}\n\nexport interface RecomputeQuestionParams extends DerivativeQuestionAuthParams {\n questionId: string;\n}\n\nexport interface DeleteQuestionParams extends DerivativeQuestionAuthParams {\n questionId: string;\n}\n\nexport interface WaitForQuestionParams extends DerivativeQuestionAuthParams {\n questionId: string;\n /** Give up after this long (default {@link DEFAULT_QUESTION_TIMEOUT_MS}). */\n timeoutMs?: number;\n /** Wait between polls (default {@link DEFAULT_QUESTION_POLL_INTERVAL_MS}). */\n pollIntervalMs?: number;\n}\n\nexport interface AskPersonalServerParams extends RegisterQuestionParams {\n timeoutMs?: number;\n pollIntervalMs?: number;\n}\n\n/** {@link askPersonalServer}'s answer. */\nexport interface AskPersonalServerResult {\n /** The settled registration (`status` is `ready`). */\n registration: DerivativeQuestion;\n /** The derived record the Personal Server wrote and the builder just read. */\n record: DataFileEnvelope;\n}\n\n/**\n * Open write sessions, keyed by the signer object so a session is never\n * shared between builder keys and nothing is retained once the caller drops\n * its signer.\n */\nconst sessionsBySigner = new WeakMap<object, Map<string, WriteSession>>();\n\nfunction sessionCacheKey(\n personalServerUrl: string,\n audience: string,\n grantId: string,\n fetchFn: typeof fetch,\n): string {\n // The token is only valid on the server that minted it, and `fetch` is\n // what decides which server that is (a test double, a proxy, the global).\n return (\n JSON.stringify([personalServerUrl, audience, grantId]) + fetchIdOf(fetchFn)\n );\n}\n\nconst fetchIds = new WeakMap<object, number>();\nlet nextFetchId = 0;\n\nfunction fetchIdOf(fetchFn: typeof fetch): string {\n let id = fetchIds.get(fetchFn);\n if (id === undefined) {\n id = ++nextFetchId;\n fetchIds.set(fetchFn, id);\n }\n return `#${id}`;\n}\n\ninterface ResolvedQuestionRequest {\n baseUrl: string;\n audience: string;\n fetchFn: typeof fetch;\n cacheKey: string;\n signerKey: object;\n}\n\nfunction resolveRequest(\n params: DerivativeQuestionAuthParams,\n): ResolvedQuestionRequest {\n if (\n typeof params.personalServerUrl !== \"string\" ||\n params.personalServerUrl.length === 0\n ) {\n throw new WriteRequestError(\"personalServerUrl is required\");\n }\n // Checked before anything is signed or sent: without a grant the Personal\n // Server has nothing to authorize the call against.\n if (typeof params.grantId !== \"string\" || params.grantId.length === 0) {\n throw new WriteRequestError(\n \"grantId is required; a question call runs under the grant carrying write:<derivedScope>\",\n );\n }\n if (params.signer === null || typeof params.signer !== \"object\") {\n throw new WriteRequestError(\n \"signer must be a viem LocalAccount, a viem WalletClient, or a { signMessage } object\",\n );\n }\n const fetchFn = resolveFetch(params.fetch);\n const baseUrl = normalizeBaseUrl(params.personalServerUrl);\n const audience = params.audience ?? baseUrl;\n return {\n baseUrl,\n audience,\n fetchFn,\n cacheKey: sessionCacheKey(baseUrl, audience, params.grantId, fetchFn),\n signerKey: params.signer,\n };\n}\n\n/**\n * The session to use: the cached one while it is comfortably live, else a\n * fresh handshake. `force` drops the cached one first (the 401 path).\n */\nasync function resolveSession(\n params: DerivativeQuestionAuthParams,\n resolved: ResolvedQuestionRequest,\n force: boolean,\n): Promise<WriteSession> {\n let cache = sessionsBySigner.get(resolved.signerKey);\n if (cache === undefined) {\n cache = new Map();\n sessionsBySigner.set(resolved.signerKey, cache);\n }\n const cached = cache.get(resolved.cacheKey);\n if (\n !force &&\n cached !== undefined &&\n cached.expiresAt > Date.now() + SESSION_REFRESH_SKEW_MS\n ) {\n return cached;\n }\n if (force) cache.delete(resolved.cacheKey);\n const session = await openWriteSession({\n personalServerUrl: resolved.baseUrl,\n signer: params.signer,\n grantId: params.grantId,\n account: params.account,\n audience: resolved.audience,\n fetch: resolved.fetchFn,\n headers: params.headers,\n retry: params.retry,\n });\n cache.set(resolved.cacheKey, session);\n return session;\n}\n\n/**\n * The 401s a re-handshake cannot fix. They are failures of the per-request\n * PROOF, not of the session, so the same call signed under a brand new\n * session fails exactly the same way: replaying it would burn a second\n * proof, add a pointless handshake, and report the wrong problem. Every\n * other 401 is treated as the session the Personal Server forgot.\n */\nconst PROOF_FAILURE_CODES = new Set([\n \"WRITE_ATTRIBUTION_REQUIRED\",\n \"WRITE_ATTRIBUTION_INVALID\",\n \"WRITE_ATTRIBUTION_SIGNER_MISMATCH\",\n \"WRITE_ATTRIBUTION_GRANT_MISMATCH\",\n \"WRITE_ATTRIBUTION_REPLAY\",\n]);\n\nfunction isStaleSession(errorCode: string | null): boolean {\n return errorCode === null || !PROOF_FAILURE_CODES.has(errorCode);\n}\n\n/**\n * Map a non-2xx question answer onto the SDK's typed errors. `body` is the\n * already-read error body, for the caller that had to peek at it (a response\n * body can only be read once).\n */\nasync function questionErrorFromResponse(\n response: Response,\n body?: PersonalServerErrorBody,\n): Promise<PersonalServerWriteError> {\n const { errorCode, message, details } =\n body ?? (await readPersonalServerErrorBody(response));\n const text =\n message ??\n `Derivative question request failed: ${response.status} ${response.statusText}`;\n switch (errorCode) {\n case \"DERIVATIVE_SOURCE_NOT_GRANTED\":\n return new DerivativeSourceNotGrantedError(text, errorCode, details);\n case \"DERIVATIVE_CYCLE\":\n return new DerivativeCycleError(text, errorCode, details);\n case \"DERIVATIVE_COMPUTE_UNAVAILABLE\":\n return new DerivativeComputeUnavailableError(text, errorCode, details);\n case \"DERIVATIVE_QUESTION_INVALID\":\n case \"LINEAGE_SCOPE_UNDER_SOURCE_PREFIX\":\n return new DerivativeQuestionInvalidError(\n text,\n response.status,\n errorCode,\n details,\n );\n case \"DERIVATIVE_QUESTION_NOT_FOUND\":\n return new DerivativeQuestionNotFoundError(text, errorCode, details);\n case \"DERIVATIVE_DERIVED_SCOPE_REQUIRED\":\n return new DerivativeDerivedScopeRequiredError(text, errorCode, details);\n default:\n break;\n }\n switch (response.status) {\n case 401:\n return new WriteUnauthorizedError(text, errorCode, details);\n case 403:\n return new WriteForbiddenError(text, errorCode, details);\n case 404:\n return new DerivativeQuestionNotFoundError(text, errorCode, details);\n case 409:\n return new WriteConflictError(text, errorCode, details);\n default:\n return new DerivativeQuestionRejectedError(\n text,\n response.status,\n errorCode,\n details,\n );\n }\n}\n\ninterface QuestionRequestSpec {\n method: \"GET\" | \"POST\" | \"DELETE\";\n /**\n * The whole request target, path AND query string. The Personal Server\n * verifies the proof against it (the query decides the authorization on\n * the list route), so it is built once and used for both the signed `uri`\n * claim and the `fetch` URL, where the two cannot drift apart.\n */\n target: string;\n /** JSON body; sent (and signed) as compact JSON. */\n body?: Record<string, unknown>;\n label: string;\n}\n\nasync function sendOnce(\n params: DerivativeQuestionAuthParams,\n resolved: ResolvedQuestionRequest,\n session: WriteSession,\n spec: QuestionRequestSpec,\n bodyBytes: Uint8Array | undefined,\n): Promise<Response> {\n return sendWithFreshProof(\n spec.label,\n resolved.fetchFn,\n params.retry,\n proofKeyFor({\n aud: session.audience,\n method: spec.method,\n uri: spec.target,\n grantId: session.grantId,\n signedBytes: bodyBytes,\n }),\n async (iat) => {\n const headers = new Headers(params.headers);\n headers.set(\"Accept\", \"application/json\");\n headers.set(\"Authorization\", `Bearer ${session.accessToken}`);\n if (bodyBytes !== undefined) {\n headers.set(\"Content-Type\", \"application/json\");\n }\n headers.set(\n WRITE_SIGNATURE_HEADER,\n await buildWeb3SignedHeader({\n signMessage: session.signer.signMessage,\n aud: session.audience,\n // The proof commits to the whole request target, query included:\n // `?derivedScope=` is what the list route authorizes against, and a\n // proof that did not cover it would authorize any other scope.\n uri: spec.target,\n method: spec.method,\n body: bodyBytes,\n grantId: session.grantId,\n // Fresh per attempt, so a retry after a thrown `fetch` is never the\n // proof the server may already have consumed, and so two identical\n // polls inside one second stay distinct.\n nonce: freshProofNonce(),\n iat,\n }),\n );\n return {\n url: `${resolved.baseUrl}${spec.target}`,\n init: {\n method: spec.method,\n headers,\n ...(bodyBytes === undefined\n ? {}\n : { body: bodyBytes as unknown as BodyInit }),\n ...(params.signal ? { signal: params.signal } : {}),\n },\n };\n },\n );\n}\n\n/**\n * Run one question call under a reused write session: fresh proof, and one\n * re-handshake when the Personal Server no longer knows the session.\n */\nasync function sendQuestionRequest<T>(\n params: DerivativeQuestionAuthParams,\n spec: QuestionRequestSpec,\n schema: z.ZodType<T>,\n): Promise<T> {\n const resolved = resolveRequest(params);\n // Compact JSON is the contract: the server re-serializes what it parsed\n // and refuses anything else with WRITE_BODY_NOT_CANONICAL.\n const bodyBytes =\n spec.body === undefined\n ? undefined\n : new TextEncoder().encode(JSON.stringify(spec.body));\n\n let session = await resolveSession(params, resolved, false);\n let response = await sendOnce(params, resolved, session, spec, bodyBytes);\n // Read once, kept for the throw below: a body cannot be read twice.\n let errorBody: PersonalServerErrorBody | undefined;\n if (response.status === 401) {\n errorBody = await readPersonalServerErrorBody(response);\n if (isStaleSession(errorBody.errorCode)) {\n // The Personal Server keeps write sessions in memory: a restart (or an\n // expiry the client did not see) invalidates the bearer, not the grant.\n // Open a new session once and replay the call with a fresh proof.\n session = await resolveSession(params, resolved, true);\n response = await sendOnce(params, resolved, session, spec, bodyBytes);\n errorBody = undefined;\n }\n }\n\n if (!response.ok) {\n throw await questionErrorFromResponse(response, errorBody);\n }\n let body: unknown;\n try {\n body = await response.json();\n } catch (err) {\n throw new DerivativeQuestionRejectedError(\n `${spec.label} response is not JSON`,\n response.status,\n null,\n { cause: errorMessage(err) },\n );\n }\n const parsed = schema.safeParse(body);\n if (!parsed.success) {\n throw new DerivativeQuestionRejectedError(\n `${spec.label} response is not a derivative question answer`,\n response.status,\n null,\n { issues: parsed.error.issues },\n );\n }\n return parsed.data;\n}\n\n/** The request target for one question id: no query, so the bare path. */\nfunction assertQuestionId(questionId: string): string {\n if (typeof questionId !== \"string\" || questionId.length === 0) {\n throw new WriteRequestError(\"questionId is required\");\n }\n return `${DERIVATIVE_QUESTIONS_PATH}/${encodeURIComponent(questionId)}`;\n}\n\n/**\n * Validate a registration the way the Personal Server does, so a builder\n * gets a typed error before a proof is signed rather than a 400 after.\n */\nfunction registrationBody(params: RegisterQuestionParams): {\n derivedScope: string;\n sourceScopes: string[];\n question: string;\n model?: string;\n} {\n const { derivedScope, question } = params;\n if (typeof derivedScope !== \"string\" || derivedScope.length === 0) {\n throw new WriteRequestError(\"derivedScope is required\");\n }\n if (!Array.isArray(params.sourceScopes) || params.sourceScopes.length === 0) {\n throw new WriteRequestError(\n \"sourceScopes must be a non-empty array of scopes\",\n );\n }\n if (params.sourceScopes.length > MAX_QUESTION_SOURCE_SCOPES) {\n throw new WriteRequestError(\n `sourceScopes lists ${params.sourceScopes.length} scopes; the maximum is ${MAX_QUESTION_SOURCE_SCOPES}`,\n { max: MAX_QUESTION_SOURCE_SCOPES, count: params.sourceScopes.length },\n );\n }\n const sourceScopes: string[] = [];\n for (const scope of params.sourceScopes) {\n if (typeof scope !== \"string\" || scope.length === 0) {\n throw new WriteRequestError(\"sourceScopes entries must be scope strings\");\n }\n if (sourceScopes.includes(scope)) {\n throw new WriteRequestError(\"sourceScopes must not repeat a scope\", {\n duplicate: scope,\n });\n }\n if (scope === derivedScope) {\n throw new WriteRequestError(\n \"derivedScope cannot be one of its own sources\",\n { scope },\n );\n }\n sourceScopes.push(scope);\n }\n if (typeof question !== \"string\" || question.trim() === \"\") {\n throw new WriteRequestError(\"question must be a non-empty string\");\n }\n if (question.length > MAX_QUESTION_CHARS) {\n throw new WriteRequestError(\n `question is ${question.length} characters; the maximum is ${MAX_QUESTION_CHARS}`,\n { max: MAX_QUESTION_CHARS, length: question.length },\n );\n }\n if (params.model !== undefined) {\n if (\n typeof params.model !== \"string\" ||\n params.model.length > MAX_QUESTION_MODEL_CHARS ||\n !MODEL_ID_PATTERN.test(params.model)\n ) {\n throw new WriteRequestError(\"model must be a provider model id\", {\n model: params.model,\n });\n }\n }\n // The lineage naming rule, applied before signing: the server would refuse\n // the registration with LINEAGE_SCOPE_UNDER_SOURCE_PREFIX.\n assertDerivedScopeNaming(derivedScope, sourceScopes);\n return {\n derivedScope,\n sourceScopes,\n question,\n ...(params.model === undefined ? {} : { model: params.model }),\n };\n}\n\n/**\n * Register a standing question over the owner's source scopes.\n *\n * @remarks\n * Sends `POST /v1/derivatives/questions`. The registration comes back\n * `pending` and the first compute is scheduled immediately; poll it with\n * {@link waitForQuestion}, then read `derivedScope`.\n *\n * @example\n * ```typescript\n * const registered = await registerQuestion({\n * personalServerUrl: \"https://ps.example.com\",\n * signer,\n * grantId,\n * derivedScope: \"coach.weekly\",\n * sourceScopes: [\"oura.sleep\", \"chatgpt.conversations\"],\n * question: \"How did my sleep relate to my mood this week?\",\n * });\n * ```\n * @returns The registration, `status: \"pending\"`.\n * @throws {WriteRequestError} Before sending: a missing grant, a bad scope\n * list, an over-long question, a derived scope under a source's namespace.\n * @throws {DerivativeSourceNotGrantedError} 403: a source scope is not\n * read-granted to the builder (`details.scopes`).\n * @throws {DerivativeCycleError} 409: the question would make the derived\n * scope a transitive source of itself.\n * @throws {DerivativeQuestionInvalidError} 400 from the server.\n * @throws {DerivativeComputeUnavailableError} 503: no compute layer.\n * @throws {WriteForbiddenError} 403: the grant does not authorize writing\n * the derived scope.\n */\nexport async function registerQuestion(\n params: RegisterQuestionParams,\n): Promise<DerivativeQuestion> {\n const body = registrationBody(params);\n return sendQuestionRequest(\n params,\n {\n method: \"POST\",\n target: DERIVATIVE_QUESTIONS_PATH,\n body,\n label: \"Register derivative question\",\n },\n DerivativeQuestionSchema,\n );\n}\n\n/**\n * Read one question's current state.\n *\n * @remarks\n * Sends `GET /v1/derivatives/questions/:id`. A builder only sees questions\n * it registered itself; anything else is a 404.\n *\n * @returns The registration, including `status`, `lastComputedAt`,\n * `derivedVersion` and (when it failed) `error`.\n * @throws {DerivativeQuestionNotFoundError} 404: unknown id, or not this\n * builder's question.\n */\nexport async function getQuestion(\n params: GetQuestionParams,\n): Promise<DerivativeQuestion> {\n const target = assertQuestionId(params.questionId);\n return sendQuestionRequest(\n params,\n { method: \"GET\", target, label: \"Read derivative question\" },\n DerivativeQuestionSchema,\n );\n}\n\n/**\n * List the questions this builder registered on a derived scope.\n *\n * @remarks\n * Sends `GET /v1/derivatives/questions?derivedScope=...`. The scope is\n * required for a builder: it is what the call is authorized against, which\n * is exactly why the signed proof commits to the query string as well as the\n * path. The target is built once and used for both, so the signature and the\n * request can never name different scopes.\n *\n * @returns The registrations, newest state included.\n * @throws {DerivativeDerivedScopeRequiredError} 400\n * `DERIVATIVE_DERIVED_SCOPE_REQUIRED` when the server saw no\n * `?derivedScope=` (the SDK refuses an empty one before sending).\n */\nexport async function listQuestions(\n params: ListQuestionsParams,\n): Promise<DerivativeQuestion[]> {\n if (\n typeof params.derivedScope !== \"string\" ||\n params.derivedScope.length === 0\n ) {\n throw new WriteRequestError(\n \"derivedScope is required; a builder may only list its own questions on a scope it may write\",\n );\n }\n const target = `${DERIVATIVE_QUESTIONS_PATH}?derivedScope=${encodeURIComponent(params.derivedScope)}`;\n const { questions } = await sendQuestionRequest(\n params,\n {\n method: \"GET\",\n target,\n label: \"List derivative questions\",\n },\n QuestionListSchema,\n );\n return questions;\n}\n\n/**\n * Ask the Personal Server to recompute a question now.\n *\n * @remarks\n * Sends `POST /v1/derivatives/questions/:id/recompute`, which answers 202\n * and schedules the compute immediately instead of after the usual quiet\n * period. Use it to retry a `failed` question; a source change recomputes on\n * its own.\n *\n * @returns The full registration view, with the status the question was put\n * into (`pending` when it had never computed, else `stale`). Servers\n * before `personal-server-ts` d91124d answered only\n * `{ questionId, derivedScope, status }` here, which no longer parses.\n */\nexport async function recomputeQuestion(\n params: RecomputeQuestionParams,\n): Promise<QuestionRecomputeResult> {\n const target = `${assertQuestionId(params.questionId)}/recompute`;\n return sendQuestionRequest(\n params,\n { method: \"POST\", target, label: \"Recompute derivative question\" },\n QuestionRecomputeResultSchema,\n );\n}\n\n/**\n * Delete a question registration.\n *\n * @remarks\n * Sends `DELETE /v1/derivatives/questions/:id`. The question stops\n * recomputing; the derived records it already wrote are left alone (delete\n * those through the data-point deletion path).\n *\n * @returns `{ questionId, deleted: true }`.\n */\nexport async function deleteQuestion(\n params: DeleteQuestionParams,\n): Promise<QuestionDeleteResult> {\n const target = assertQuestionId(params.questionId);\n return sendQuestionRequest(\n params,\n { method: \"DELETE\", target, label: \"Delete derivative question\" },\n QuestionDeleteResultSchema,\n );\n}\n\n/** `true` once the question has settled: nothing more to wait for. */\nfunction isSettled(status: QuestionStatus): boolean {\n return status === \"ready\" || status === \"failed\";\n}\n\nfunction abortError(signal: AbortSignal): Error {\n const reason: unknown = signal.reason;\n if (reason instanceof Error) return reason;\n const error = new Error(\"The operation was aborted\");\n error.name = \"AbortError\";\n return error;\n}\n\n/**\n * Poll a question until it settles.\n *\n * @remarks\n * Calls {@link getQuestion} every `pollIntervalMs` until `status` is `ready`\n * or `failed` and returns that state; a `failed` question is returned, not\n * thrown, so the caller can read `error` and decide whether to\n * {@link recomputeQuestion}. All polls share the one write session and each\n * signs its own proof.\n *\n * @example\n * ```typescript\n * const settled = await waitForQuestion({\n * personalServerUrl,\n * signer,\n * grantId,\n * questionId: registered.questionId,\n * timeoutMs: 60_000,\n * });\n * if (settled.status === \"ready\") {\n * // read derivedScope\n * }\n * ```\n * @returns The settled registration (`ready` or `failed`).\n * @throws {DerivativeQuestionTimeoutError} The question had not settled\n * within `timeoutMs`; it keeps computing on the server.\n * @throws Whatever {@link getQuestion} throws, and the `signal`'s abort\n * reason when the caller aborts.\n */\nexport async function waitForQuestion(\n params: WaitForQuestionParams,\n): Promise<DerivativeQuestion> {\n const timeoutMs = Math.max(\n 0,\n params.timeoutMs ?? DEFAULT_QUESTION_TIMEOUT_MS,\n );\n const pollIntervalMs = Math.max(\n 0,\n params.pollIntervalMs ?? DEFAULT_QUESTION_POLL_INTERVAL_MS,\n );\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n if (params.signal?.aborted) throw abortError(params.signal);\n const latest = await getQuestion(params);\n if (isSettled(latest.status)) return latest;\n const remaining = deadline - Date.now();\n if (remaining <= 0) {\n throw new DerivativeQuestionTimeoutError(\n `Derivative question ${latest.questionId} was still ${latest.status} after ${timeoutMs}ms`,\n {\n questionId: latest.questionId,\n derivedScope: latest.derivedScope,\n status: latest.status,\n timeoutMs,\n },\n );\n }\n await sleep(Math.min(pollIntervalMs, remaining));\n }\n}\n\n/**\n * Register a question, wait for it, and read the answer: the whole builder\n * loop in one call.\n *\n * @remarks\n * {@link registerQuestion} + {@link waitForQuestion} +\n * {@link readPersonalServerData} on the derived scope, which is why the\n * grant needs a bare read entry for `derivedScope` on top of\n * `write:<derivedScope>` and the source reads. The read is the plain\n * Web3Signed one; when the grant is priced, settle the 402 yourself with the\n * escrow-aware read from `@opendatalabs/vana-sdk/server` and use\n * {@link registerQuestion} and {@link waitForQuestion} directly.\n *\n * A question registered this way keeps recomputing after the call returns:\n * every later change to a source scope refreshes the derived record, and the\n * builder can read it again without registering anything.\n *\n * @example\n * ```typescript\n * const { registration, record } = await askPersonalServer({\n * personalServerUrl: \"https://ps.example.com\",\n * signer,\n * grantId,\n * derivedScope: \"coach.weekly\",\n * sourceScopes: [\"oura.sleep\"],\n * question: \"How did my sleep trend this week?\",\n * });\n * console.log(record.data.answer, registration.questionId);\n * ```\n * @returns The settled registration and the derived record.\n * @throws {DerivativeQuestionFailedError} The question settled as `failed`\n * (`details.error` is the server's reason).\n * @throws Everything {@link registerQuestion}, {@link waitForQuestion} and\n * the read path throw.\n */\nexport async function askPersonalServer(\n params: AskPersonalServerParams,\n): Promise<AskPersonalServerResult> {\n const registered = await registerQuestion(params);\n const registration = await waitForQuestion({\n ...params,\n questionId: registered.questionId,\n });\n if (registration.status !== \"ready\") {\n throw new DerivativeQuestionFailedError(\n `Derivative question ${registration.questionId} failed: ${registration.error ?? \"no reason given\"}`,\n {\n questionId: registration.questionId,\n derivedScope: registration.derivedScope,\n error: registration.error,\n },\n );\n }\n const signer = resolveWriteSigner(params.signer, {\n account: params.account,\n });\n const readParams: ReadPersonalServerDataParams = {\n personalServerUrl: normalizeBaseUrl(params.personalServerUrl),\n scope: params.derivedScope,\n grantId: params.grantId,\n signMessage: signer.signMessage,\n ...(params.audience === undefined ? {} : { audience: params.audience }),\n ...(params.headers === undefined ? {} : { headers: params.headers }),\n ...(params.fetch === undefined ? {} : { fetch: params.fetch }),\n };\n const record = await readPersonalServerData(readParams);\n return { registration, record };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -20,6 +20,7 @@ var write_request_exports = {};
|
|
|
20
20
|
__export(write_request_exports, {
|
|
21
21
|
errorMessage: () => errorMessage,
|
|
22
22
|
finiteOr: () => finiteOr,
|
|
23
|
+
freshProofNonce: () => freshProofNonce,
|
|
23
24
|
nextProofIat: () => nextProofIat,
|
|
24
25
|
normalizeBaseUrl: () => normalizeBaseUrl,
|
|
25
26
|
proofKeyFor: () => proofKeyFor,
|
|
@@ -91,6 +92,18 @@ function nextProofIat(proofKey) {
|
|
|
91
92
|
if (waitSec <= 0) return Promise.resolve(iat);
|
|
92
93
|
return sleep(waitSec * 1e3).then(() => iat);
|
|
93
94
|
}
|
|
95
|
+
function freshProofNonce() {
|
|
96
|
+
const webCrypto = globalThis.crypto;
|
|
97
|
+
if (typeof webCrypto?.randomUUID === "function") {
|
|
98
|
+
return webCrypto.randomUUID();
|
|
99
|
+
}
|
|
100
|
+
if (typeof webCrypto?.getRandomValues === "function") {
|
|
101
|
+
return (0, import_viem.bytesToHex)(webCrypto.getRandomValues(new Uint8Array(16)));
|
|
102
|
+
}
|
|
103
|
+
throw new import_errors.WriteRequestError(
|
|
104
|
+
"No secure random source available to build a proof nonce; provide a crypto global"
|
|
105
|
+
);
|
|
106
|
+
}
|
|
94
107
|
function proofKeyFor(parts) {
|
|
95
108
|
return (0, import_viem.bytesToHex)(
|
|
96
109
|
(0, import_sha2.sha256)(
|
|
@@ -132,6 +145,7 @@ async function sendWithFreshProof(label, fetchFn, options, proofKey, build) {
|
|
|
132
145
|
0 && (module.exports = {
|
|
133
146
|
errorMessage,
|
|
134
147
|
finiteOr,
|
|
148
|
+
freshProofNonce,
|
|
135
149
|
nextProofIat,
|
|
136
150
|
normalizeBaseUrl,
|
|
137
151
|
proofKeyFor,
|
|
@@ -1 +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"]}
|
|
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/**\n * A fresh `nonce` claim for one proof.\n *\n * @remarks\n * The Personal Server keys its replay guard on `(builder, nonce)` when a\n * proof carries a nonce, and on the whole proof when it does not. A nonce is\n * therefore what makes two identical requests signed inside the same second\n * distinct instead of the second being refused as a replay, which is the\n * difference between a poll loop that works and one that dies on its second\n * pass. Every question call sends one.\n */\nexport function freshProofNonce(): string {\n const webCrypto = globalThis.crypto;\n if (typeof webCrypto?.randomUUID === \"function\") {\n return webCrypto.randomUUID();\n }\n // Older runtimes expose getRandomValues without randomUUID; 16 random bytes\n // are the same uniqueness with a different spelling.\n if (typeof webCrypto?.getRandomValues === \"function\") {\n return bytesToHex(webCrypto.getRandomValues(new Uint8Array(16)));\n }\n throw new WriteRequestError(\n \"No secure random source available to build a proof nonce; provide a crypto global\",\n );\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"]}
|
|
@@ -45,6 +45,18 @@ export declare function sleep(ms: number): Promise<void>;
|
|
|
45
45
|
* than {@link PROOF_IAT_MAX_AHEAD_SECONDS}.
|
|
46
46
|
*/
|
|
47
47
|
export declare function nextProofIat(proofKey: string): Promise<number>;
|
|
48
|
+
/**
|
|
49
|
+
* A fresh `nonce` claim for one proof.
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* The Personal Server keys its replay guard on `(builder, nonce)` when a
|
|
53
|
+
* proof carries a nonce, and on the whole proof when it does not. A nonce is
|
|
54
|
+
* therefore what makes two identical requests signed inside the same second
|
|
55
|
+
* distinct instead of the second being refused as a replay, which is the
|
|
56
|
+
* difference between a poll loop that works and one that dies on its second
|
|
57
|
+
* pass. Every question call sends one.
|
|
58
|
+
*/
|
|
59
|
+
export declare function freshProofNonce(): string;
|
|
48
60
|
/** The identity a proof is deduplicated by. */
|
|
49
61
|
export declare function proofKeyFor(parts: {
|
|
50
62
|
aud: string;
|
|
@@ -61,6 +61,18 @@ function nextProofIat(proofKey) {
|
|
|
61
61
|
if (waitSec <= 0) return Promise.resolve(iat);
|
|
62
62
|
return sleep(waitSec * 1e3).then(() => iat);
|
|
63
63
|
}
|
|
64
|
+
function freshProofNonce() {
|
|
65
|
+
const webCrypto = globalThis.crypto;
|
|
66
|
+
if (typeof webCrypto?.randomUUID === "function") {
|
|
67
|
+
return webCrypto.randomUUID();
|
|
68
|
+
}
|
|
69
|
+
if (typeof webCrypto?.getRandomValues === "function") {
|
|
70
|
+
return bytesToHex(webCrypto.getRandomValues(new Uint8Array(16)));
|
|
71
|
+
}
|
|
72
|
+
throw new WriteRequestError(
|
|
73
|
+
"No secure random source available to build a proof nonce; provide a crypto global"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
64
76
|
function proofKeyFor(parts) {
|
|
65
77
|
return bytesToHex(
|
|
66
78
|
sha256(
|
|
@@ -101,6 +113,7 @@ async function sendWithFreshProof(label, fetchFn, options, proofKey, build) {
|
|
|
101
113
|
export {
|
|
102
114
|
errorMessage,
|
|
103
115
|
finiteOr,
|
|
116
|
+
freshProofNonce,
|
|
104
117
|
nextProofIat,
|
|
105
118
|
normalizeBaseUrl,
|
|
106
119
|
proofKeyFor,
|
|
@@ -1 +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"]}
|
|
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/**\n * A fresh `nonce` claim for one proof.\n *\n * @remarks\n * The Personal Server keys its replay guard on `(builder, nonce)` when a\n * proof carries a nonce, and on the whole proof when it does not. A nonce is\n * therefore what makes two identical requests signed inside the same second\n * distinct instead of the second being refused as a replay, which is the\n * difference between a poll loop that works and one that dies on its second\n * pass. Every question call sends one.\n */\nexport function freshProofNonce(): string {\n const webCrypto = globalThis.crypto;\n if (typeof webCrypto?.randomUUID === \"function\") {\n return webCrypto.randomUUID();\n }\n // Older runtimes expose getRandomValues without randomUUID; 16 random bytes\n // are the same uniqueness with a different spelling.\n if (typeof webCrypto?.getRandomValues === \"function\") {\n return bytesToHex(webCrypto.getRandomValues(new Uint8Array(16)));\n }\n throw new WriteRequestError(\n \"No secure random source available to build a proof nonce; provide a crypto global\",\n );\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"]}
|