@substrat-run/control-plane-api 0.176.0 → 0.178.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 (82) hide show
  1. package/dist/api.d.ts +12 -0
  2. package/dist/api.d.ts.map +1 -1
  3. package/dist/api.js +612 -210
  4. package/dist/api.js.map +1 -1
  5. package/dist/backups.d.ts +80 -16
  6. package/dist/backups.d.ts.map +1 -1
  7. package/dist/backups.js +3 -0
  8. package/dist/backups.js.map +1 -1
  9. package/dist/browser.d.ts +1 -1
  10. package/dist/browser.d.ts.map +1 -1
  11. package/dist/browser.js.map +1 -1
  12. package/dist/builder-turns.d.ts +16 -0
  13. package/dist/builder-turns.d.ts.map +1 -0
  14. package/dist/builder-turns.js +43 -0
  15. package/dist/builder-turns.js.map +1 -0
  16. package/dist/connection-binding-relay.d.ts +28 -1
  17. package/dist/connection-binding-relay.d.ts.map +1 -1
  18. package/dist/connection-binding-relay.js +87 -2
  19. package/dist/connection-binding-relay.js.map +1 -1
  20. package/dist/connection-relay.d.ts +2 -2
  21. package/dist/connection-relay.d.ts.map +1 -1
  22. package/dist/connection-relay.js.map +1 -1
  23. package/dist/directory-backup.d.ts +4 -0
  24. package/dist/directory-backup.d.ts.map +1 -1
  25. package/dist/directory-backup.js +60 -7
  26. package/dist/directory-backup.js.map +1 -1
  27. package/dist/index.d.ts +4 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/lake-gateway.d.ts +9 -0
  32. package/dist/lake-gateway.d.ts.map +1 -1
  33. package/dist/lake-gateway.js +18 -1
  34. package/dist/lake-gateway.js.map +1 -1
  35. package/dist/migration-extract.d.ts +92 -0
  36. package/dist/migration-extract.d.ts.map +1 -0
  37. package/dist/migration-extract.js +217 -0
  38. package/dist/migration-extract.js.map +1 -0
  39. package/dist/platform-entry.d.ts +2 -0
  40. package/dist/platform-entry.d.ts.map +1 -1
  41. package/dist/platform-entry.generated.d.ts +1 -1
  42. package/dist/platform-entry.generated.d.ts.map +1 -1
  43. package/dist/platform-entry.generated.js +2 -2
  44. package/dist/platform-entry.generated.js.map +1 -1
  45. package/dist/platform-entry.js +2 -1
  46. package/dist/platform-entry.js.map +1 -1
  47. package/dist/platform-scope-stores.d.ts +10 -12
  48. package/dist/platform-scope-stores.d.ts.map +1 -1
  49. package/dist/platform-scope-stores.js +47 -24
  50. package/dist/platform-scope-stores.js.map +1 -1
  51. package/dist/r2-backups.d.ts +8 -5
  52. package/dist/r2-backups.d.ts.map +1 -1
  53. package/dist/r2-backups.js +232 -56
  54. package/dist/r2-backups.js.map +1 -1
  55. package/dist/seal.d.ts +5 -2
  56. package/dist/seal.d.ts.map +1 -1
  57. package/dist/seal.js +3 -0
  58. package/dist/seal.js.map +1 -1
  59. package/dist/staff-client.d.ts +10 -1
  60. package/dist/staff-client.d.ts.map +1 -1
  61. package/dist/staff-client.js +4 -2
  62. package/dist/staff-client.js.map +1 -1
  63. package/dist/store-entry.d.ts.map +1 -1
  64. package/dist/store-entry.generated.d.ts +2 -7
  65. package/dist/store-entry.generated.d.ts.map +1 -1
  66. package/dist/store-entry.generated.js +5 -10
  67. package/dist/store-entry.generated.js.map +1 -1
  68. package/dist/store-entry.js +3 -2
  69. package/dist/store-entry.js.map +1 -1
  70. package/dist/store-releases.d.ts +18 -4
  71. package/dist/store-releases.d.ts.map +1 -1
  72. package/dist/store-releases.js +74 -11
  73. package/dist/store-releases.js.map +1 -1
  74. package/dist/store-script.d.ts +12 -2
  75. package/dist/store-script.d.ts.map +1 -1
  76. package/dist/store-script.js +47 -9
  77. package/dist/store-script.js.map +1 -1
  78. package/dist/vertical-client.d.ts +6 -0
  79. package/dist/vertical-client.d.ts.map +1 -1
  80. package/dist/vertical-client.js +16 -0
  81. package/dist/vertical-client.js.map +1 -1
  82. package/package.json +9 -9
@@ -1,5 +1,5 @@
1
- import { LIST_PAGE_MAX, mayHoldScopeStores, scopeStoresDeclaredIn, STORE_DRIVERS, issueScopeStoreEnvelope, SCOPE_STORE_ENVELOPE_HEADER, SCOPE_STORE_SIGNATURE_HEADER, ScopeStoreKeyError, scopeStoreObjectName, scopeStoreScriptName, storeEraseResult, storeExportResult, storeSizeResult, StoreError, } from '@substrat-run/contracts';
2
- import { callReservedStoreOperation } from '@substrat-run/kernel';
1
+ import { LIST_PAGE_MAX, mayHoldScopeStores, scopeStoresDeclaredIn, STORE_DRIVERS, issueScopeStoreEnvelope, SCOPE_STORE_ENVELOPE_HEADER, SCOPE_STORE_SIGNATURE_HEADER, ScopeStoreKeyError, STORE_CALL_MAX_BYTES, scopeStoreBodyBytes, scopeStoreObjectName, scopeStoreScriptName, storeEraseResult, storeDriverDumpPageBytes, storeSizeResult, StoreError, } from '@substrat-run/contracts';
2
+ import { callReservedStoreOperation, readStoreDump, writeStoreDump } from '@substrat-run/kernel';
3
3
  import { ControlPlaneError } from '@substrat-run/control-plane-client';
4
4
  import { storedDeployManifest } from './deploy.js';
5
5
  /** Bindings a version manifest declares. Missing JSON is none. Unreadable JSON fails closed. */
@@ -65,34 +65,48 @@ export async function unrouteScope(admin, actor, scope) {
65
65
  * ordinary install whose store the router serves. A pin that names another script than the
66
66
  * pointer's is unknown, as #2242's erasure gate has it.
67
67
  *
68
- * The gate is `mayHoldScopeStores`: with no driver registered nothing can hold a store, and the
69
- * answer is none. Otherwise a version that cannot be named, a manifest that cannot be read, or one
70
- * the gate says may hold a store without naming it refuses (503), rather than let a backup, a
71
- * wipe, a replay or an erasure skip a store that may hold data — where a store can exist (#2394).
72
- * One can exist only under a binding some version of the scope's vertical declared: the router
73
- * reaches a store only through a version row's manifest (`READ_ROUTE_SQL`), and `publishVersion`
74
- * records that row's bindings in the same transaction (`HostAdmin.verticalScopeStores`). So when
75
- * that record says the vertical has declared none, the answer is none; when it cannot be read, the
76
- * refusal stands. A bind or promote that lands after the read is the erasure fence's to refuse,
77
- * as it is for a version that declares none.
68
+ * The vertical's record of declared bindings is read first (`declaredRecordOf`): with no driver
69
+ * registered, or a record that says the vertical has declared none, the answer is none; a record
70
+ * that cannot be read refuses. Only then is the running version read, and a version that cannot be
71
+ * named, a manifest that cannot be read, or one the gate (`mayHoldScopeStores`) says may hold a
72
+ * store without naming it refuses (503), rather than let a backup, a wipe, a replay or an erasure
73
+ * skip a store that may hold data (#2394). A bind or promote that lands after the read is the
74
+ * erasure fence's to refuse, as it is for a version that declares none.
78
75
  *
79
76
  * `running` is a resolution the caller already holds (`runningVersionOf`), so a caller that fences
80
77
  * on the version reads it once and judges the stores against the same read.
81
78
  */
82
79
  export async function runningVersionStoresOf(admin, actor, scope, drivers = STORE_DRIVERS, running) {
80
+ const record = await declaredRecordOf(admin, actor, scope, drivers);
81
+ return record.length === 0 ? [] : runningStoresOfDeclaring(admin, actor, scope, drivers, running);
82
+ }
83
+ /**
84
+ * The vertical's record of every binding any version declared, read first and once: a vertical
85
+ * that never declared a store answers none with this one read, which is what the sweep pays for
86
+ * almost every scope it meters or reaps. Sound because a store exists only under a binding some
87
+ * version declared: the router reaches a store only through a version row's manifest
88
+ * (`READ_ROUTE_SQL`), every insert of a version row records its bindings in the same transaction,
89
+ * and the read itself first indexes any stored version not yet indexed, so the record covers every
90
+ * version the vertical holds (#2292). A record that cannot be read refuses (503). None, with no
91
+ * read, where nothing can hold a store: no vertical, or no driver registered.
92
+ */
93
+ async function declaredRecordOf(admin, actor, scope, drivers) {
83
94
  if (!scope.vertical || !mayHoldScopeStores(undefined, drivers))
84
95
  return [];
96
+ return storesDeclaredBy(admin, actor, scope.vertical);
97
+ }
98
+ /** The running version's stores, for a scope whose vertical has declared at least one (the record says so). */
99
+ async function runningStoresOfDeclaring(admin, actor, scope, drivers, running) {
100
+ const vertical = scope.vertical;
85
101
  const { versionId } = running ?? await runningVersionOf(admin, actor, scope);
86
102
  const manifestJson = versionId
87
- ? await admin.versionManifest(actor, scope.vertical, versionId).catch(() => undefined)
103
+ ? await admin.versionManifest(actor, vertical, versionId).catch(() => undefined)
88
104
  : undefined;
89
105
  if (!mayHoldScopeStores(manifestJson, drivers))
90
106
  return [];
91
107
  const declared = typeof manifestJson === 'string' && scopeStoresDeclaredIn(manifestJson) ? declaredStoresOf(manifestJson) : [];
92
108
  if (declared.length > 0)
93
109
  return declared;
94
- if ((await storesDeclaredBy(admin, actor, scope.vertical)).length === 0)
95
- return [];
96
110
  throw new ControlPlaneError(503, `scope ${scope.id} may hold scope stores, and the version it runs cannot say which`);
97
111
  }
98
112
  /** Every binding any version of `vertical` declared (#2286); a record that cannot be read refuses (503). */
@@ -109,18 +123,19 @@ async function storesDeclaredBy(admin, actor, vertical) {
109
123
  * (`runningVersionStoresOf`, which still fails closed first), and every binding any version of its
110
124
  * vertical has declared (`HostAdmin.verticalScopeStores`, read by its keys). Export, wipe, erasure
111
125
  * and the meter reach `all`, so an object under a binding a later version dropped is still
112
- * reached. A restore imports only into `running` (L8).
126
+ * reached. A restore imports only into `running` (L8). The record is read once, first, and judges
127
+ * both.
113
128
  *
114
129
  * A binding keeps its driver for the life of the vertical, since publish refuses a driver change; a
115
130
  * running version whose driver disagrees with the record, or a record that cannot be read,
116
131
  * refuses (503).
117
132
  */
118
133
  export async function scopeStoreReachOf(admin, actor, scope, drivers = STORE_DRIVERS, running) {
119
- const current = await runningVersionStoresOf(admin, actor, scope, drivers, running);
120
- if (!scope.vertical || !mayHoldScopeStores(undefined, drivers))
121
- return { running: current, all: current };
122
- const declared = await storesDeclaredBy(admin, actor, scope.vertical);
123
- const all = new Map(declared.map((store) => [store.binding, store.driver]));
134
+ const record = await declaredRecordOf(admin, actor, scope, drivers);
135
+ if (record.length === 0)
136
+ return { running: [], all: [] };
137
+ const current = await runningStoresOfDeclaring(admin, actor, scope, drivers, running);
138
+ const all = new Map(record.map((store) => [store.binding, store.driver]));
124
139
  for (const store of current) {
125
140
  const driver = all.get(store.binding);
126
141
  if (driver !== undefined && driver !== store.driver) {
@@ -159,6 +174,11 @@ export async function callPlatformScopeStoreAt(dispatch, target, operation, inpu
159
174
  try {
160
175
  return await callReservedStoreOperation(async (request) => {
161
176
  const body = JSON.stringify({ ...request, objectName });
177
+ // The store entry refuses a body over the limit (#2811), so it is refused here, before it is signed.
178
+ const bytes = scopeStoreBodyBytes(body);
179
+ if (bytes > STORE_CALL_MAX_BYTES) {
180
+ throw new ControlPlaneError(413, `scope store call '${request.operation}' carries a body of ${bytes} bytes, over the limit of ${STORE_CALL_MAX_BYTES}. Nothing was sent.`);
181
+ }
162
182
  let issued;
163
183
  try {
164
184
  issued = await issueScopeStoreEnvelope({
@@ -198,21 +218,24 @@ export async function callPlatformScopeStoreAt(dispatch, target, operation, inpu
198
218
  asControlPlane(error);
199
219
  }
200
220
  }
201
- /** The five reserved operations, one call per declared binding. */
221
+ /** The reserved operations a platform call makes, a dump in pages (D-69), one chain per declared binding. */
202
222
  export function platformScopeStoresFor(dispatch) {
203
223
  const call = (target, operation, input) => callPlatformScopeStore(dispatch, target, operation, input);
224
+ const reservedAt = (target) => ((operation, input) => call(target, operation, input));
204
225
  return {
205
226
  async exportStores({ tenantId, scopeId, vertical, stores }) {
206
227
  const exported = [];
207
228
  for (const store of stores) {
208
- const { tables } = storeExportResult.parse(await call({ ...store, tenantId, scopeId, vertical }, 'substrat.export'));
229
+ const target = { ...store, tenantId, scopeId, vertical };
230
+ const tables = await readStoreDump(reservedAt(target), storeDriverDumpPageBytes(store.driver));
209
231
  exported.push({ binding: store.binding, driver: store.driver, tables });
210
232
  }
211
233
  return exported;
212
234
  },
213
235
  async importStores({ tenantId, scopeId, vertical, stores }) {
214
236
  for (const store of stores) {
215
- await call({ ...store, tenantId, scopeId, vertical }, 'substrat.import', { tables: store.tables });
237
+ const target = { ...store, tenantId, scopeId, vertical };
238
+ await writeStoreDump(reservedAt(target), store.tables, storeDriverDumpPageBytes(store.driver));
216
239
  }
217
240
  },
218
241
  async eraseSubject({ tenantId, scopeId, vertical, subjectId, stores }) {
@@ -1 +1 @@
1
- {"version":3,"file":"platform-scope-stores.js","sourceRoot":"","sources":["../src/platform-scope-stores.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"platform-scope-stores.js","sourceRoot":"","sources":["../src/platform-scope-stores.ts"],"names":[],"mappings":"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"}
@@ -20,7 +20,7 @@ export declare function createR2DirectoryBackupStore(bucket: unknown): Directory
20
20
  * *"Iceberg is the contract, the query engine is replaceable"*, and the same
21
21
  * paragraph leaves R2 SQL's fitness explicitly open pending a benchmark. Writing
22
22
  * partitioned NDJSON to a bucket the platform already operates gets the events out
23
- * of the scope — which is what bounds an outbox that is never pruned — without
23
+ * of the scope — which is what licenses the retention prune that bounds the outbox — without
24
24
  * committing the ingest path to a product decision nobody has made yet. A
25
25
  * compaction step turns these objects into Iceberg later, and the seam means the
26
26
  * drain never learns which happened.
@@ -72,10 +72,13 @@ interface PruneOptions {
72
72
  }
73
73
  /**
74
74
  * Drop `scopes/…` reap copies (#493) older than the window. A copy's age is its
75
- * `capturedAt` — the key's last segment, same as the store's own fallback address — so a
76
- * hand-uploaded object without metadata still ages out rather than living forever by
77
- * omission. An unparsable timestamp keeps the copy: deleting what cannot be dated is not
78
- * retention.
75
+ * `capturedAt`: the object's metadata, else the key's segment after the scope, which is the
76
+ * manifest's name and the directory a paged copy's entries sit in (#2839). So a hand-uploaded
77
+ * object without metadata still ages out rather than living forever by omission. An
78
+ * unparsable timestamp keeps the copy: deleting what cannot be dated is not retention.
79
+ *
80
+ * A copy's manifest sorts before its entries (`.` before `/`), so it goes first: a pass cut
81
+ * short leaves entries no backup names, for the next pass, never a backup missing its entries.
79
82
  *
80
83
  * Note the interaction with #37 (erasure): per-subject payloads inside these dumps are
81
84
  * sealed under per-subject DEKs, so an old copy's PII exposure is bounded by key
@@ -1 +1 @@
1
- {"version":3,"file":"r2-backups.d.ts","sourceRoot":"","sources":["../src/r2-backups.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAsC3E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAsErE;AAcD;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,OAAO,GAAG,oBAAoB,CAgElF;AAyCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CA6C5D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,aAAa,CAgCpE;AAED;;;;;;;;;;;;;;;GAeG;AACH,UAAU,YAAY;IACpB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAuBD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAa/F;AAiBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
1
+ {"version":3,"file":"r2-backups.d.ts","sourceRoot":"","sources":["../src/r2-backups.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,KAAK,EAAE,oBAAoB,EAAoB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAoE7F,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAqHrE;AAoED;;;;;;;;GAQG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,OAAO,GAAG,oBAAoB,CAsHlF;AAyCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,CA6C5D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,aAAa,CAgCpE;AAED;;;;;;;;;;;;;;;GAeG;AACH,UAAU,YAAY;IACpB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAuBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAa/F;AAiBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,OAAO,EACf,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
@@ -9,55 +9,101 @@
9
9
  * One bucket, held by the PLATFORM, not by the tenant. A tenant-held bucket would be
10
10
  * deleted by the very teardown these copies exist to survive.
11
11
  */
12
- import { directoryDump, scopeDump, } from '@substrat-run/contracts';
12
+ import { directoryDump, mergeDumpPages, scopeDump, scopeDumpChange, scopeDumpPageTable, scopeDumpTable, z, } from '@substrat-run/contracts';
13
13
  /**
14
- * Keys are `scopes/<tenantId>/<scopeId>/<capturedAt>.json`, so a scope's copies are one
14
+ * A backup is `scopes/<tenantId>/<scopeId>/<capturedAt>.json`, so a scope's copies are one
15
15
  * prefix list and a tenant's are one level up. The scheme is private to this module —
16
16
  * callers address a backup by (tenant, scope, capturedAt) and never build a path.
17
+ *
18
+ * That object is the backup's MANIFEST (#2839): the dump's header and how many entries hold its
19
+ * tables, each entry its own object at `scopes/<tenantId>/<scopeId>/<capturedAt>/<stageId>/<seq>.json`.
20
+ * One object per page rather than one multipart object, because a restore reads it back a page at
21
+ * a time: a single JSON document cannot be read in parts. The manifest is written last, so a
22
+ * backup that did not finish has none and is never listed or read.
23
+ *
24
+ * A backup written before #2839 is one whole dump at the manifest's key. It is read as a backup
25
+ * of one entry, which is its tables.
17
26
  */
18
27
  function backupKey(tenantId, scopeId, capturedAt) {
19
28
  return `scopes/${tenantId}/${scopeId}/${capturedAt}.json`;
20
29
  }
30
+ function entryKey(tenantId, scopeId, capturedAt, stageId, seq) {
31
+ return `scopes/${tenantId}/${scopeId}/${capturedAt}/${stageId}/${seq}.json`;
32
+ }
21
33
  function scopePrefix(tenantId, scopeId) {
22
34
  return `scopes/${tenantId}/${scopeId}/`;
23
35
  }
36
+ /** A backup's manifest: the dump without its tables, and where its entries are. */
37
+ const backupManifest = scopeDump.omit({ tables: true }).extend({
38
+ format: z.literal('paged'),
39
+ stageId: z.string().min(1),
40
+ entries: z.number().int().positive(),
41
+ tables: z.number().int().nonnegative(),
42
+ });
43
+ const backupEntry = z.discriminatedUnion('kind', [
44
+ z.object({ kind: z.literal('page'), tables: z.array(scopeDumpPageTable) }),
45
+ z.object({ kind: z.literal('changes'), tables: z.array(scopeDumpChange) }),
46
+ ]);
47
+ /** Byte length, not string length: a size is the stored object's, and non-ASCII would under-report. */
48
+ const byteLength = (body) => new TextEncoder().encode(body).length;
24
49
  export function createR2BackupStore(bucket) {
25
50
  const r2 = bucket;
26
51
  return {
27
- async put({ vertical, dump }) {
28
- const body = JSON.stringify(dump);
29
- // Byte length, not string length: the size is meant to be the stored object's,
30
- // and a dump full of non-ASCII would otherwise under-report.
31
- const size = new TextEncoder().encode(body).length;
32
- const backup = {
33
- tenantId: dump.tenantId,
34
- scopeId: dump.scopeId,
35
- vertical,
36
- capturedAt: dump.capturedAt,
37
- size,
38
- tables: dump.tables.length,
39
- };
40
- await r2.put(backupKey(dump.tenantId, dump.scopeId, dump.capturedAt), body, {
41
- httpMetadata: { contentType: 'application/json' },
42
- // Mirrored into custom metadata so `list` can answer without fetching every
43
- // dump — a scope's backups are megabytes, its listing is a table row.
44
- customMetadata: {
45
- tenantId: backup.tenantId,
46
- scopeId: backup.scopeId,
47
- capturedAt: backup.capturedAt,
48
- tables: String(backup.tables),
49
- ...(vertical ? { vertical } : {}),
52
+ begin({ vertical, header }) {
53
+ const { tenantId, scopeId, capturedAt } = header;
54
+ const written = new Map();
55
+ return {
56
+ async put(stageId, seq, entry) {
57
+ const body = JSON.stringify(entry);
58
+ await r2.put(entryKey(tenantId, scopeId, capturedAt, stageId, seq), body, {
59
+ httpMetadata: { contentType: 'application/json' },
60
+ // Retention dates every object of a backup by this, entries included.
61
+ customMetadata: { capturedAt },
62
+ });
63
+ const sizes = written.get(stageId) ?? [];
64
+ sizes[seq] = byteLength(body);
65
+ written.set(stageId, sizes);
50
66
  },
51
- });
52
- return backup;
67
+ async discard(stageId) {
68
+ const sizes = written.get(stageId) ?? [];
69
+ written.delete(stageId);
70
+ await Promise.all(sizes.map((_, seq) => r2.delete(entryKey(tenantId, scopeId, capturedAt, stageId, seq)).catch(() => undefined)));
71
+ },
72
+ async commit({ stageId, entries, tables }) {
73
+ const sizes = written.get(stageId) ?? [];
74
+ for (let seq = 0; seq < entries; seq++) {
75
+ if (sizes[seq] === undefined)
76
+ throw new Error(`backup entry ${seq} of ${entries} was never written`);
77
+ }
78
+ const body = JSON.stringify({ ...header, format: 'paged', stageId, entries, tables });
79
+ const size = byteLength(body) + sizes.slice(0, entries).reduce((a, b) => a + b, 0);
80
+ const backup = { tenantId, scopeId, vertical, capturedAt, size, tables };
81
+ await r2.put(backupKey(tenantId, scopeId, capturedAt), body, {
82
+ httpMetadata: { contentType: 'application/json' },
83
+ // Mirrored into custom metadata so `list` can answer without fetching every
84
+ // manifest. `size` too: the manifest is the smallest part of the copy.
85
+ customMetadata: {
86
+ tenantId,
87
+ scopeId,
88
+ capturedAt,
89
+ tables: String(tables),
90
+ size: String(size),
91
+ ...(vertical ? { vertical } : {}),
92
+ },
93
+ });
94
+ return backup;
95
+ },
96
+ };
53
97
  },
54
98
  async list({ tenantId, scopeId }) {
55
99
  const prefix = scopePrefix(tenantId, scopeId);
56
100
  const out = [];
57
101
  let cursor;
58
102
  do {
103
+ // One level: a backup's entries sit under a directory of their own, and are not backups.
59
104
  const page = await r2.list({
60
105
  prefix,
106
+ delimiter: '/',
61
107
  include: ['customMetadata'],
62
108
  ...(cursor ? { cursor } : {}),
63
109
  });
@@ -71,7 +117,7 @@ export function createR2BackupStore(bucket) {
71
117
  scopeId,
72
118
  vertical: meta.vertical ?? null,
73
119
  capturedAt,
74
- size: o.size,
120
+ size: meta.size ? Number(meta.size) : o.size,
75
121
  tables: meta.tables ? Number(meta.tables) : 0,
76
122
  });
77
123
  }
@@ -86,8 +132,32 @@ export function createR2BackupStore(bucket) {
86
132
  if (!obj)
87
133
  return null;
88
134
  // Parsed through the contract on the way out: a corrupted or hand-edited object
89
- // fails HERE, loudly, rather than at the restore that trusted it.
90
- return scopeDump.parse(JSON.parse(await obj.text()));
135
+ // fails HERE, loudly, rather than at the restore that trusted it. A manifest says so,
136
+ // so a damaged one is refused as a manifest rather than as a whole dump.
137
+ const raw = JSON.parse(await obj.text());
138
+ if (raw?.format !== 'paged') {
139
+ const { tables, ...header } = scopeDump.parse(raw);
140
+ return {
141
+ header,
142
+ entries: 1,
143
+ entry: async (index) => {
144
+ if (index !== 0)
145
+ throw new Error(`backup ${capturedAt} of scope ${scopeId} has one entry`);
146
+ return { kind: 'page', tables };
147
+ },
148
+ };
149
+ }
150
+ const { format: _, stageId, entries, tables: __, ...header } = backupManifest.parse(raw);
151
+ return {
152
+ header,
153
+ entries,
154
+ entry: async (index) => {
155
+ const part = await r2.get(entryKey(tenantId, scopeId, capturedAt, stageId, index));
156
+ if (!part)
157
+ throw new Error(`backup ${capturedAt} of scope ${scopeId} is missing entry ${index}`);
158
+ return backupEntry.parse(JSON.parse(await part.text()));
159
+ },
160
+ };
91
161
  },
92
162
  };
93
163
  }
@@ -96,11 +166,61 @@ export function createR2BackupStore(bucket) {
96
166
  * directory per deployment and `capturedAt` is a copy's whole address. Sharing a bucket
97
167
  * with the scope backups above is safe by construction: `scopes/` and `directory/` cannot
98
168
  * collide, and neither prefix is ever built by a caller.
169
+ *
170
+ * That object is the copy's MANIFEST (#2868), as a scope backup's is (#2839): its capture time
171
+ * and how many pages hold its tables, each page its own object at
172
+ * `directory/<capturedAt>/<stageId>/<seq>.json`. The manifest is written last, so a copy that did
173
+ * not finish has none and is never listed or read. A copy written before #2868 is one whole dump
174
+ * at the manifest's key, and still reads.
99
175
  */
100
176
  function directoryKey(capturedAt) {
101
177
  return `directory/${capturedAt}.json`;
102
178
  }
179
+ function directoryPageKey(capturedAt, stageId, seq) {
180
+ return `directory/${capturedAt}/${stageId}/${seq}.json`;
181
+ }
103
182
  const DIRECTORY_PREFIX = 'directory/';
183
+ /**
184
+ * How old a copy's pages must be, with no manifest, before a commit sweeps them (#2868). A copy cut
185
+ * off mid-write (its invocation's limits, an eviction) never reaches `discard`, and a pageful
186
+ * of the directory each time would otherwise stay forever. A day, so a copy still being written
187
+ * beside this one, the manual "back up now" racing the cron, is never one of them.
188
+ */
189
+ const ABANDONED_PAGES_MS = 24 * 3_600_000;
190
+ /** Outside `directory/`, so it is never listed as a copy: the newest started copy (#2868). */
191
+ const DIRECTORY_ATTEMPT_KEY = 'directory-attempt.json';
192
+ const directoryManifest = z.object({
193
+ capturedAt: z.string().min(1),
194
+ format: z.literal('paged'),
195
+ stageId: z.string().min(1),
196
+ entries: z.number().int().positive(),
197
+ tables: z.number().int().nonnegative(),
198
+ });
199
+ const directoryPage = z.object({ tables: z.array(scopeDumpTable) });
200
+ /**
201
+ * Delete the pages of every copy captured before `before` that has no manifest: copies cut off
202
+ * before their commit (#2868). Read with no delimiter, so every key under `directory/` is seen:
203
+ * thirty copies of a few dozen pages each is a list call or two, once a day.
204
+ */
205
+ async function sweepAbandonedPages(r2, before) {
206
+ const manifests = new Set();
207
+ const pages = [];
208
+ let cursor;
209
+ do {
210
+ const page = await r2.list({ prefix: DIRECTORY_PREFIX, ...(cursor ? { cursor } : {}) });
211
+ for (const { key } of page.objects) {
212
+ const rest = key.slice(DIRECTORY_PREFIX.length);
213
+ const slash = rest.indexOf('/');
214
+ if (slash < 0)
215
+ manifests.add(rest.replace(/\.json$/, ''));
216
+ else
217
+ pages.push({ key, capturedAt: rest.slice(0, slash) });
218
+ }
219
+ cursor = page.truncated ? page.cursor : undefined;
220
+ } while (cursor);
221
+ const abandoned = pages.filter((p) => p.capturedAt < before && !manifests.has(p.capturedAt));
222
+ await Promise.all(abandoned.map((p) => r2.delete(p.key).catch(() => undefined)));
223
+ }
104
224
  /**
105
225
  * The Cloudflare R2 implementation of `DirectoryBackupStore` (#40).
106
226
  *
@@ -113,32 +233,53 @@ const DIRECTORY_PREFIX = 'directory/';
113
233
  export function createR2DirectoryBackupStore(bucket) {
114
234
  const r2 = bucket;
115
235
  return {
116
- async put({ dump }) {
117
- const body = JSON.stringify(dump);
118
- // Byte length, not string length — the size is the stored object's.
119
- const size = new TextEncoder().encode(body).length;
120
- const backup = {
121
- capturedAt: dump.capturedAt,
122
- size,
123
- tables: dump.tables.length,
124
- };
125
- await r2.put(directoryKey(dump.capturedAt), body, {
126
- httpMetadata: { contentType: 'application/json' },
127
- // Mirrored into custom metadata so `list` answers without fetching every dump —
128
- // a directory copy is megabytes, its listing is a table row.
129
- customMetadata: {
130
- capturedAt: backup.capturedAt,
131
- tables: String(backup.tables),
236
+ begin({ capturedAt }) {
237
+ const stageId = crypto.randomUUID();
238
+ const sizes = [];
239
+ return {
240
+ async put(seq, tables) {
241
+ const body = JSON.stringify({ tables });
242
+ await r2.put(directoryPageKey(capturedAt, stageId, seq), body, {
243
+ httpMetadata: { contentType: 'application/json' },
244
+ });
245
+ sizes[seq] = byteLength(body);
132
246
  },
133
- });
134
- return backup;
247
+ async discard() {
248
+ await Promise.all(sizes.map((_, seq) => r2.delete(directoryPageKey(capturedAt, stageId, seq)).catch(() => undefined)));
249
+ },
250
+ async commit({ entries, tables }) {
251
+ for (let seq = 0; seq < entries; seq++) {
252
+ if (sizes[seq] === undefined)
253
+ throw new Error(`directory backup page ${seq} of ${entries} was never written`);
254
+ }
255
+ const body = JSON.stringify({ capturedAt, format: 'paged', stageId, entries, tables });
256
+ const size = byteLength(body) + sizes.slice(0, entries).reduce((a, b) => a + b, 0);
257
+ await r2.put(directoryKey(capturedAt), body, {
258
+ httpMetadata: { contentType: 'application/json' },
259
+ // Mirrored into custom metadata so `list` answers without fetching every manifest.
260
+ // `size` too: the manifest is the smallest part of the copy.
261
+ customMetadata: { capturedAt, tables: String(tables), size: String(size) },
262
+ });
263
+ // Best-effort, and after the manifest: the copy is committed now, and a failed sweep
264
+ // must not reject the commit, or the caller's `discard` would delete a listed copy's pages.
265
+ try {
266
+ await sweepAbandonedPages(r2, new Date(Date.parse(capturedAt) - ABANDONED_PAGES_MS).toISOString());
267
+ }
268
+ catch (err) {
269
+ console.error('directory-backup: sweeping abandoned pages failed; the next commit tries again', err instanceof Error ? err.message : String(err));
270
+ }
271
+ return { capturedAt, size, tables };
272
+ },
273
+ };
135
274
  },
136
275
  async list() {
137
276
  const out = [];
138
277
  let cursor;
139
278
  do {
279
+ // One level: a copy's pages sit under a directory of their own, and are not copies.
140
280
  const page = await r2.list({
141
281
  prefix: DIRECTORY_PREFIX,
282
+ delimiter: '/',
142
283
  include: ['customMetadata'],
143
284
  ...(cursor ? { cursor } : {}),
144
285
  });
@@ -149,7 +290,7 @@ export function createR2DirectoryBackupStore(bucket) {
149
290
  const capturedAt = meta.capturedAt ?? o.key.slice(DIRECTORY_PREFIX.length).replace(/\.json$/, '');
150
291
  out.push({
151
292
  capturedAt,
152
- size: o.size,
293
+ size: meta.size ? Number(meta.size) : o.size,
153
294
  tables: meta.tables ? Number(meta.tables) : 0,
154
295
  });
155
296
  }
@@ -165,10 +306,42 @@ export function createR2DirectoryBackupStore(bucket) {
165
306
  return null;
166
307
  // Parsed through the contract on the way out, so a corrupted object fails HERE
167
308
  // rather than half-way through the restore that trusted it.
168
- return directoryDump.parse(JSON.parse(await obj.text()));
309
+ const raw = JSON.parse(await obj.text());
310
+ if (raw?.format !== 'paged')
311
+ return directoryDump.parse(raw);
312
+ const manifest = directoryManifest.parse(raw);
313
+ const pages = [];
314
+ for (let seq = 0; seq < manifest.entries; seq++) {
315
+ const part = await r2.get(directoryPageKey(capturedAt, manifest.stageId, seq));
316
+ if (!part)
317
+ throw new Error(`directory backup ${capturedAt} is missing page ${seq}`);
318
+ pages.push(directoryPage.parse(JSON.parse(await part.text())).tables);
319
+ }
320
+ // `restoreDirectory` takes one dump, so the pages are joined here (a paged restore is #2868's follow-up).
321
+ return { capturedAt, tables: mergeDumpPages(pages) };
169
322
  },
170
323
  async delete({ capturedAt }) {
324
+ // The manifest first: once it is gone the copy is no longer listed, whatever of its pages remain.
171
325
  await r2.delete(directoryKey(capturedAt));
326
+ const prefix = `${DIRECTORY_PREFIX}${capturedAt}/`;
327
+ let cursor;
328
+ do {
329
+ const page = await r2.list({ prefix, ...(cursor ? { cursor } : {}) });
330
+ await Promise.all(page.objects.map((o) => r2.delete(o.key)));
331
+ cursor = page.truncated ? page.cursor : undefined;
332
+ } while (cursor);
333
+ },
334
+ async markAttempt({ attemptedAt }) {
335
+ await r2.put(DIRECTORY_ATTEMPT_KEY, JSON.stringify({ attemptedAt }), {
336
+ httpMetadata: { contentType: 'application/json' },
337
+ });
338
+ },
339
+ async lastAttempt() {
340
+ const obj = await r2.get(DIRECTORY_ATTEMPT_KEY);
341
+ if (!obj)
342
+ return null;
343
+ const parsed = z.object({ attemptedAt: z.string().min(1) }).safeParse(JSON.parse(await obj.text()));
344
+ return parsed.success ? parsed.data.attemptedAt : null;
172
345
  },
173
346
  };
174
347
  }
@@ -217,7 +390,7 @@ function occurrenceDay(occurredAt) {
217
390
  * *"Iceberg is the contract, the query engine is replaceable"*, and the same
218
391
  * paragraph leaves R2 SQL's fitness explicitly open pending a benchmark. Writing
219
392
  * partitioned NDJSON to a bucket the platform already operates gets the events out
220
- * of the scope — which is what bounds an outbox that is never pruned — without
393
+ * of the scope — which is what licenses the retention prune that bounds the outbox — without
221
394
  * committing the ingest path to a product decision nobody has made yet. A
222
395
  * compaction step turns these objects into Iceberg later, and the seam means the
223
396
  * drain never learns which happened.
@@ -338,10 +511,13 @@ async function listAll(r2, prefix) {
338
511
  const DAY_MS = 86_400_000;
339
512
  /**
340
513
  * Drop `scopes/…` reap copies (#493) older than the window. A copy's age is its
341
- * `capturedAt` — the key's last segment, same as the store's own fallback address — so a
342
- * hand-uploaded object without metadata still ages out rather than living forever by
343
- * omission. An unparsable timestamp keeps the copy: deleting what cannot be dated is not
344
- * retention.
514
+ * `capturedAt`: the object's metadata, else the key's segment after the scope, which is the
515
+ * manifest's name and the directory a paged copy's entries sit in (#2839). So a hand-uploaded
516
+ * object without metadata still ages out rather than living forever by omission. An
517
+ * unparsable timestamp keeps the copy: deleting what cannot be dated is not retention.
518
+ *
519
+ * A copy's manifest sorts before its entries (`.` before `/`), so it goes first: a pass cut
520
+ * short leaves entries no backup names, for the next pass, never a backup missing its entries.
345
521
  *
346
522
  * Note the interaction with #37 (erasure): per-subject payloads inside these dumps are
347
523
  * sealed under per-subject DEKs, so an old copy's PII exposure is bounded by key
@@ -352,7 +528,7 @@ export async function pruneScopeBackups(bucket, options) {
352
528
  const cutoff = (options.now?.() ?? new Date()).getTime() - options.olderThanDays * DAY_MS;
353
529
  let pruned = 0;
354
530
  for (const o of await listAll(r2, 'scopes/')) {
355
- const capturedAt = o.customMetadata?.capturedAt ?? o.key.slice(o.key.lastIndexOf('/') + 1).replace(/\.json$/, '');
531
+ const capturedAt = o.customMetadata?.capturedAt ?? (o.key.split('/')[3] ?? '').replace(/\.json$/, '');
356
532
  const at = new Date(capturedAt).getTime();
357
533
  if (Number.isNaN(at) || at >= cutoff)
358
534
  continue;