@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.
Files changed (180) hide show
  1. package/dist/api-auth.d.ts +1 -0
  2. package/dist/api-auth.d.ts.map +1 -1
  3. package/dist/api-auth.js.map +1 -1
  4. package/dist/api.d.ts +3 -0
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +3 -0
  7. package/dist/api.js.map +1 -1
  8. package/dist/app.d.ts +14 -2
  9. package/dist/app.d.ts.map +1 -1
  10. package/dist/app.js +64 -8
  11. package/dist/app.js.map +1 -1
  12. package/dist/bootstrap.d.ts +19 -0
  13. package/dist/bootstrap.d.ts.map +1 -1
  14. package/dist/bootstrap.js +87 -4
  15. package/dist/bootstrap.js.map +1 -1
  16. package/dist/client.d.ts +1 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +7 -0
  19. package/dist/client.js.map +1 -1
  20. package/dist/enclave-main.d.ts.map +1 -1
  21. package/dist/enclave-main.js +0 -2
  22. package/dist/enclave-main.js.map +1 -1
  23. package/dist/mcp/durable-state.d.ts +2 -1
  24. package/dist/mcp/durable-state.d.ts.map +1 -1
  25. package/dist/mcp/durable-state.js +35 -1
  26. package/dist/mcp/durable-state.js.map +1 -1
  27. package/dist/pdpp/bootstrap.d.ts.map +1 -1
  28. package/dist/pdpp/bootstrap.js +1 -0
  29. package/dist/pdpp/bootstrap.js.map +1 -1
  30. package/dist/pdpp/records-bootstrap.d.ts +47 -1
  31. package/dist/pdpp/records-bootstrap.d.ts.map +1 -1
  32. package/dist/pdpp/records-bootstrap.js +78 -2
  33. package/dist/pdpp/records-bootstrap.js.map +1 -1
  34. package/dist/pdpp/replica-authorization.d.ts +35 -0
  35. package/dist/pdpp/replica-authorization.d.ts.map +1 -0
  36. package/dist/pdpp/replica-authorization.js +234 -0
  37. package/dist/pdpp/replica-authorization.js.map +1 -0
  38. package/dist/pending-blob-deletions.d.ts +1 -0
  39. package/dist/pending-blob-deletions.d.ts.map +1 -1
  40. package/dist/pending-blob-deletions.js +8 -4
  41. package/dist/pending-blob-deletions.js.map +1 -1
  42. package/dist/routes/mcp.d.ts +18 -2
  43. package/dist/routes/mcp.d.ts.map +1 -1
  44. package/dist/routes/mcp.js +111 -11
  45. package/dist/routes/mcp.js.map +1 -1
  46. package/dist/routes/oura.d.ts +6 -0
  47. package/dist/routes/oura.d.ts.map +1 -0
  48. package/dist/routes/oura.js +272 -0
  49. package/dist/routes/oura.js.map +1 -0
  50. package/dist/routes/pdpp-auth-query-keys.d.ts +7 -0
  51. package/dist/routes/pdpp-auth-query-keys.d.ts.map +1 -0
  52. package/dist/routes/pdpp-auth-query-keys.js +25 -0
  53. package/dist/routes/pdpp-auth-query-keys.js.map +1 -0
  54. package/dist/routes/pdpp-auth.d.ts +3 -1
  55. package/dist/routes/pdpp-auth.d.ts.map +1 -1
  56. package/dist/routes/pdpp-auth.js +126 -45
  57. package/dist/routes/pdpp-auth.js.map +1 -1
  58. package/dist/routes/pdpp-blobs.d.ts +13 -1
  59. package/dist/routes/pdpp-blobs.d.ts.map +1 -1
  60. package/dist/routes/pdpp-blobs.js +100 -65
  61. package/dist/routes/pdpp-blobs.js.map +1 -1
  62. package/dist/routes/pdpp-connections.d.ts.map +1 -1
  63. package/dist/routes/pdpp-connections.js +24 -1
  64. package/dist/routes/pdpp-connections.js.map +1 -1
  65. package/dist/routes/pdpp-instance-bindings.d.ts +3 -0
  66. package/dist/routes/pdpp-instance-bindings.d.ts.map +1 -1
  67. package/dist/routes/pdpp-instance-bindings.js +63 -0
  68. package/dist/routes/pdpp-instance-bindings.js.map +1 -1
  69. package/dist/routes/pdpp-record-query.d.ts +19 -0
  70. package/dist/routes/pdpp-record-query.d.ts.map +1 -0
  71. package/dist/routes/pdpp-record-query.js +51 -0
  72. package/dist/routes/pdpp-record-query.js.map +1 -0
  73. package/dist/routes/pdpp-records.d.ts +12 -1
  74. package/dist/routes/pdpp-records.d.ts.map +1 -1
  75. package/dist/routes/pdpp-records.js +120 -233
  76. package/dist/routes/pdpp-records.js.map +1 -1
  77. package/dist/routes/pdpp-replica-quarantine-fixture.d.ts +65 -0
  78. package/dist/routes/pdpp-replica-quarantine-fixture.d.ts.map +1 -0
  79. package/dist/routes/pdpp-replica-quarantine-fixture.js +389 -0
  80. package/dist/routes/pdpp-replica-quarantine-fixture.js.map +1 -0
  81. package/dist/storage/claude-method-transition.d.ts +12 -0
  82. package/dist/storage/claude-method-transition.d.ts.map +1 -0
  83. package/dist/storage/claude-method-transition.js +139 -0
  84. package/dist/storage/claude-method-transition.js.map +1 -0
  85. package/dist/storage/import-operations.d.ts +16 -0
  86. package/dist/storage/import-operations.d.ts.map +1 -1
  87. package/dist/storage/import-operations.js +54 -1
  88. package/dist/storage/import-operations.js.map +1 -1
  89. package/dist/storage/index-schema.d.ts +1 -1
  90. package/dist/storage/index-schema.d.ts.map +1 -1
  91. package/dist/storage/index-schema.js +22 -5
  92. package/dist/storage/index-schema.js.map +1 -1
  93. package/dist/storage/legacy-scope-authority.d.ts.map +1 -1
  94. package/dist/storage/legacy-scope-authority.js +83 -11
  95. package/dist/storage/legacy-scope-authority.js.map +1 -1
  96. package/dist/storage/node-data-storage.d.ts.map +1 -1
  97. package/dist/storage/node-data-storage.js +58 -2
  98. package/dist/storage/node-data-storage.js.map +1 -1
  99. package/dist/storage/pdpp-binary-chunks.d.ts +120 -0
  100. package/dist/storage/pdpp-binary-chunks.d.ts.map +1 -0
  101. package/dist/storage/pdpp-binary-chunks.js +480 -0
  102. package/dist/storage/pdpp-binary-chunks.js.map +1 -0
  103. package/dist/storage/pdpp-records-sqlite-store.d.ts +96 -15
  104. package/dist/storage/pdpp-records-sqlite-store.d.ts.map +1 -1
  105. package/dist/storage/pdpp-records-sqlite-store.js +616 -211
  106. package/dist/storage/pdpp-records-sqlite-store.js.map +1 -1
  107. package/dist/storage/pdpp-replica-canonical-port.d.ts +202 -0
  108. package/dist/storage/pdpp-replica-canonical-port.d.ts.map +1 -0
  109. package/dist/storage/pdpp-replica-canonical-port.js +451 -0
  110. package/dist/storage/pdpp-replica-canonical-port.js.map +1 -0
  111. package/dist/storage/pdpp-replica-coordinator-state.d.ts +16 -0
  112. package/dist/storage/pdpp-replica-coordinator-state.d.ts.map +1 -0
  113. package/dist/storage/pdpp-replica-coordinator-state.js +25 -0
  114. package/dist/storage/pdpp-replica-coordinator-state.js.map +1 -0
  115. package/dist/storage/pdpp-replica-copies.d.ts +81 -0
  116. package/dist/storage/pdpp-replica-copies.d.ts.map +1 -0
  117. package/dist/storage/pdpp-replica-copies.js +59 -0
  118. package/dist/storage/pdpp-replica-copies.js.map +1 -0
  119. package/dist/storage/pdpp-replica-reconciler.d.ts +96 -0
  120. package/dist/storage/pdpp-replica-reconciler.d.ts.map +1 -0
  121. package/dist/storage/pdpp-replica-reconciler.js +710 -0
  122. package/dist/storage/pdpp-replica-reconciler.js.map +1 -0
  123. package/dist/storage/pdpp-replica-sql.d.ts +13 -0
  124. package/dist/storage/pdpp-replica-sql.d.ts.map +1 -0
  125. package/dist/storage/pdpp-replica-sql.js +43 -0
  126. package/dist/storage/pdpp-replica-sql.js.map +1 -0
  127. package/dist/storage/pdpp-replica-staging.d.ts +36 -0
  128. package/dist/storage/pdpp-replica-staging.d.ts.map +1 -0
  129. package/dist/storage/pdpp-replica-staging.js +202 -0
  130. package/dist/storage/pdpp-replica-staging.js.map +1 -0
  131. package/dist/storage/pdpp-replica-visibility-view.d.ts +64 -0
  132. package/dist/storage/pdpp-replica-visibility-view.d.ts.map +1 -0
  133. package/dist/storage/pdpp-replica-visibility-view.js +1397 -0
  134. package/dist/storage/pdpp-replica-visibility-view.js.map +1 -0
  135. package/dist/storage/pdpp-replica-writer-identity.d.ts +64 -0
  136. package/dist/storage/pdpp-replica-writer-identity.d.ts.map +1 -0
  137. package/dist/storage/pdpp-replica-writer-identity.js +81 -0
  138. package/dist/storage/pdpp-replica-writer-identity.js.map +1 -0
  139. package/dist/storage/pdpp-replication-door.d.ts +60 -0
  140. package/dist/storage/pdpp-replication-door.d.ts.map +1 -0
  141. package/dist/storage/pdpp-replication-door.js +203 -0
  142. package/dist/storage/pdpp-replication-door.js.map +1 -0
  143. package/dist/storage/pdpp-replication-host.d.ts +67 -0
  144. package/dist/storage/pdpp-replication-host.d.ts.map +1 -0
  145. package/dist/storage/pdpp-replication-host.js +159 -0
  146. package/dist/storage/pdpp-replication-host.js.map +1 -0
  147. package/dist/storage/pdpp-replication-memory.fixture.d.ts +2 -0
  148. package/dist/storage/pdpp-replication-memory.fixture.d.ts.map +1 -0
  149. package/dist/storage/pdpp-replication-memory.fixture.js +267 -0
  150. package/dist/storage/pdpp-replication-memory.fixture.js.map +1 -0
  151. package/dist/storage/pdpp-replication-preparation.d.ts +3 -0
  152. package/dist/storage/pdpp-replication-preparation.d.ts.map +1 -0
  153. package/dist/storage/pdpp-replication-preparation.js +3 -0
  154. package/dist/storage/pdpp-replication-preparation.js.map +1 -0
  155. package/dist/storage/pdpp-replication-promotion-memory.fixture.d.ts +2 -0
  156. package/dist/storage/pdpp-replication-promotion-memory.fixture.d.ts.map +1 -0
  157. package/dist/storage/pdpp-replication-promotion-memory.fixture.js +65 -0
  158. package/dist/storage/pdpp-replication-promotion-memory.fixture.js.map +1 -0
  159. package/dist/storage/pdpp-replication-receiver.d.ts +230 -0
  160. package/dist/storage/pdpp-replication-receiver.d.ts.map +1 -0
  161. package/dist/storage/pdpp-replication-receiver.js +950 -0
  162. package/dist/storage/pdpp-replication-receiver.js.map +1 -0
  163. package/dist/storage/pdpp-replication-sqlite.d.ts +95 -0
  164. package/dist/storage/pdpp-replication-sqlite.d.ts.map +1 -0
  165. package/dist/storage/pdpp-replication-sqlite.js +615 -0
  166. package/dist/storage/pdpp-replication-sqlite.js.map +1 -0
  167. package/dist/storage/pdpp-replication-transport.d.ts +39 -0
  168. package/dist/storage/pdpp-replication-transport.d.ts.map +1 -0
  169. package/dist/storage/pdpp-replication-transport.js +196 -0
  170. package/dist/storage/pdpp-replication-transport.js.map +1 -0
  171. package/dist/storage/schema-versions.d.ts +5 -0
  172. package/dist/storage/schema-versions.d.ts.map +1 -0
  173. package/dist/storage/schema-versions.js +26 -0
  174. package/dist/storage/schema-versions.js.map +1 -0
  175. package/dist/storage/strava-method-transition.d.ts +18 -0
  176. package/dist/storage/strava-method-transition.d.ts.map +1 -0
  177. package/dist/storage/strava-method-transition.js +209 -0
  178. package/dist/storage/strava-method-transition.js.map +1 -0
  179. package/dist/ui/ps-lite-debug.js +137018 -106931
  180. 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 { CursorExpiredError, decodeCursor, encodeCursor, InvalidCursorError, InvalidCursorSyntaxError, recordKeyWithinGrantResources, mapInactiveToError, recordWithinGrantTimeConstraint, resolveReadScope, summarizeIngest, } from "@opendatalabs/personal-server-ts-core/storage/pdpp-records";
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 DEFAULT_LIMIT = 25;
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 = 512 * 1024 * 1024;
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
- const context = await deps.auth.resolveToken(token);
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
- rejectUnknownParams(c, "listStreams");
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 = deps.store.listStreams(instanceIds);
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
- ...summarizeGrantVisible(deps.store, s.stream, streamGrant?.instance_ids ?? [], streamGrant),
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
- rejectUnknownParams(c, "streamMetadata");
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
- rejectUnknownParams(c, "listRecords", context.tokenKind);
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 } = parseLimit(c.req.query("limit"));
688
- const order = parseOrder(c.req.query("order"));
689
- const requestedFields = c.req
690
- .query("fields")
691
- ?.split(",")
692
- .map((f) => f.trim());
693
- // §8: `fields` is a sparse fieldset on the durable client surface, and
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 = deps.store.changesSince(stream, {
627
+ const page = reads(context).changesSince(stream, {
719
628
  instanceIds: effectiveInstanceIds,
720
- changesSince: c.req.query("changes_since"),
721
- cursor: c.req.query("cursor"),
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
- // Same ordering requirement as the changes_since branch above: fetch
751
- // unprojected, filter by time_constraint against the real value, then
752
- // project fields only in toRecordJson.
753
- // Grant filtering happens after the store pages, so a page of `limit`
754
- // stored rows can yield fewer than `limit` visible rows. Returning that
755
- // short page directly would make `limit` mean "at most N stored rows"
756
- // rather than "at most N records you may see", and would report
757
- // has_more/next_cursor for a position in the unfiltered sequence.
758
- // Accumulate visible rows across store pages instead, then derive
759
- // paging state from the filtered view.
760
- const visible = [];
761
- let pageCursor = cursor;
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: moreVisible,
806
- ...(moreVisible &&
807
- last &&
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
- rejectUnknownParams(c, "getRecord", context.tokenKind);
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 = deps.store.getRecord(instance, stream, recordKey);
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
- !recordWithinGrantTimeConstraint(found.data, scope.streamGrant)) {
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
- // `fields` is a declared parameter of this endpoint, and `v0.2-4-2`
861
- // builds disclosed `data` from the grant AND the request-time
862
- // selection. It was parsed nowhere, so `?fields=date` returned every
863
- // granted field with a 200 — the response did something other than what
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
- rejectUnknownParams(c, "deleteRecord");
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
- // `listRecords` already excludes tombstones, so a deleted row should not
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: MAX_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: MAX_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
- function toRecordJson(stream, row, fields) {
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
- object: "record",
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
- object: "record",
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