@opendatalabs/personal-server-ts-server 1.34.1 → 1.35.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/dist/api-auth.d.ts +1 -0
- package/dist/api-auth.d.ts.map +1 -1
- package/dist/api-auth.js.map +1 -1
- package/dist/api.d.ts +3 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -0
- package/dist/api.js.map +1 -1
- package/dist/app.d.ts +14 -2
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +64 -8
- package/dist/app.js.map +1 -1
- package/dist/bootstrap.d.ts +19 -0
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +87 -4
- package/dist/bootstrap.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +7 -0
- package/dist/client.js.map +1 -1
- package/dist/enclave-main.d.ts.map +1 -1
- package/dist/enclave-main.js +0 -2
- package/dist/enclave-main.js.map +1 -1
- package/dist/mcp/durable-state.d.ts +2 -1
- package/dist/mcp/durable-state.d.ts.map +1 -1
- package/dist/mcp/durable-state.js +35 -1
- package/dist/mcp/durable-state.js.map +1 -1
- package/dist/pdpp/bootstrap.d.ts.map +1 -1
- package/dist/pdpp/bootstrap.js +1 -0
- package/dist/pdpp/bootstrap.js.map +1 -1
- package/dist/pdpp/records-bootstrap.d.ts +47 -1
- package/dist/pdpp/records-bootstrap.d.ts.map +1 -1
- package/dist/pdpp/records-bootstrap.js +78 -2
- package/dist/pdpp/records-bootstrap.js.map +1 -1
- package/dist/pdpp/replica-authorization.d.ts +35 -0
- package/dist/pdpp/replica-authorization.d.ts.map +1 -0
- package/dist/pdpp/replica-authorization.js +234 -0
- package/dist/pdpp/replica-authorization.js.map +1 -0
- package/dist/pending-blob-deletions.d.ts +1 -0
- package/dist/pending-blob-deletions.d.ts.map +1 -1
- package/dist/pending-blob-deletions.js +8 -4
- package/dist/pending-blob-deletions.js.map +1 -1
- package/dist/routes/mcp.d.ts +18 -2
- package/dist/routes/mcp.d.ts.map +1 -1
- package/dist/routes/mcp.js +111 -11
- package/dist/routes/mcp.js.map +1 -1
- package/dist/routes/oura.d.ts +6 -0
- package/dist/routes/oura.d.ts.map +1 -0
- package/dist/routes/oura.js +272 -0
- package/dist/routes/oura.js.map +1 -0
- package/dist/routes/pdpp-auth-query-keys.d.ts +7 -0
- package/dist/routes/pdpp-auth-query-keys.d.ts.map +1 -0
- package/dist/routes/pdpp-auth-query-keys.js +25 -0
- package/dist/routes/pdpp-auth-query-keys.js.map +1 -0
- package/dist/routes/pdpp-auth.d.ts +3 -1
- package/dist/routes/pdpp-auth.d.ts.map +1 -1
- package/dist/routes/pdpp-auth.js +126 -45
- package/dist/routes/pdpp-auth.js.map +1 -1
- package/dist/routes/pdpp-blobs.d.ts +13 -1
- package/dist/routes/pdpp-blobs.d.ts.map +1 -1
- package/dist/routes/pdpp-blobs.js +100 -65
- package/dist/routes/pdpp-blobs.js.map +1 -1
- package/dist/routes/pdpp-connections.d.ts.map +1 -1
- package/dist/routes/pdpp-connections.js +24 -1
- package/dist/routes/pdpp-connections.js.map +1 -1
- package/dist/routes/pdpp-instance-bindings.d.ts +3 -0
- package/dist/routes/pdpp-instance-bindings.d.ts.map +1 -1
- package/dist/routes/pdpp-instance-bindings.js +63 -0
- package/dist/routes/pdpp-instance-bindings.js.map +1 -1
- package/dist/routes/pdpp-record-query.d.ts +19 -0
- package/dist/routes/pdpp-record-query.d.ts.map +1 -0
- package/dist/routes/pdpp-record-query.js +51 -0
- package/dist/routes/pdpp-record-query.js.map +1 -0
- package/dist/routes/pdpp-records.d.ts +12 -1
- package/dist/routes/pdpp-records.d.ts.map +1 -1
- package/dist/routes/pdpp-records.js +120 -233
- package/dist/routes/pdpp-records.js.map +1 -1
- package/dist/routes/pdpp-replica-quarantine-fixture.d.ts +65 -0
- package/dist/routes/pdpp-replica-quarantine-fixture.d.ts.map +1 -0
- package/dist/routes/pdpp-replica-quarantine-fixture.js +389 -0
- package/dist/routes/pdpp-replica-quarantine-fixture.js.map +1 -0
- package/dist/storage/claude-method-transition.d.ts +12 -0
- package/dist/storage/claude-method-transition.d.ts.map +1 -0
- package/dist/storage/claude-method-transition.js +139 -0
- package/dist/storage/claude-method-transition.js.map +1 -0
- package/dist/storage/import-operations.d.ts +16 -0
- package/dist/storage/import-operations.d.ts.map +1 -1
- package/dist/storage/import-operations.js +54 -1
- package/dist/storage/import-operations.js.map +1 -1
- package/dist/storage/index-schema.d.ts +1 -1
- package/dist/storage/index-schema.d.ts.map +1 -1
- package/dist/storage/index-schema.js +22 -5
- package/dist/storage/index-schema.js.map +1 -1
- package/dist/storage/legacy-scope-authority.d.ts.map +1 -1
- package/dist/storage/legacy-scope-authority.js +83 -11
- package/dist/storage/legacy-scope-authority.js.map +1 -1
- package/dist/storage/node-data-storage.d.ts.map +1 -1
- package/dist/storage/node-data-storage.js +58 -2
- package/dist/storage/node-data-storage.js.map +1 -1
- package/dist/storage/pdpp-binary-chunks.d.ts +120 -0
- package/dist/storage/pdpp-binary-chunks.d.ts.map +1 -0
- package/dist/storage/pdpp-binary-chunks.js +480 -0
- package/dist/storage/pdpp-binary-chunks.js.map +1 -0
- package/dist/storage/pdpp-records-sqlite-store.d.ts +96 -15
- package/dist/storage/pdpp-records-sqlite-store.d.ts.map +1 -1
- package/dist/storage/pdpp-records-sqlite-store.js +616 -211
- package/dist/storage/pdpp-records-sqlite-store.js.map +1 -1
- package/dist/storage/pdpp-replica-canonical-port.d.ts +202 -0
- package/dist/storage/pdpp-replica-canonical-port.d.ts.map +1 -0
- package/dist/storage/pdpp-replica-canonical-port.js +451 -0
- package/dist/storage/pdpp-replica-canonical-port.js.map +1 -0
- package/dist/storage/pdpp-replica-coordinator-state.d.ts +16 -0
- package/dist/storage/pdpp-replica-coordinator-state.d.ts.map +1 -0
- package/dist/storage/pdpp-replica-coordinator-state.js +25 -0
- package/dist/storage/pdpp-replica-coordinator-state.js.map +1 -0
- package/dist/storage/pdpp-replica-copies.d.ts +81 -0
- package/dist/storage/pdpp-replica-copies.d.ts.map +1 -0
- package/dist/storage/pdpp-replica-copies.js +59 -0
- package/dist/storage/pdpp-replica-copies.js.map +1 -0
- package/dist/storage/pdpp-replica-reconciler.d.ts +96 -0
- package/dist/storage/pdpp-replica-reconciler.d.ts.map +1 -0
- package/dist/storage/pdpp-replica-reconciler.js +710 -0
- package/dist/storage/pdpp-replica-reconciler.js.map +1 -0
- package/dist/storage/pdpp-replica-sql.d.ts +13 -0
- package/dist/storage/pdpp-replica-sql.d.ts.map +1 -0
- package/dist/storage/pdpp-replica-sql.js +43 -0
- package/dist/storage/pdpp-replica-sql.js.map +1 -0
- package/dist/storage/pdpp-replica-staging.d.ts +36 -0
- package/dist/storage/pdpp-replica-staging.d.ts.map +1 -0
- package/dist/storage/pdpp-replica-staging.js +202 -0
- package/dist/storage/pdpp-replica-staging.js.map +1 -0
- package/dist/storage/pdpp-replica-visibility-view.d.ts +64 -0
- package/dist/storage/pdpp-replica-visibility-view.d.ts.map +1 -0
- package/dist/storage/pdpp-replica-visibility-view.js +1397 -0
- package/dist/storage/pdpp-replica-visibility-view.js.map +1 -0
- package/dist/storage/pdpp-replica-writer-identity.d.ts +64 -0
- package/dist/storage/pdpp-replica-writer-identity.d.ts.map +1 -0
- package/dist/storage/pdpp-replica-writer-identity.js +81 -0
- package/dist/storage/pdpp-replica-writer-identity.js.map +1 -0
- package/dist/storage/pdpp-replication-door.d.ts +60 -0
- package/dist/storage/pdpp-replication-door.d.ts.map +1 -0
- package/dist/storage/pdpp-replication-door.js +203 -0
- package/dist/storage/pdpp-replication-door.js.map +1 -0
- package/dist/storage/pdpp-replication-host.d.ts +67 -0
- package/dist/storage/pdpp-replication-host.d.ts.map +1 -0
- package/dist/storage/pdpp-replication-host.js +159 -0
- package/dist/storage/pdpp-replication-host.js.map +1 -0
- package/dist/storage/pdpp-replication-memory.fixture.d.ts +2 -0
- package/dist/storage/pdpp-replication-memory.fixture.d.ts.map +1 -0
- package/dist/storage/pdpp-replication-memory.fixture.js +267 -0
- package/dist/storage/pdpp-replication-memory.fixture.js.map +1 -0
- package/dist/storage/pdpp-replication-preparation.d.ts +3 -0
- package/dist/storage/pdpp-replication-preparation.d.ts.map +1 -0
- package/dist/storage/pdpp-replication-preparation.js +3 -0
- package/dist/storage/pdpp-replication-preparation.js.map +1 -0
- package/dist/storage/pdpp-replication-promotion-memory.fixture.d.ts +2 -0
- package/dist/storage/pdpp-replication-promotion-memory.fixture.d.ts.map +1 -0
- package/dist/storage/pdpp-replication-promotion-memory.fixture.js +65 -0
- package/dist/storage/pdpp-replication-promotion-memory.fixture.js.map +1 -0
- package/dist/storage/pdpp-replication-receiver.d.ts +230 -0
- package/dist/storage/pdpp-replication-receiver.d.ts.map +1 -0
- package/dist/storage/pdpp-replication-receiver.js +950 -0
- package/dist/storage/pdpp-replication-receiver.js.map +1 -0
- package/dist/storage/pdpp-replication-sqlite.d.ts +95 -0
- package/dist/storage/pdpp-replication-sqlite.d.ts.map +1 -0
- package/dist/storage/pdpp-replication-sqlite.js +615 -0
- package/dist/storage/pdpp-replication-sqlite.js.map +1 -0
- package/dist/storage/pdpp-replication-transport.d.ts +39 -0
- package/dist/storage/pdpp-replication-transport.d.ts.map +1 -0
- package/dist/storage/pdpp-replication-transport.js +196 -0
- package/dist/storage/pdpp-replication-transport.js.map +1 -0
- package/dist/storage/schema-versions.d.ts +5 -0
- package/dist/storage/schema-versions.d.ts.map +1 -0
- package/dist/storage/schema-versions.js +26 -0
- package/dist/storage/schema-versions.js.map +1 -0
- package/dist/storage/strava-method-transition.d.ts +18 -0
- package/dist/storage/strava-method-transition.d.ts.map +1 -0
- package/dist/storage/strava-method-transition.js +209 -0
- package/dist/storage/strava-method-transition.js.map +1 -0
- package/dist/ui/ps-lite-debug.js +137018 -106931
- package/package.json +3 -3
|
@@ -1,27 +1,18 @@
|
|
|
1
|
+
import { grantIsCandidate } from "@opendatalabs/personal-server-ts-core/pdpp";
|
|
2
|
+
import { parseRecordQuery } from "./pdpp-record-query.js";
|
|
1
3
|
import { Hono } from "hono";
|
|
2
4
|
import { randomUUID } from "node:crypto";
|
|
3
5
|
import { isRfc3339DateTime, PdppBindingError, } from "../storage/pdpp-records-sqlite-store.js";
|
|
4
6
|
import { PdppError } from "@opendatalabs/personal-server-ts-core/errors/pdpp";
|
|
5
7
|
import { PDPP_VERSION } from "@opendatalabs/personal-server-ts-core/pdpp-version";
|
|
6
|
-
import {
|
|
8
|
+
import { PdppAuthorizationUnavailableError } from "@opendatalabs/personal-server-ts-core/ports/pdpp-auth";
|
|
9
|
+
import { REPLICA_SOURCE_MAX_BLOB_BYTES, REPLICA_SOURCE_MAX_RECORD_JSON_BYTES, } from "@opendatalabs/personal-server-ts-core/replication/limits";
|
|
10
|
+
import { CursorExpiredError, InvalidCursorError, InvalidCursorSyntaxError, recordKeyWithinGrantResources, mapInactiveToError, recordWithinGrantTimeConstraint, resolveReadScope, summarizeIngest, } from "@opendatalabs/personal-server-ts-core/storage/pdpp-records";
|
|
7
11
|
import { createRecordDataValidator } from "../pdpp/record-schema.js";
|
|
8
12
|
import { StreamCompletionError, } from "../storage/pdpp-stream-completion.js";
|
|
9
|
-
const
|
|
10
|
-
const MAX_LIMIT = 100;
|
|
11
|
-
export const MAX_BLOB_UPLOAD_BYTES = 512 * 1024 * 1024;
|
|
12
|
-
function rebindListCursor(template, order, last) {
|
|
13
|
-
const bound = decodeCursor(template);
|
|
14
|
-
if (bound.kind !== "list")
|
|
15
|
-
throw new InvalidCursorError();
|
|
16
|
-
return encodeCursor({
|
|
17
|
-
...bound,
|
|
18
|
-
order,
|
|
19
|
-
sortValue: last.emittedAt,
|
|
20
|
-
recordKey: last.recordKey,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
13
|
+
export const MAX_BLOB_UPLOAD_BYTES = REPLICA_SOURCE_MAX_BLOB_BYTES;
|
|
23
14
|
/** 512 MiB — max body size for PDPP record ingest and replace routes. */
|
|
24
|
-
export const MAX_INGEST_BODY_BYTES =
|
|
15
|
+
export const MAX_INGEST_BODY_BYTES = REPLICA_SOURCE_MAX_RECORD_JSON_BYTES;
|
|
25
16
|
/** 64 KiB of UTF-8 — max body size for the completion metadata route. */
|
|
26
17
|
export const MAX_COMPLETION_BODY_BYTES = 64 * 1024;
|
|
27
18
|
class CompletionBodyTooLarge extends Error {
|
|
@@ -235,6 +226,9 @@ function sendError(c, err, reqId) {
|
|
|
235
226
|
"Request-Id": reqId,
|
|
236
227
|
"PDPP-Version": PDPP_VERSION,
|
|
237
228
|
};
|
|
229
|
+
if (err instanceof PdppAuthorizationUnavailableError) {
|
|
230
|
+
headers["Retry-After"] = String(err.retryAfterSeconds);
|
|
231
|
+
}
|
|
238
232
|
if (err.status === 401) {
|
|
239
233
|
headers["WWW-Authenticate"] =
|
|
240
234
|
`Bearer error="invalid_token", resource_metadata="${resourceMetadataUrlFor(c)}"`;
|
|
@@ -262,99 +256,6 @@ function unauthorized(c, reqId, resourceMetadataUrl) {
|
|
|
262
256
|
"WWW-Authenticate": `Bearer error="invalid_token", resource_metadata="${resourceMetadataUrl}"`,
|
|
263
257
|
});
|
|
264
258
|
}
|
|
265
|
-
function parseLimit(raw) {
|
|
266
|
-
if (!raw)
|
|
267
|
-
return { limit: DEFAULT_LIMIT, clamped: false };
|
|
268
|
-
const parsed = Number.parseInt(raw, 10);
|
|
269
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
270
|
-
throw new PdppError("invalid_request", "limit must be a positive integer", {
|
|
271
|
-
param: "limit",
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
if (parsed > MAX_LIMIT)
|
|
275
|
-
return { limit: MAX_LIMIT, clamped: true };
|
|
276
|
-
return { limit: parsed, clamped: false };
|
|
277
|
-
}
|
|
278
|
-
function parseOrder(raw) {
|
|
279
|
-
if (raw === undefined)
|
|
280
|
-
return "desc";
|
|
281
|
-
if (raw === "asc" || raw === "desc")
|
|
282
|
-
return raw;
|
|
283
|
-
throw new PdppError("invalid_request", "order must be 'asc' or 'desc'", {
|
|
284
|
-
param: "order",
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
/** Query parameters implemented by each endpoint. Unsupported shapes must fail. */
|
|
288
|
-
const KNOWN_QUERY_PARAMS = {
|
|
289
|
-
listStreams: [],
|
|
290
|
-
streamMetadata: [],
|
|
291
|
-
listRecords: ["limit", "order", "cursor", "fields", "changes_since"],
|
|
292
|
-
getRecord: ["fields"],
|
|
293
|
-
deleteRecord: [],
|
|
294
|
-
};
|
|
295
|
-
/** Validate exact parameter names before consulting serving metadata. */
|
|
296
|
-
function rejectUnknownParams(c, endpoint, tokenKind) {
|
|
297
|
-
const allowed = KNOWN_QUERY_PARAMS[endpoint];
|
|
298
|
-
const recordRead = endpoint === "listRecords" || endpoint === "getRecord";
|
|
299
|
-
for (const key of new URL(c.req.url).searchParams.keys()) {
|
|
300
|
-
if (allowed.includes(key))
|
|
301
|
-
continue;
|
|
302
|
-
const base = key.replace(/\[.*$/, "");
|
|
303
|
-
// Current serving metadata declares no expandable relations. Client
|
|
304
|
-
// requests must still receive invalid_request, before metadata lookup.
|
|
305
|
-
if (recordRead &&
|
|
306
|
-
tokenKind === "owner" &&
|
|
307
|
-
(base === "expand" || base === "expand_limit")) {
|
|
308
|
-
throw new PdppError("invalid_expand", "No expandable relation is declared", {
|
|
309
|
-
param: key,
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
throw new PdppError("invalid_request", `Unsupported query parameter '${key}'`, {
|
|
313
|
-
param: key,
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Narrow a client's requested `fields` within what its grant already allows.
|
|
319
|
-
*
|
|
320
|
-
* Three rules, in order:
|
|
321
|
-
* - a field outside the grant is a 400, not a silent drop. A client asking
|
|
322
|
-
* for something it was never granted has made an error it needs to see;
|
|
323
|
-
* quietly returning less would let it believe it received that field.
|
|
324
|
-
* - the result is the intersection, so a request can only ever narrow.
|
|
325
|
-
* - under a v0.1 grant ONLY, the declaration's required fields are re-added,
|
|
326
|
-
* because v0.1 §8 keeps schema-required fields in every projection
|
|
327
|
-
* regardless of the request. A v0.2 grant gets the bare intersection:
|
|
328
|
-
* `v0.2-4-2` builds disclosed `data` from only the members permitted by
|
|
329
|
-
* the grant AND the request-time selection, and `v0.2-4-1` forbids adding
|
|
330
|
-
* one back because the schema requires it. Re-widening a sparse v0.2
|
|
331
|
-
* request would disclose more than both the owner and the client asked
|
|
332
|
-
* for, which is the one direction this function must never move.
|
|
333
|
-
*
|
|
334
|
-
* Returns the full granted set when no `fields` was requested, preserving the
|
|
335
|
-
* existing default.
|
|
336
|
-
*/
|
|
337
|
-
function narrowClientFields(requested, granted, declaration, schemaRequiredFloor) {
|
|
338
|
-
if (!requested || requested.length === 0)
|
|
339
|
-
return granted;
|
|
340
|
-
if (!granted)
|
|
341
|
-
return requested;
|
|
342
|
-
const grantedSet = new Set(granted);
|
|
343
|
-
for (const field of requested) {
|
|
344
|
-
if (!grantedSet.has(field)) {
|
|
345
|
-
throw new PdppError("invalid_request", `Field '${field}' is not in this grant's authorized fields for the stream`, { param: "fields" });
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
const narrowed = new Set(requested.filter((f) => grantedSet.has(f)));
|
|
349
|
-
if (schemaRequiredFloor) {
|
|
350
|
-
// v0.1 only: the consent floor survives a sparse request.
|
|
351
|
-
for (const required of declaration?.requiredFields ?? []) {
|
|
352
|
-
if (grantedSet.has(required))
|
|
353
|
-
narrowed.add(required);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return Array.from(narrowed);
|
|
357
|
-
}
|
|
358
259
|
/**
|
|
359
260
|
* Project a declared JSON Schema down to exactly the granted fields.
|
|
360
261
|
*
|
|
@@ -383,6 +284,11 @@ function projectSchemaToFields(schema, fields) {
|
|
|
383
284
|
export function pdppRecordsRoutes(deps) {
|
|
384
285
|
const app = new Hono();
|
|
385
286
|
const validateData = createRecordDataValidator(deps.declarations);
|
|
287
|
+
const readsFor = deps.reads ?? (() => deps.store);
|
|
288
|
+
/** The token class selects the reader; request input never does. */
|
|
289
|
+
function reads(context) {
|
|
290
|
+
return readsFor(context.tokenKind === "owner" ? "owner" : "grant");
|
|
291
|
+
}
|
|
386
292
|
/**
|
|
387
293
|
* Append one PDPP read to the owner access feed.
|
|
388
294
|
*
|
|
@@ -424,6 +330,8 @@ export function pdppRecordsRoutes(deps) {
|
|
|
424
330
|
app.use("*", async (c, next) => {
|
|
425
331
|
const reqId = requestId();
|
|
426
332
|
c.set("reqId", reqId);
|
|
333
|
+
// Record responses carry personal data: no shared or stored copies.
|
|
334
|
+
c.header("Cache-Control", "private, no-store");
|
|
427
335
|
await next();
|
|
428
336
|
});
|
|
429
337
|
async function authenticate(c, reqId) {
|
|
@@ -432,7 +340,16 @@ export function pdppRecordsRoutes(deps) {
|
|
|
432
340
|
if (!token) {
|
|
433
341
|
return { error: unauthorized(c, reqId, resourceMetadataUrlFor(c)) };
|
|
434
342
|
}
|
|
435
|
-
|
|
343
|
+
let context;
|
|
344
|
+
try {
|
|
345
|
+
context = await deps.auth.resolveToken(token);
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (!(error instanceof PdppAuthorizationUnavailableError))
|
|
349
|
+
throw error;
|
|
350
|
+
logApiError(deps, "authorization", reqId, error);
|
|
351
|
+
return { error: sendError(c, error, reqId) };
|
|
352
|
+
}
|
|
436
353
|
// Chain enforcement, when the deployment configures it. Placed here so
|
|
437
354
|
// EVERY endpoint is covered by one gate: a check added per-route is a
|
|
438
355
|
// check that a later route forgets, and an unenforced read path is
|
|
@@ -543,7 +460,7 @@ export function pdppRecordsRoutes(deps) {
|
|
|
543
460
|
if (error)
|
|
544
461
|
return error;
|
|
545
462
|
try {
|
|
546
|
-
|
|
463
|
+
parseRecordQuery(c.req.url, "listStreams");
|
|
547
464
|
}
|
|
548
465
|
catch (err) {
|
|
549
466
|
return sendError(c, mapAndLog(deps, "GET /v1/streams", reqId, err), reqId);
|
|
@@ -563,7 +480,7 @@ export function pdppRecordsRoutes(deps) {
|
|
|
563
480
|
if (context.tokenKind === "owner" && instanceIds.length === 0) {
|
|
564
481
|
return sendError(c, mapAndLog(deps, "GET /v1/streams", reqId, new PdppError("grant_stream_not_allowed", "Owner token is not scoped to a currently owned instance")), reqId);
|
|
565
482
|
}
|
|
566
|
-
const streams =
|
|
483
|
+
const streams = reads(context).listStreams(instanceIds);
|
|
567
484
|
// A client token gets a closed projection: only the streams its grant
|
|
568
485
|
// names, and counts/recency computed over only the records the grant
|
|
569
486
|
// actually exposes. The store's raw listing counts every record in the
|
|
@@ -572,6 +489,9 @@ export function pdppRecordsRoutes(deps) {
|
|
|
572
489
|
const visibleStreams = context.tokenKind === "owner"
|
|
573
490
|
? streams.map((s) => ({
|
|
574
491
|
name: s.stream,
|
|
492
|
+
confirmation: s.confirmation,
|
|
493
|
+
visibility: s.visibility,
|
|
494
|
+
status: s.status,
|
|
575
495
|
recordCount: s.recordCount,
|
|
576
496
|
lastUpdated: s.lastUpdated,
|
|
577
497
|
budgetExhausted: false,
|
|
@@ -582,7 +502,10 @@ export function pdppRecordsRoutes(deps) {
|
|
|
582
502
|
const streamGrant = context.grant?.streams.find((g) => g.name === s.stream);
|
|
583
503
|
return {
|
|
584
504
|
name: s.stream,
|
|
585
|
-
|
|
505
|
+
confirmation: undefined,
|
|
506
|
+
visibility: undefined,
|
|
507
|
+
status: undefined,
|
|
508
|
+
...summarizeGrantVisible(reads(context), s.stream, streamGrant?.instance_ids ?? [], streamGrant),
|
|
586
509
|
};
|
|
587
510
|
});
|
|
588
511
|
// Report truncation explicitly rather than letting a capped count read as
|
|
@@ -598,6 +521,8 @@ export function pdppRecordsRoutes(deps) {
|
|
|
598
521
|
name: s.name,
|
|
599
522
|
record_count: s.recordCount,
|
|
600
523
|
last_updated: s.lastUpdated,
|
|
524
|
+
...(s.confirmation && { confirmation: s.confirmation }),
|
|
525
|
+
...(s.status && { visibility: s.visibility, status: s.status }),
|
|
601
526
|
})),
|
|
602
527
|
...(cappedStreams.length > 0 && {
|
|
603
528
|
meta: {
|
|
@@ -620,7 +545,7 @@ export function pdppRecordsRoutes(deps) {
|
|
|
620
545
|
if (error)
|
|
621
546
|
return error;
|
|
622
547
|
try {
|
|
623
|
-
|
|
548
|
+
parseRecordQuery(c.req.url, "streamMetadata");
|
|
624
549
|
const stream = c.req.param("stream");
|
|
625
550
|
const { declaration, ownerInstanceIds } = readDeclaration(context, stream);
|
|
626
551
|
const scope = resolveReadScope(context, stream, declaration);
|
|
@@ -673,10 +598,10 @@ export function pdppRecordsRoutes(deps) {
|
|
|
673
598
|
if (error)
|
|
674
599
|
return error;
|
|
675
600
|
try {
|
|
676
|
-
|
|
601
|
+
const query = parseRecordQuery(c.req.url, "listRecords", context.tokenKind);
|
|
677
602
|
const stream = c.req.param("stream");
|
|
678
603
|
const { declaration, ownerInstanceIds } = readDeclaration(context, stream);
|
|
679
|
-
const scope = resolveReadScope(context, stream, declaration);
|
|
604
|
+
const scope = resolveReadScope(context, stream, declaration, query.fields);
|
|
680
605
|
const effectiveInstanceIds = context.tokenKind === "owner" ? ownerInstanceIds : scope.instanceIds;
|
|
681
606
|
if (context.tokenKind === "client") {
|
|
682
607
|
requireRegisteredConnections(effectiveInstanceIds);
|
|
@@ -684,29 +609,13 @@ export function pdppRecordsRoutes(deps) {
|
|
|
684
609
|
if (context.tokenKind === "owner" && effectiveInstanceIds.length === 0) {
|
|
685
610
|
throw new PdppError("grant_stream_not_allowed", "Owner token is not scoped to a currently owned instance");
|
|
686
611
|
}
|
|
687
|
-
const { limit, clamped } =
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
.
|
|
693
|
-
|
|
694
|
-
// an unsupported shape must not be silently ignored. A client token
|
|
695
|
-
// previously had its `fields` parsed and then discarded, so
|
|
696
|
-
// `?fields=id` returned every granted field — the response did
|
|
697
|
-
// something other than what was asked, with a 200.
|
|
698
|
-
//
|
|
699
|
-
// A client may only NARROW within its grant; it can never widen. So the
|
|
700
|
-
// request is validated against the granted set, then intersected with
|
|
701
|
-
// it, then the schema-required floor is re-added.
|
|
702
|
-
const fields = context.tokenKind === "client"
|
|
703
|
-
? narrowClientFields(requestedFields, scope.fields, declaration, scope.schemaRequiredFloor)
|
|
704
|
-
: requestedFields
|
|
705
|
-
? [...requestedFields]
|
|
706
|
-
: undefined;
|
|
707
|
-
const cursor = c.req.query("cursor");
|
|
708
|
-
if (c.req.query("changes_since") !== undefined) {
|
|
709
|
-
// `fields` is passed here for ELIGIBILITY narrowing only (spec §4:
|
|
612
|
+
const { limit, clamped, order, cursor } = query;
|
|
613
|
+
const fields = scope.fields;
|
|
614
|
+
if (query.changesSince !== undefined) {
|
|
615
|
+
// Eligibility compares the frozen candidate grant projection; sparse
|
|
616
|
+
// request fields shape only the response. Legacy profiles retain their
|
|
617
|
+
// existing request-field eligibility. The store returns raw rows.
|
|
618
|
+
// (spec §4:
|
|
710
619
|
// a record whose only change was to an unauthorized field must not
|
|
711
620
|
// appear as "changed" at all). The store returns each row's RAW,
|
|
712
621
|
// unprojected data regardless of `fields` — response-shaping
|
|
@@ -715,14 +624,26 @@ export function pdppRecordsRoutes(deps) {
|
|
|
715
624
|
// field value. This split matters: if the store projected the
|
|
716
625
|
// returned data too, a grant whose fields exclude the
|
|
717
626
|
// time_constraint field would silently break that filter.
|
|
718
|
-
const page =
|
|
627
|
+
const page = reads(context).changesSince(stream, {
|
|
719
628
|
instanceIds: effectiveInstanceIds,
|
|
720
|
-
changesSince:
|
|
721
|
-
|
|
629
|
+
changesSince: grantIsCandidate(context.grant) &&
|
|
630
|
+
query.changesSince === "beginning"
|
|
631
|
+
? undefined
|
|
632
|
+
: query.changesSince,
|
|
633
|
+
cursor,
|
|
722
634
|
limit,
|
|
723
|
-
fields
|
|
635
|
+
fields: grantIsCandidate(context.grant)
|
|
636
|
+
? scope.streamGrant.fields
|
|
637
|
+
: fields,
|
|
638
|
+
...(grantIsCandidate(context.grant) && {
|
|
639
|
+
expireOnErasure: true,
|
|
640
|
+
isVisible: (row) => recordKeyWithinGrantResources(row.recordKey, scope.streamGrant) &&
|
|
641
|
+
recordWithinGrantTimeConstraint(row.data, scope.streamGrant),
|
|
642
|
+
}),
|
|
724
643
|
});
|
|
725
644
|
const visible = page.data.filter((row) => {
|
|
645
|
+
if (grantIsCandidate(context.grant))
|
|
646
|
+
return true; // store compared both authorized historical states
|
|
726
647
|
if (row.deleted) {
|
|
727
648
|
return recordKeyWithinGrantResources(row.recordKey, scope.streamGrant);
|
|
728
649
|
}
|
|
@@ -743,56 +664,22 @@ export function pdppRecordsRoutes(deps) {
|
|
|
743
664
|
...(page.nextChangesSince && {
|
|
744
665
|
next_changes_since: page.nextChangesSince,
|
|
745
666
|
}),
|
|
746
|
-
data: visible.map((row) => toRecordJson(stream, row, fields)),
|
|
667
|
+
data: visible.map((row) => toRecordJson(stream, row, fields, grantIsCandidate(context.grant))),
|
|
747
668
|
...(clamped && { meta }),
|
|
748
669
|
}, 200, { "Request-Id": reqId, "PDPP-Version": PDPP_VERSION });
|
|
749
670
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
let storeHasMore = false;
|
|
763
|
-
let cursorTemplate;
|
|
764
|
-
do {
|
|
765
|
-
const requestCursor = pageCursor;
|
|
766
|
-
const page = deps.store.listRecords(stream, {
|
|
767
|
-
instanceIds: effectiveInstanceIds,
|
|
768
|
-
// Fetch at most one client page from the store at a time. The route
|
|
769
|
-
// may need several store pages to collect enough grant-visible rows,
|
|
770
|
-
// but keeping this page size at the client limit means the store
|
|
771
|
-
// usually gives us a bound cursor exactly at the visible boundary.
|
|
772
|
-
limit,
|
|
773
|
-
order,
|
|
774
|
-
cursor: requestCursor,
|
|
775
|
-
});
|
|
776
|
-
for (const row of page.data) {
|
|
777
|
-
if (recordKeyWithinGrantResources(row.recordKey, scope.streamGrant) &&
|
|
778
|
-
recordWithinGrantTimeConstraint(row.data, scope.streamGrant)) {
|
|
779
|
-
visible.push(row);
|
|
780
|
-
}
|
|
781
|
-
if (visible.length > limit)
|
|
782
|
-
break;
|
|
783
|
-
}
|
|
784
|
-
pageCursor = page.nextCursor ?? undefined;
|
|
785
|
-
cursorTemplate = pageCursor ?? requestCursor;
|
|
786
|
-
storeHasMore = page.hasMore;
|
|
787
|
-
} while (visible.length <= limit && storeHasMore && pageCursor);
|
|
788
|
-
// limit + 1 visible rows means at least one more record the caller may
|
|
789
|
-
// see; trim it and report has_more from the filtered view.
|
|
790
|
-
const moreVisible = visible.length > limit;
|
|
791
|
-
const data = moreVisible ? visible.slice(0, limit) : visible;
|
|
792
|
-
const last = data[data.length - 1];
|
|
793
|
-
if (moreVisible && last && !cursorTemplate) {
|
|
794
|
-
throw new Error("Filtered list has more rows but no bound store cursor");
|
|
795
|
-
}
|
|
671
|
+
const page = reads(context).listRecords(stream, {
|
|
672
|
+
instanceIds: effectiveInstanceIds,
|
|
673
|
+
limit,
|
|
674
|
+
order,
|
|
675
|
+
cursor,
|
|
676
|
+
// Only a replica preview reader returns removal rows; only the owner
|
|
677
|
+
// may see one.
|
|
678
|
+
isVisible: (row) => recordKeyWithinGrantResources(row.recordKey, scope.streamGrant) &&
|
|
679
|
+
(row.deleted
|
|
680
|
+
? context.tokenKind === "owner"
|
|
681
|
+
: recordWithinGrantTimeConstraint(row.data, scope.streamGrant)),
|
|
682
|
+
});
|
|
796
683
|
const meta = clamped
|
|
797
684
|
? {
|
|
798
685
|
warnings: [
|
|
@@ -802,13 +689,9 @@ export function pdppRecordsRoutes(deps) {
|
|
|
802
689
|
: undefined;
|
|
803
690
|
const response = c.json({
|
|
804
691
|
object: "list",
|
|
805
|
-
has_more:
|
|
806
|
-
...(
|
|
807
|
-
|
|
808
|
-
cursorTemplate && {
|
|
809
|
-
next_cursor: rebindListCursor(cursorTemplate, order, last),
|
|
810
|
-
}),
|
|
811
|
-
data: data.map((row) => toRecordJson(stream, row, fields)),
|
|
692
|
+
has_more: page.hasMore,
|
|
693
|
+
...(page.nextCursor && { next_cursor: page.nextCursor }),
|
|
694
|
+
data: page.data.map((row) => toRecordJson(stream, row, fields, grantIsCandidate(context?.grant))),
|
|
812
695
|
...(meta && { meta }),
|
|
813
696
|
}, 200, { "Request-Id": reqId, "PDPP-Version": PDPP_VERSION });
|
|
814
697
|
await logClientRead(c, context, stream, "completed", reqId);
|
|
@@ -829,7 +712,7 @@ export function pdppRecordsRoutes(deps) {
|
|
|
829
712
|
if (error)
|
|
830
713
|
return error;
|
|
831
714
|
try {
|
|
832
|
-
|
|
715
|
+
const query = parseRecordQuery(c.req.url, "getRecord", context.tokenKind);
|
|
833
716
|
// §8 requires client-token `expand[]` to be REJECTED before the
|
|
834
717
|
// declaration is consulted, on single-record reads as well as lists.
|
|
835
718
|
// Silently ignoring it told a client its request was honored when a
|
|
@@ -837,7 +720,7 @@ export function pdppRecordsRoutes(deps) {
|
|
|
837
720
|
const stream = c.req.param("stream");
|
|
838
721
|
const recordKey = decodeURIComponent(c.req.param("id"));
|
|
839
722
|
const { declaration, ownerInstanceIds } = readDeclaration(context, stream);
|
|
840
|
-
const scope = resolveReadScope(context, stream, declaration);
|
|
723
|
+
const scope = resolveReadScope(context, stream, declaration, query.fields);
|
|
841
724
|
const effectiveInstanceIds = context.tokenKind === "owner" ? ownerInstanceIds : scope.instanceIds;
|
|
842
725
|
if (context.tokenKind === "client") {
|
|
843
726
|
requireRegisteredConnections(effectiveInstanceIds);
|
|
@@ -847,36 +730,21 @@ export function pdppRecordsRoutes(deps) {
|
|
|
847
730
|
}
|
|
848
731
|
let found;
|
|
849
732
|
for (const instance of effectiveInstanceIds) {
|
|
850
|
-
const record =
|
|
733
|
+
const record = reads(context).getRecord(instance, stream, recordKey);
|
|
851
734
|
if (record) {
|
|
852
735
|
found = record;
|
|
853
736
|
break;
|
|
854
737
|
}
|
|
855
738
|
}
|
|
856
739
|
if (!found ||
|
|
857
|
-
!
|
|
740
|
+
(!(found.deleted && context.tokenKind === "owner") &&
|
|
741
|
+
!recordWithinGrantTimeConstraint(found.data, scope.streamGrant))) {
|
|
858
742
|
throw new PdppError("not_found", "Record not found");
|
|
859
743
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
// was asked, and a client narrowing its own exposure did not get it.
|
|
865
|
-
const requestedFields = c.req
|
|
866
|
-
.query("fields")
|
|
867
|
-
?.split(",")
|
|
868
|
-
.map((f) => f.trim());
|
|
869
|
-
const fields = context.tokenKind === "client"
|
|
870
|
-
? narrowClientFields(requestedFields, scope.fields, declaration, scope.schemaRequiredFloor)
|
|
871
|
-
: (requestedFields ?? scope.fields);
|
|
872
|
-
const data = projectFields(found.data, fields);
|
|
873
|
-
const response = c.json({
|
|
874
|
-
object: "record",
|
|
875
|
-
id: recordKey,
|
|
876
|
-
stream,
|
|
877
|
-
data,
|
|
878
|
-
emitted_at: found.emittedAt,
|
|
879
|
-
}, 200, { "Request-Id": reqId, "PDPP-Version": PDPP_VERSION });
|
|
744
|
+
const fields = scope.fields;
|
|
745
|
+
const response = c.json(toRecordJson(stream, found.deleted
|
|
746
|
+
? found
|
|
747
|
+
: { ...found, data: projectFields(found.data, fields) }, undefined, grantIsCandidate(context?.grant)), 200, { "Request-Id": reqId, "PDPP-Version": PDPP_VERSION });
|
|
880
748
|
await logClientRead(c, context, stream, "completed", reqId);
|
|
881
749
|
return response;
|
|
882
750
|
}
|
|
@@ -892,7 +760,7 @@ export function pdppRecordsRoutes(deps) {
|
|
|
892
760
|
if (error)
|
|
893
761
|
return error;
|
|
894
762
|
try {
|
|
895
|
-
|
|
763
|
+
parseRecordQuery(c.req.url, "deleteRecord");
|
|
896
764
|
if (context.tokenKind !== "owner") {
|
|
897
765
|
throw new PdppError("authentication_error", "Delete requires an owner token");
|
|
898
766
|
}
|
|
@@ -919,6 +787,12 @@ export function pdppRecordsRoutes(deps) {
|
|
|
919
787
|
});
|
|
920
788
|
}
|
|
921
789
|
catch (err) {
|
|
790
|
+
if (err instanceof PdppBindingError) {
|
|
791
|
+
return c.json({
|
|
792
|
+
error: { code: err.reason, message: err.reason },
|
|
793
|
+
request_id: reqId,
|
|
794
|
+
}, 409, { "Request-Id": reqId, "PDPP-Version": PDPP_VERSION });
|
|
795
|
+
}
|
|
922
796
|
return sendError(c, mapAndLog(deps, "DELETE /v1/streams/:stream/records/:id", reqId, err), reqId);
|
|
923
797
|
}
|
|
924
798
|
});
|
|
@@ -1443,8 +1317,7 @@ const STREAM_SCAN_ROW_BUDGET = 10_000;
|
|
|
1443
1317
|
* against the record's real value, not a projected one.
|
|
1444
1318
|
*/
|
|
1445
1319
|
function summarizeGrantVisible(store, stream, instanceIds, streamGrant) {
|
|
1446
|
-
//
|
|
1447
|
-
// appear here; the guard keeps the narrowing honest rather than asserting.
|
|
1320
|
+
// A pending removal has no current grant-visible value to summarize.
|
|
1448
1321
|
const visible = (row) => !row.deleted &&
|
|
1449
1322
|
recordKeyWithinGrantResources(row.recordKey, streamGrant) &&
|
|
1450
1323
|
recordWithinGrantTimeConstraint(row.data, streamGrant);
|
|
@@ -1457,7 +1330,7 @@ function summarizeGrantVisible(store, stream, instanceIds, streamGrant) {
|
|
|
1457
1330
|
outer: while (scanned < STREAM_SCAN_ROW_BUDGET) {
|
|
1458
1331
|
const page = store.listRecords(stream, {
|
|
1459
1332
|
instanceIds,
|
|
1460
|
-
limit:
|
|
1333
|
+
limit: 100,
|
|
1461
1334
|
order: "desc",
|
|
1462
1335
|
cursor,
|
|
1463
1336
|
});
|
|
@@ -1486,7 +1359,7 @@ function summarizeGrantVisible(store, stream, instanceIds, streamGrant) {
|
|
|
1486
1359
|
for (;;) {
|
|
1487
1360
|
const page = store.listRecords(stream, {
|
|
1488
1361
|
instanceIds,
|
|
1489
|
-
limit:
|
|
1362
|
+
limit: 100,
|
|
1490
1363
|
order: "asc",
|
|
1491
1364
|
cursor,
|
|
1492
1365
|
});
|
|
@@ -1515,23 +1388,37 @@ function summarizeGrantVisible(store, stream, instanceIds, streamGrant) {
|
|
|
1515
1388
|
budgetExhausted: incomplete,
|
|
1516
1389
|
};
|
|
1517
1390
|
}
|
|
1518
|
-
|
|
1391
|
+
/**
|
|
1392
|
+
* One response record or tombstone. The candidate names a record by its
|
|
1393
|
+
* instance and key together, so it adds `instance_id` (the grant's handle,
|
|
1394
|
+
* which is the stored instance) and serves the minimal client tombstone. The
|
|
1395
|
+
* current contract's shape is unchanged.
|
|
1396
|
+
*/
|
|
1397
|
+
function toRecordJson(stream, row, fields, candidate = false) {
|
|
1398
|
+
const identity = {
|
|
1399
|
+
object: "record",
|
|
1400
|
+
id: row.recordKey,
|
|
1401
|
+
...(candidate && { instance_id: row.instance }),
|
|
1402
|
+
stream,
|
|
1403
|
+
};
|
|
1519
1404
|
if (row.deleted) {
|
|
1405
|
+
if (candidate)
|
|
1406
|
+
return { ...identity, deleted: true };
|
|
1520
1407
|
return {
|
|
1521
|
-
|
|
1522
|
-
id: row.recordKey,
|
|
1523
|
-
stream,
|
|
1408
|
+
...identity,
|
|
1524
1409
|
deleted: true,
|
|
1525
1410
|
deleted_at: row.deletedAt,
|
|
1526
1411
|
emitted_at: row.emittedAt,
|
|
1412
|
+
...(row.confirmation && { confirmation: row.confirmation }),
|
|
1413
|
+
...(row.status && { visibility: row.visibility, status: row.status }),
|
|
1527
1414
|
};
|
|
1528
1415
|
}
|
|
1529
1416
|
return {
|
|
1530
|
-
|
|
1531
|
-
id: row.recordKey,
|
|
1532
|
-
stream,
|
|
1417
|
+
...identity,
|
|
1533
1418
|
data: projectFields(row.data, fields),
|
|
1534
1419
|
emitted_at: row.emittedAt,
|
|
1420
|
+
...(row.confirmation && { confirmation: row.confirmation }),
|
|
1421
|
+
...(row.status && { visibility: row.visibility, status: row.status }),
|
|
1535
1422
|
};
|
|
1536
1423
|
}
|
|
1537
1424
|
//# sourceMappingURL=pdpp-records.js.map
|