@byok-sdk/cloud-dataplane 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,14 +5,15 @@ implementations of **all ten cloud-local store ports and all seven `@byok-sdk/co
5
5
  ports**, the R2/S3 object adapter that backs the blob port, and the forward-only
6
6
  migration runner that creates the tables they read.
7
7
 
8
- Three store/maintenance compositions plus one transaction authority ship from here. `createPostgresCloudStores` supplies the full
8
+ Four store/maintenance compositions plus one transaction authority ship from here. `createPostgresCloudStores` supplies the full
9
9
  `CloudStores` bundle (`activity`, `devices`, `pairingCodes`, `nonces`, `dedup`, `tasks`,
10
10
  `receipts`, `proofReceipts`, `blobs`, `rateLimiter`); `createPostgresCoreStores`
11
11
  supplies the full `CoreStores` bundle (`mailbox`, `board`, `truth`, `presence`,
12
12
  `objects`, `quota`, `skillPacks`). Both return every port rather than a subset,
13
13
  because the conformance suites certify a composition as a whole — there is no
14
14
  partial bundle for them to run. `createPostgresCloudMaintenance` is the third,
15
- host-only operational composition; it is deliberately outside both port
15
+ host-only operational composition; `createPostgresTenantErasure` is the fourth,
16
+ Node-only destructive operator composition. Both deliberately sit outside port
16
17
  inventories.
17
18
 
18
19
  `PostgresDeviceAssertionReplayAuthority` is a separate online security
@@ -54,14 +55,15 @@ the SDK never detects where it is running and never falls back between the two.
54
55
 
55
56
  - **Node/VPS resident service.** Import the package root
56
57
  (`@byok-sdk/cloud-dataplane`) and talk to Postgres and R2/S3 directly. The
57
- migration runner and the cleanup/maintenance composition run in-process:
58
+ migration runner, cleanup/maintenance, and tenant erasure run in-process:
58
59
  this is the entry that carries the Node-only operations.
59
60
  - **Cloudflare Workers.** Import `@byok-sdk/cloud-dataplane/runtime` — the
60
61
  online request path alone (pool, both store compositions, R2 blob store,
61
62
  truth committer). Its graph never reaches a node builtin beyond what
62
63
  `nodejs_compat` provides, so `pg` stays external in this package's build and
63
64
  is satisfied by the platform: Hyperdrive terminates the database connection,
64
- and R2 is reached over `fetch` with `aws4fetch`. Migrations and cleanup have
65
+ and R2 is reached over `fetch` with `aws4fetch`. Migrations, cleanup, and
66
+ tenant erasure have
65
67
  no place on a Worker — they read files off disk — so run them from a CI job
66
68
  or an operator's Node process against the direct DSN:
67
69
 
@@ -168,6 +170,58 @@ those ports. It sits here rather than inside `@byok-sdk/cloud` for two reasons:
168
170
  and `@byok-sdk/cloud` stay loadable on Workers precisely because `pg` never enters
169
171
  their dependency graph.
170
172
 
173
+ ## Tenant erasure
174
+
175
+ `createPostgresTenantErasure` is a separate Node-only operator primitive, not a
176
+ zero-retention shortcut and not `PostgresCloudCleanup` with a different cursor.
177
+ It owns the product-table inventory, FK-safe deletion order, one operation id,
178
+ CAS lease, bounded R2/SQL pages, and completed receipt. The
179
+ `tenant_erasure_operation` row is package/operator control evidence rather than
180
+ tenant product data: it deliberately remains after completion. The migration
181
+ ledger, deployment config, and every other tenant are never touched.
182
+
183
+ The host must authorize the request and quiesce product writes before starting;
184
+ the SDK cannot manufacture a host write fence. Use a direct Postgres DSN, never
185
+ a request-path pooler, and configure the exact same immutable `keyPrefix` used
186
+ by the serving blob composition.
187
+
188
+ ```ts
189
+ import {
190
+ createByokPool,
191
+ createPostgresTenantErasure,
192
+ type TenantErasureResult,
193
+ } from '@byok-sdk/cloud-dataplane';
194
+
195
+ const pool = createByokPool({ connectionString: env.BYOK_DIRECT_DATABASE_URL });
196
+ const erasure = createPostgresTenantErasure({
197
+ pool,
198
+ clock: { now: () => new Date() },
199
+ objectStorage: {
200
+ endpoint: env.R2_ENDPOINT,
201
+ bucket: env.R2_BUCKET,
202
+ accessKeyId: env.R2_ACCESS_KEY_ID,
203
+ secretAccessKey: env.R2_SECRET_ACCESS_KEY,
204
+ region: 'auto',
205
+ keyPrefix: 'production', // omit only when the deployment has always been unprefixed
206
+ signingClock: { now: () => new Date() },
207
+ },
208
+ });
209
+
210
+ const result: TenantErasureResult = await erasure.eraseTenant(tenantId, operationId);
211
+ // `outstanding` / `partial`: retry the same operationId; `conflict`: another
212
+ // unfinished operation owns this tenant; `completed`: retain the receipt.
213
+ ```
214
+
215
+ Erasure lists only the canonical `tenants/<tenant>/objects/sha256/...` namespace
216
+ (with the configured deployment prefix), deletes each listed canonical object
217
+ before any product table page, and persists progress only after the delete
218
+ returns. A missing/canonical object is a replay-safe delete; a malformed key
219
+ under that namespace is schema/layout drift and returns typed `partial` without
220
+ starting SQL deletion. A response loss/crash between R2 and SQL replays the
221
+ object deletion before it can advance to rows. See
222
+ `deploy/runbooks/tenant-erasure.md` for the operator sequence and recovery
223
+ rules.
224
+
171
225
  Dependency direction is one-way: `cloud-dataplane → core + cloud + protocol +
172
226
  pg +` the explicit S3 signer/XML parser. The protocol edge is used only by the
173
227
  host-owned dead-letter replay path to rebind frozen envelope bytes to the new
@@ -187,11 +241,18 @@ vendoring the SQL into your own repository would make that copy a second source
187
241
  of truth, free to drift from the runner installed beside it.
188
242
 
189
243
  ```ts
190
- import { createByokPool, migrate, migrationsDir } from '@byok-sdk/cloud-dataplane';
244
+ import {
245
+ createByokPool,
246
+ migrate,
247
+ migrationsDir,
248
+ verifyMigrations,
249
+ } from '@byok-sdk/cloud-dataplane';
191
250
 
192
251
  const pool = createByokPool({ connectionString: process.env.DATABASE_URL! });
193
252
  const result = await migrate(pool, migrationsDir());
194
253
  console.log(result.applied); // e.g. ['0001_cloud_local.sql', ..., '0004_device_proof_truth.sql']
254
+ const applied = await verifyMigrations(pool);
255
+ console.log(applied); // exact ordered [{ version, checksum }, ...] rows
195
256
  ```
196
257
 
197
258
  `migrate` still takes its directory explicitly, because the same runner also
@@ -218,6 +279,14 @@ The runner:
218
279
  A consequence of per-file transactions: a statement that cannot run inside one
219
280
  (`CREATE INDEX CONCURRENTLY`) cannot appear in a migration file.
220
281
 
282
+ `verifyMigrations(pool, directory = migrationsDir())` is a Node-only root export
283
+ for readiness/readback checks. It reads the package migration files and the
284
+ existing `byok_schema_migration` ledger, returning exact rows in migration order
285
+ only when every version and checksum matches. Missing rows, unexpected rows,
286
+ checksum drift, and an absent ledger table throw one aggregate
287
+ `MigrationStateMismatchError` with stable `issues` ordering. Verification never
288
+ creates the ledger or applies migrations; `./runtime` does not export it.
289
+
221
290
  ## Pool
222
291
 
223
292
  `createByokPool` exists to configure one thing that matters: `int8` columns
package/dist/index.d.ts CHANGED
@@ -27,8 +27,10 @@
27
27
  * tsup pass over `src/runtime.ts`, and pinned by the runtime-entry test.
28
28
  */
29
29
  export * from './runtime';
30
- export { MigrationChecksumMismatchError, MigrationFilenameError, migrate, readMigrationFiles } from './migrate';
31
- export type { MigrationFile, MigrationResult } from './migrate';
30
+ export { MigrationChecksumMismatchError, MigrationFilenameError, MigrationStateMismatchError, migrate, readMigrationFiles, verifyMigrations, } from './migrate';
31
+ export type { MigrationFile, MigrationResult, MigrationStateIssue, MigrationVerificationRow, } from './migrate';
32
32
  export { migrationsDir } from './migrations-dir';
33
33
  export { CLOUD_CLEANUP_ERROR_CODES, CloudCleanupError, PostgresCloudCleanup, createPostgresCloudMaintenance, } from './cleanup';
34
34
  export type { CleanupJobState, CloudCleanupErrorCode, CloudCleanupResult, DeadLetterPage, DeadLetterQuery, DeadLetterRef, DeadLetterReplayInput, ObjectUsageRebuildResult, PostgresCloudCleanupOptions, PostgresCloudMaintenanceOptions, TenantRetentionPolicy, TenantRetentionPolicyInput, } from './cleanup';
35
+ export { TENANT_ERASURE_ERROR_CODES, TENANT_ERASURE_TABLES, PostgresTenantErasure, TenantErasureError, createPostgresTenantErasure, } from './tenant-erasure';
36
+ export type { PostgresTenantErasureCompositionOptions, PostgresTenantErasureOptions, TenantErasureConflict, TenantErasureErrorCode, TenantErasureReadback, TenantErasureResult, TenantErasureStatus, } from './tenant-erasure';