@substrat-run/control-plane-api 0.49.0 → 0.51.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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * The scope-backup write seam (#493) — where a reap's recoverable copy goes.
3
+ *
4
+ * A reap frees a scope's Durable Object storage because Cloudflare never garbage-
5
+ * collects a DO, and it is irreversible. Before #493 the operator had to REMEMBER to
6
+ * take a copy first, from a different surface; this seam is what makes "a reap always
7
+ * leaves a recoverable copy" a property of the route instead of a habit.
8
+ *
9
+ * Why a dump and not a snapshot fork. The obvious cheap move — `orchestratedSnapshot`
10
+ * before the reap — does not survive the case the flow exists for. A fork is provisioned
11
+ * INSIDE the vertical's own deployment and activated, so (a) its bytes live in the very
12
+ * deployment a retirement is about to delete, and (b) it counts as a live scope in
13
+ * `countScopesForVertical`, which means it re-blocks the `deleteVertical` the reap was
14
+ * clearing. A dump leaves the deployment entirely, and `POST …/restore` already loads one
15
+ * back, so export→store→restore is the round trip that actually closes.
16
+ *
17
+ * Provider-neutral by the same posture as `ObservabilityReader` and `DeployVerticalFn`:
18
+ * the contract lives here, an implementation lives in its own module (`r2-backups.ts` for
19
+ * Cloudflare R2), and the host injects one. A backup is addressed by (tenant, scope,
20
+ * capturedAt) rather than by a store-shaped key, so the key scheme stays the store's
21
+ * private business and no caller can build a path into someone else's tenant.
22
+ *
23
+ * Fidelity is deliberately FULL, never masked (#493). A masked dump has its PII redacted,
24
+ * so restoring one produces a structurally-valid but factually wrong scope — a backup that
25
+ * cannot restore is a false promise, and the promise is the whole point. This is not the
26
+ * §6 governed pull: the bytes go platform→platform and are never handed to a caller, which
27
+ * is why the export route's masking default does not apply here.
28
+ */
29
+ import type { DirectoryBackup, DirectoryDump, ScopeBackup, ScopeDump } from '@substrat-run/contracts';
30
+ export type { ScopeBackup, DirectoryBackup };
31
+ export interface ScopeBackupStore {
32
+ /**
33
+ * Store one full-fidelity dump and return its metadata. Must be durable before it
34
+ * returns: the reap's whole guarantee is that this resolved before any byte was wiped.
35
+ */
36
+ put(input: {
37
+ vertical: string | null;
38
+ dump: ScopeDump;
39
+ }): Promise<ScopeBackup>;
40
+ /** Every backup held for one scope, newest first. */
41
+ list(input: {
42
+ tenantId: string;
43
+ scopeId: string;
44
+ }): Promise<ScopeBackup[]>;
45
+ /** One backup's dump, by its capture time. Null when no such copy is held. */
46
+ get(input: {
47
+ tenantId: string;
48
+ scopeId: string;
49
+ capturedAt: string;
50
+ }): Promise<ScopeDump | null>;
51
+ }
52
+ /**
53
+ * The DIRECTORY-backup store (#40) — the platform's own copy, not a tenant's.
54
+ *
55
+ * A sibling seam rather than a widened `ScopeBackupStore`, because the two are the same
56
+ * mechanism serving different jobs and the difference is worth keeping in the types.
57
+ * A scope backup is taken at a MOMENT — before a reap, before a migration — and is
58
+ * addressed by the scope it came from. A directory backup is taken on a SCHEDULE, has
59
+ * no tenant to be addressed by, and is pruned to a retention window. Only this one
60
+ * needs `delete`, and only this one is ever written by a sweep with no operator behind
61
+ * it.
62
+ *
63
+ * What it defends against is also different, and the distinction is the whole design
64
+ * (#40's own analysis). Per-scope point-in-time recovery already covers corruption
65
+ * inside the account — ~30 days, continuous, and strictly better than a daily copy, so
66
+ * scheduled per-scope backups are deliberately NOT built. The directory is the case PITR
67
+ * cannot answer: it is one Durable Object, and a bug that deletes it outright leaves
68
+ * nothing to rewind. Copies live outside the DO for that reason.
69
+ *
70
+ * Honest scoping: with the store bound in the platform's OWN account (the shape shipped
71
+ * today), this survives losing the directory, not losing the account. The seam is
72
+ * provider-neutral so an off-account target is a drop-in — see control-plane.md §4.9.
73
+ */
74
+ export interface DirectoryBackupStore {
75
+ /**
76
+ * Store one full-fidelity directory dump and return its metadata. Durable before it
77
+ * returns, like the scope store's `put`.
78
+ */
79
+ put(input: {
80
+ dump: DirectoryDump;
81
+ }): Promise<DirectoryBackup>;
82
+ /** Every directory copy held, newest first. */
83
+ list(): Promise<DirectoryBackup[]>;
84
+ /** One copy's dump, by its capture time. Null when no such copy is held. */
85
+ get(input: {
86
+ capturedAt: string;
87
+ }): Promise<DirectoryDump | null>;
88
+ /** Drop one copy — how the retention window is enforced. Missing is not an error. */
89
+ delete(input: {
90
+ capturedAt: string;
91
+ }): Promise<void>;
92
+ }
93
+ //# sourceMappingURL=backups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backups.d.ts","sourceRoot":"","sources":["../src/backups.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAKtG,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC;AAE7C,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,SAAS,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/E,qDAAqD;IACrD,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3E,8EAA8E;IAC9E,GAAG,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;CAClG;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,oBAAoB;IACnC;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,aAAa,CAAA;KAAE,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9D,+CAA+C;IAC/C,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACnC,4EAA4E;IAC5E,GAAG,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAClE,qFAAqF;IACrF,MAAM,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtD"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The scope-backup write seam (#493) — where a reap's recoverable copy goes.
3
+ *
4
+ * A reap frees a scope's Durable Object storage because Cloudflare never garbage-
5
+ * collects a DO, and it is irreversible. Before #493 the operator had to REMEMBER to
6
+ * take a copy first, from a different surface; this seam is what makes "a reap always
7
+ * leaves a recoverable copy" a property of the route instead of a habit.
8
+ *
9
+ * Why a dump and not a snapshot fork. The obvious cheap move — `orchestratedSnapshot`
10
+ * before the reap — does not survive the case the flow exists for. A fork is provisioned
11
+ * INSIDE the vertical's own deployment and activated, so (a) its bytes live in the very
12
+ * deployment a retirement is about to delete, and (b) it counts as a live scope in
13
+ * `countScopesForVertical`, which means it re-blocks the `deleteVertical` the reap was
14
+ * clearing. A dump leaves the deployment entirely, and `POST …/restore` already loads one
15
+ * back, so export→store→restore is the round trip that actually closes.
16
+ *
17
+ * Provider-neutral by the same posture as `ObservabilityReader` and `DeployVerticalFn`:
18
+ * the contract lives here, an implementation lives in its own module (`r2-backups.ts` for
19
+ * Cloudflare R2), and the host injects one. A backup is addressed by (tenant, scope,
20
+ * capturedAt) rather than by a store-shaped key, so the key scheme stays the store's
21
+ * private business and no caller can build a path into someone else's tenant.
22
+ *
23
+ * Fidelity is deliberately FULL, never masked (#493). A masked dump has its PII redacted,
24
+ * so restoring one produces a structurally-valid but factually wrong scope — a backup that
25
+ * cannot restore is a false promise, and the promise is the whole point. This is not the
26
+ * §6 governed pull: the bytes go platform→platform and are never handed to a caller, which
27
+ * is why the export route's masking default does not apply here.
28
+ */
29
+ export {};
30
+ //# sourceMappingURL=backups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backups.js","sourceRoot":"","sources":["../src/backups.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The scheduled directory backup (#40) — the platform's own disaster-recovery pass.
3
+ *
4
+ * `control-plane.md` names the stake without resolving it: *"The directory becomes a real
5
+ * database, with its own migrations and backup story… Losing it is losing the platform,
6
+ * not losing a cache."* Every OTHER database the platform holds is a scope, protected by
7
+ * ~30-day Durable Object point-in-time recovery — which is continuous, per-scope, and
8
+ * strictly better than any daily copy, so scheduled per-scope backups are deliberately not
9
+ * built here. The directory is the one database PITR cannot answer for: it is a single DO,
10
+ * and a control-plane bug that deletes it outright leaves nothing to rewind. So a copy
11
+ * lives OUTSIDE the DO, and this is what takes it.
12
+ *
13
+ * Driven by the same cron that drives the platform sweep, and the cadence is enforced HERE
14
+ * rather than by a second trigger. A quarter-hourly sweep asking "is a copy due?" and answering
15
+ * from the store is idempotent, survives a missed tick (the next pass takes it, late
16
+ * rather than never), and needs no durable "last run" state of its own — the newest stored
17
+ * copy IS that state. A dedicated daily trigger would have to be right about time zones,
18
+ * deploys and restarts to do the same job worse.
19
+ */
20
+ import type { DirectoryBackup, PlatformActorId } from '@substrat-run/contracts';
21
+ import type { HostAdmin } from '@substrat-run/kernel';
22
+ import type { DirectoryBackupStore } from './backups.js';
23
+ export interface DirectoryBackupOptions {
24
+ admin: HostAdmin;
25
+ store: DirectoryBackupStore;
26
+ /** The platform actor the export is audited under. */
27
+ actor: PlatformActorId;
28
+ /** Hours between copies (default 24). */
29
+ intervalHours?: number;
30
+ /** How many copies to keep (default 30). Older ones are deleted after a successful put. */
31
+ retain?: number;
32
+ /** Take a copy regardless of cadence — what the manual "back up now" route passes. */
33
+ force?: boolean;
34
+ /** Injected clock, for tests. */
35
+ now?: () => Date;
36
+ }
37
+ export interface DirectoryBackupResult {
38
+ /** The copy taken this pass, or null when the cadence guard skipped it. */
39
+ taken: DirectoryBackup | null;
40
+ /** When nothing was taken: the copy that was still fresh enough to skip for. */
41
+ skippedFor: string | null;
42
+ /** Copies deleted by the retention window this pass. */
43
+ pruned: number;
44
+ }
45
+ /**
46
+ * Take a directory backup if one is due, then prune to the retention window.
47
+ *
48
+ * Order matters and is the inverse of the reap's (#493). A reap stores BEFORE it wipes,
49
+ * because the wipe is what the copy protects against. Here the prune runs AFTER a
50
+ * successful put, so a failed capture can never be the thing that deletes the last good
51
+ * copy — the worst case is an extra copy kept, never a missing one.
52
+ *
53
+ * Throws whatever the export or the store threw. The caller (a cron pass) decides what a
54
+ * failure means; it must not be silent, which is why nothing is swallowed here.
55
+ */
56
+ export declare function backupDirectoryIfDue(options: DirectoryBackupOptions): Promise<DirectoryBackupResult>;
57
+ //# sourceMappingURL=directory-backup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-backup.d.ts","sourceRoot":"","sources":["../src/directory-backup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAUzD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,oBAAoB,CAAC;IAC5B,sDAAsD;IACtD,KAAK,EAAE,eAAe,CAAC;IACvB,yCAAyC;IACzC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iCAAiC;IACjC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,2EAA2E;IAC3E,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IAC9B,gFAAgF;IAChF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,wDAAwD;IACxD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CA+BhC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The scheduled directory backup (#40) — the platform's own disaster-recovery pass.
3
+ *
4
+ * `control-plane.md` names the stake without resolving it: *"The directory becomes a real
5
+ * database, with its own migrations and backup story… Losing it is losing the platform,
6
+ * not losing a cache."* Every OTHER database the platform holds is a scope, protected by
7
+ * ~30-day Durable Object point-in-time recovery — which is continuous, per-scope, and
8
+ * strictly better than any daily copy, so scheduled per-scope backups are deliberately not
9
+ * built here. The directory is the one database PITR cannot answer for: it is a single DO,
10
+ * and a control-plane bug that deletes it outright leaves nothing to rewind. So a copy
11
+ * lives OUTSIDE the DO, and this is what takes it.
12
+ *
13
+ * Driven by the same cron that drives the platform sweep, and the cadence is enforced HERE
14
+ * rather than by a second trigger. A quarter-hourly sweep asking "is a copy due?" and answering
15
+ * from the store is idempotent, survives a missed tick (the next pass takes it, late
16
+ * rather than never), and needs no durable "last run" state of its own — the newest stored
17
+ * copy IS that state. A dedicated daily trigger would have to be right about time zones,
18
+ * deploys and restarts to do the same job worse.
19
+ */
20
+ /** One copy a day; the sweep runs far more often and skips in between. */
21
+ const DEFAULT_INTERVAL_HOURS = 24;
22
+ /**
23
+ * Thirty copies — a month of daily history, matching the DO PITR horizon so the two
24
+ * defences expire together rather than leaving a window covered by neither.
25
+ */
26
+ const DEFAULT_RETAIN = 30;
27
+ /**
28
+ * Take a directory backup if one is due, then prune to the retention window.
29
+ *
30
+ * Order matters and is the inverse of the reap's (#493). A reap stores BEFORE it wipes,
31
+ * because the wipe is what the copy protects against. Here the prune runs AFTER a
32
+ * successful put, so a failed capture can never be the thing that deletes the last good
33
+ * copy — the worst case is an extra copy kept, never a missing one.
34
+ *
35
+ * Throws whatever the export or the store threw. The caller (a cron pass) decides what a
36
+ * failure means; it must not be silent, which is why nothing is swallowed here.
37
+ */
38
+ export async function backupDirectoryIfDue(options) {
39
+ const now = options.now?.() ?? new Date();
40
+ const retain = options.retain ?? DEFAULT_RETAIN;
41
+ const intervalMs = (options.intervalHours ?? DEFAULT_INTERVAL_HOURS) * 3_600_000;
42
+ // The store IS the schedule's memory: newest copy, newest first (the store sorts).
43
+ const existing = await options.store.list();
44
+ const newest = existing[0];
45
+ if (!options.force && newest) {
46
+ const age = now.getTime() - new Date(newest.capturedAt).getTime();
47
+ // `age < 0` — a copy stamped in the future, from a clock skew or a hand-uploaded
48
+ // object — counts as fresh. Skipping is the conservative read: taking a copy is
49
+ // cheap, but treating a future timestamp as "infinitely overdue" would take one on
50
+ // every single pass until real time caught up.
51
+ if (age < intervalMs)
52
+ return { taken: null, skippedFor: newest.capturedAt, pruned: 0 };
53
+ }
54
+ const dump = await options.admin.exportDirectory(options.actor);
55
+ const taken = await options.store.put({ dump });
56
+ // Prune from a re-read list, not from `existing`: the copy just written has to be in
57
+ // the reckoning, or a retention window of N would keep N+1 forever.
58
+ let pruned = 0;
59
+ if (retain > 0) {
60
+ const all = await options.store.list();
61
+ for (const stale of all.slice(retain)) {
62
+ await options.store.delete({ capturedAt: stale.capturedAt });
63
+ pruned += 1;
64
+ }
65
+ }
66
+ return { taken, skippedFor: null, pruned };
67
+ }
68
+ //# sourceMappingURL=directory-backup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directory-backup.js","sourceRoot":"","sources":["../src/directory-backup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAMH,0EAA0E;AAC1E,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAClC;;;GAGG;AACH,MAAM,cAAc,GAAG,EAAE,CAAC;AA0B1B;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAA+B;IAE/B,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC;IAC1C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,cAAc,CAAC;IAChD,MAAM,UAAU,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,sBAAsB,CAAC,GAAG,SAAS,CAAC;IAEjF,mFAAmF;IACnF,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,CAAC,OAAO,CAAC,KAAK,IAAI,MAAM,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC;QAClE,iFAAiF;QACjF,gFAAgF;QAChF,mFAAmF;QACnF,+CAA+C;QAC/C,IAAI,GAAG,GAAG,UAAU;YAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACzF,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAChE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IAEhD,qFAAqF;IACrF,oEAAoE;IACpE,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACvC,KAAK,MAAM,KAAK,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YACtC,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;YAC7D,MAAM,IAAI,CAAC,CAAC;QACd,CAAC;IACH,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Resolving a scope's Durable Object to something an operator can OPEN.
3
+ *
4
+ * A scope's data lives in one Durable Object, named after the scope id inside a namespace
5
+ * (a class × a script). The console has always been able to say the name; the dashboard,
6
+ * however, addresses a namespace by an id Cloudflare assigns — not by the class or the
7
+ * script — so without that id the best a link could do was the account-wide namespace
8
+ * list, which on a real fleet is one entry per pushed script.
9
+ *
10
+ * This is the seam that closes the gap: given the script a scope serves from, hand back
11
+ * the namespaces defined in it. Injected by the host like every other Cloudflare read
12
+ * (D-34: this package holds no SDK and no credential), and entirely optional — absent, the
13
+ * console falls back to the namespace list exactly as before.
14
+ */
15
+ /** One Durable Object namespace as Cloudflare describes it. */
16
+ export interface DoNamespaceRecord {
17
+ /** The dashboard's address for this namespace — the whole point of the read. */
18
+ id: string;
19
+ /** The exported class the namespace is defined by (e.g. `ScopeDO`). */
20
+ className: string;
21
+ /** The script defining it — a dispatch-namespace script name for a pushed vertical. */
22
+ script: string | null;
23
+ name: string | null;
24
+ useSqlite: boolean;
25
+ }
26
+ export interface DoNamespaceReader {
27
+ /** Every Durable Object namespace on the account. Filtering by script happens above:
28
+ * Cloudflare's list endpoint takes no filter, and a reader that pretended otherwise
29
+ * would hide the pagination cost from the one place that can cache it. */
30
+ list(): Promise<DoNamespaceRecord[]>;
31
+ }
32
+ export interface CfDoNamespaceOptions {
33
+ accountId: string;
34
+ apiToken: string;
35
+ /** Injected for tests. */
36
+ fetchImpl?: typeof fetch;
37
+ /**
38
+ * How long a listing stays fresh. The mapping changes only when a script is pushed or
39
+ * deleted, and the read is one-or-more paginated API calls that would otherwise run on
40
+ * every scope-detail open — so the default trades a few minutes of staleness (worst
41
+ * case: a just-pushed script's namespace is not linkable yet) for not hammering the API.
42
+ */
43
+ ttlMs?: number;
44
+ /** Hard cap on pages walked, so an account with a very large fleet degrades to a slow
45
+ * read rather than an unbounded one. */
46
+ maxPages?: number;
47
+ }
48
+ /**
49
+ * A {@link DoNamespaceReader} over Cloudflare's account-wide list endpoint, with a small
50
+ * TTL cache. Pure web-standard `fetch`, like every other Cloudflare seam in this package.
51
+ */
52
+ export declare function createCfDoNamespaceReader(opts: CfDoNamespaceOptions): DoNamespaceReader;
53
+ /**
54
+ * The namespaces defined in one script, most-likely-scope-class first.
55
+ *
56
+ * A vertical's script defines several classes (the scope DO, an identity DO, sweepers), so
57
+ * ORDER is the useful part: the console links the first entry, and a scope's data is in the
58
+ * scope class. Matching is on the class name because that is the only stable signal — the
59
+ * namespace's `name` is Cloudflare's, and the binding name lives in the script's settings,
60
+ * not here. Every namespace of the script is still returned, so a vertical that named its
61
+ * class something else is one click away rather than unreachable.
62
+ */
63
+ export declare function namespacesForScript(rows: DoNamespaceRecord[], script: string): DoNamespaceRecord[];
64
+ //# sourceMappingURL=do-namespaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"do-namespaces.d.ts","sourceRoot":"","sources":["../src/do-namespaces.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAC;IACX,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC;;+EAE2E;IAC3E,IAAI,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;6CACyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAID;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,oBAAoB,GAAG,iBAAiB,CAiEvF;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,EAAE,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE,CASlG"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Resolving a scope's Durable Object to something an operator can OPEN.
3
+ *
4
+ * A scope's data lives in one Durable Object, named after the scope id inside a namespace
5
+ * (a class × a script). The console has always been able to say the name; the dashboard,
6
+ * however, addresses a namespace by an id Cloudflare assigns — not by the class or the
7
+ * script — so without that id the best a link could do was the account-wide namespace
8
+ * list, which on a real fleet is one entry per pushed script.
9
+ *
10
+ * This is the seam that closes the gap: given the script a scope serves from, hand back
11
+ * the namespaces defined in it. Injected by the host like every other Cloudflare read
12
+ * (D-34: this package holds no SDK and no credential), and entirely optional — absent, the
13
+ * console falls back to the namespace list exactly as before.
14
+ */
15
+ const PER_PAGE = 100;
16
+ /**
17
+ * A {@link DoNamespaceReader} over Cloudflare's account-wide list endpoint, with a small
18
+ * TTL cache. Pure web-standard `fetch`, like every other Cloudflare seam in this package.
19
+ */
20
+ export function createCfDoNamespaceReader(opts) {
21
+ const fetchImpl = opts.fetchImpl ?? fetch;
22
+ const ttlMs = opts.ttlMs ?? 5 * 60_000;
23
+ const maxPages = opts.maxPages ?? 50;
24
+ const base = `https://api.cloudflare.com/client/v4/accounts/${opts.accountId}/workers/durable_objects/namespaces`;
25
+ let cached = null;
26
+ // Concurrent scope-detail opens must not each start their own walk; they share the
27
+ // in-flight promise and settle together.
28
+ let inFlight = null;
29
+ async function walk() {
30
+ const rows = [];
31
+ for (let page = 1; page <= maxPages; page++) {
32
+ const res = await fetchImpl(`${base}?page=${page}&per_page=${PER_PAGE}`, {
33
+ headers: { authorization: `Bearer ${opts.apiToken}` },
34
+ });
35
+ if (!res.ok) {
36
+ throw new Error(`Cloudflare Durable Object namespace list failed (${res.status})`);
37
+ }
38
+ const body = (await res.json());
39
+ if (body.success === false) {
40
+ throw new Error(`Cloudflare Durable Object namespace list failed: ${body.errors?.map((e) => e.message).join('; ') || 'unknown error'}`);
41
+ }
42
+ const batch = body.result ?? [];
43
+ for (const r of batch) {
44
+ // An id-less row cannot be linked to, which is this read's whole purpose.
45
+ if (!r.id || !r.class)
46
+ continue;
47
+ rows.push({
48
+ id: r.id,
49
+ className: r.class,
50
+ script: r.script ?? null,
51
+ name: r.name ?? null,
52
+ useSqlite: r.use_sqlite === true,
53
+ });
54
+ }
55
+ if (batch.length < PER_PAGE)
56
+ break;
57
+ }
58
+ return rows;
59
+ }
60
+ return {
61
+ async list() {
62
+ const now = Date.now();
63
+ if (cached && now - cached.at < ttlMs)
64
+ return cached.rows;
65
+ if (inFlight)
66
+ return inFlight;
67
+ inFlight = walk()
68
+ .then((rows) => {
69
+ cached = { at: Date.now(), rows };
70
+ return rows;
71
+ })
72
+ .finally(() => {
73
+ inFlight = null;
74
+ });
75
+ return inFlight;
76
+ },
77
+ };
78
+ }
79
+ /**
80
+ * The namespaces defined in one script, most-likely-scope-class first.
81
+ *
82
+ * A vertical's script defines several classes (the scope DO, an identity DO, sweepers), so
83
+ * ORDER is the useful part: the console links the first entry, and a scope's data is in the
84
+ * scope class. Matching is on the class name because that is the only stable signal — the
85
+ * namespace's `name` is Cloudflare's, and the binding name lives in the script's settings,
86
+ * not here. Every namespace of the script is still returned, so a vertical that named its
87
+ * class something else is one click away rather than unreachable.
88
+ */
89
+ export function namespacesForScript(rows, script) {
90
+ const mine = rows.filter((r) => r.script === script);
91
+ const rank = (r) => {
92
+ const c = r.className.toLowerCase();
93
+ if (c === 'scopedo')
94
+ return 0;
95
+ if (c.includes('scope'))
96
+ return 1;
97
+ return 2;
98
+ };
99
+ return mine.sort((a, b) => rank(a) - rank(b) || a.className.localeCompare(b.className));
100
+ }
101
+ //# sourceMappingURL=do-namespaces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"do-namespaces.js","sourceRoot":"","sources":["../src/do-namespaces.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -16,6 +16,13 @@ export { blobStoreBindings, collectBlobStoreHandles, collectTenantStoreHandles,
16
16
  export { createCustomHostnameProvisioner, mapCfStatus, extractRecords, reconcilePendingHostnames, isCustomHostname, validateBindableHostname, } from './custom-hostnames.js';
17
17
  export type { CustomHostnameProvisioner, CustomHostnameProvisionerOptions, CustomHostnameIssuance, ReconcileHostnamesResult, } from './custom-hostnames.js';
18
18
  export type { ObservabilityReader, ServiceMetricsRow, RecentLogEvent } from './observability.js';
19
+ export type { PlatformRuntime } from './platform-runtime.js';
20
+ export { createCfDoNamespaceReader, namespacesForScript } from './do-namespaces.js';
21
+ export type { DoNamespaceReader, DoNamespaceRecord, CfDoNamespaceOptions } from './do-namespaces.js';
19
22
  export { createCfObservabilityReader } from './cf-observability.js';
20
23
  export type { CfObservabilityOptions } from './cf-observability.js';
24
+ export type { ScopeBackup, ScopeBackupStore, DirectoryBackup, DirectoryBackupStore, } from './backups.js';
25
+ export { createR2BackupStore, createR2DirectoryBackupStore } from './r2-backups.js';
26
+ export { backupDirectoryIfDue } from './directory-backup.js';
27
+ export type { DirectoryBackupOptions, DirectoryBackupResult } from './directory-backup.js';
21
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACjD,YAAY,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AACvD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACrG,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,SAAS,GACV,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpE,YAAY,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,EACtB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,qBAAqB,EACrB,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACrG,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAChG,YAAY,EACV,kBAAkB,EAClB,qBAAqB,EACrB,iBAAiB,EACjB,aAAa,EACb,aAAa,GACd,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,+BAA+B,EAC/B,WAAW,EACX,cAAc,EACd,yBAAyB,EACzB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,yBAAyB,EACzB,gCAAgC,EAChC,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACjG,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACpE,YAAY,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -8,5 +8,11 @@ export { assertSandboxContract, deployManifest, deploymentRefFor, stableDeployme
8
8
  export { createWfpUploader, createWfpModulesFetcher, createWfpBindingsPatcher } from './wfp.js';
9
9
  export { blobStoreBindings, collectBlobStoreHandles, collectTenantStoreHandles, tenantStoreBindings, } from './tenant-stores.js';
10
10
  export { createCustomHostnameProvisioner, mapCfStatus, extractRecords, reconcilePendingHostnames, isCustomHostname, validateBindableHostname, } from './custom-hostnames.js';
11
+ export { createCfDoNamespaceReader, namespacesForScript } from './do-namespaces.js';
11
12
  export { createCfObservabilityReader } from './cf-observability.js';
13
+ export { createR2BackupStore, createR2DirectoryBackupStore } from './r2-backups.js';
14
+ // #40 — the scheduled directory copy. Exported as a function the CP worker's cron calls,
15
+ // not as a route with a timer behind it: this package stays library-only, exactly as the
16
+ // platform-request drain does (the recurrence has to come from a deployment, #444).
17
+ export { backupDirectoryIfDue } from './directory-backup.js';
12
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAUrG,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,GAUvB,MAAM,qBAAqB,CAAC;AAO7B,OAAO,EACL,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAQhG,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,+BAA+B,EAC/B,WAAW,EACX,cAAc,EACd,yBAAyB,EACzB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAQ/B,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,aAAa,EACb,2BAA2B,EAC3B,mBAAmB,EACnB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAUrG,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,uBAAuB,EACvB,mBAAmB,EACnB,sBAAsB,EACtB,sBAAsB,GAUvB,MAAM,qBAAqB,CAAC;AAO7B,OAAO,EACL,qBAAqB,EACrB,cAAc,EACd,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAC;AAQhG,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,yBAAyB,EACzB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,+BAA+B,EAC/B,WAAW,EACX,cAAc,EACd,yBAAyB,EACzB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAS/B,OAAO,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEpF,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AAQpE,OAAO,EAAE,mBAAmB,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AACpF,yFAAyF;AACzF,yFAAyF;AACzF,oFAAoF;AACpF,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/mask.d.ts CHANGED
@@ -1,4 +1,19 @@
1
1
  import type { ScopeDumpTable } from '@substrat-run/contracts';
2
2
  /** Mask one dump in place-shape (returns new arrays; never mutates the input). */
3
3
  export declare function maskDump(tables: ScopeDumpTable[]): ScopeDumpTable[];
4
+ /**
5
+ * The same heuristic applied to plain JSON records — the directory half of a tenant
6
+ * export (#36).
7
+ *
8
+ * A tenant export carries two kinds of thing: scope databases (table-shaped, masked by
9
+ * `maskDump` above) and directory records (object-shaped — a tenant's display name, an
10
+ * org's name, an identity link's external id, which is usually an email). Both halves
11
+ * must be masked by the SAME rule, or the default-masked promise is only half true and
12
+ * the leak is in the half nobody looked at.
13
+ *
14
+ * So this reuses `maskJsonValue` rather than growing a second heuristic: one PII column
15
+ * list, one recursive sweep, two entry points. Ids and timestamps pass through — the
16
+ * sweep targets free text, and ids are what keep an export intelligible.
17
+ */
18
+ export declare function maskRecords<T>(records: readonly T[]): T[];
4
19
  //# sourceMappingURL=mask.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAoD9D,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAqBnE"}
1
+ {"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AA2D9D,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAqBnE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,EAAE,CAEzD"}
package/dist/mask.js CHANGED
@@ -22,7 +22,14 @@
22
22
  */
23
23
  // Free-text/PII column names. `name` is included on purpose: entity names
24
24
  // (customers, properties, contacts) are customer data even when they look benign.
25
- const PII_COLUMN = /(^|_)(email|e?mail_address|phone|mobile|tel|address|street|city|postal|zip|ssn|personnummer|name|first_name|last_name|full_name|contact|note|notes|comment|comments|message|subject|body|description)($|_)/i;
25
+ //
26
+ // `external_id` earns its place from the platform's OWN schema (#36): an identity link's
27
+ // `externalId` is the provider's subject, which in practice is very often the person's
28
+ // email address — so a tenant export that swept only the obvious columns would hand out
29
+ // the one field most likely to name a human. It is the deliberately lossy direction of
30
+ // the trade: an opaque third-party id (a document ref, an upstream order number) gets
31
+ // masked too, which costs a masked pull some fidelity and costs a leak nothing.
32
+ const PII_COLUMN = /(^|_)(email|e?mail_address|phone|mobile|tel|address|street|city|postal|zip|ssn|personnummer|name|first_name|last_name|full_name|contact|external_id|note|notes|comment|comments|message|subject|body|description)($|_)/i;
26
33
  // Columns that carry JSON documents worth sweeping by key rather than blanking.
27
34
  const JSON_COLUMN = /(^|_)(payload|detail|details|data|before|after)($|_)/i;
28
35
  const MASKED = '[masked]';
@@ -68,4 +75,21 @@ export function maskDump(tables) {
68
75
  return { ...t, rows };
69
76
  });
70
77
  }
78
+ /**
79
+ * The same heuristic applied to plain JSON records — the directory half of a tenant
80
+ * export (#36).
81
+ *
82
+ * A tenant export carries two kinds of thing: scope databases (table-shaped, masked by
83
+ * `maskDump` above) and directory records (object-shaped — a tenant's display name, an
84
+ * org's name, an identity link's external id, which is usually an email). Both halves
85
+ * must be masked by the SAME rule, or the default-masked promise is only half true and
86
+ * the leak is in the half nobody looked at.
87
+ *
88
+ * So this reuses `maskJsonValue` rather than growing a second heuristic: one PII column
89
+ * list, one recursive sweep, two entry points. Ids and timestamps pass through — the
90
+ * sweep targets free text, and ids are what keep an export intelligible.
91
+ */
92
+ export function maskRecords(records) {
93
+ return records.map((r) => maskJsonValue(r, false));
94
+ }
71
95
  //# sourceMappingURL=mask.js.map
package/dist/mask.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,0EAA0E;AAC1E,kFAAkF;AAClF,MAAM,UAAU,GAAG,6MAA6M,CAAC;AAEjO,gFAAgF;AAChF,MAAM,WAAW,GAAG,uDAAuD,CAAC;AAE5E,MAAM,MAAM,GAAG,UAAU,CAAC;AAE1B,iFAAiF;AACjF,iFAAiF;AACjF,MAAM,UAAU,GAAG,CAAC,IAAY,EAAW,EAAE,CAC3C,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,CAAC;AAE/D,SAAS,aAAa,CAAC,KAAc,EAAE,UAAmB;IACxD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;IAClE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;IAChF,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,GAAG,GAA4B,EAAE,CAAC;QACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YACtE,GAAG,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,EAAE,UAAU,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,QAAQ,CAAC,MAAwB;IAC/C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACtB,MAAM,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,CAAC,CAAC;QAChE,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAC9B,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YAClB,IAAI,OAAO,IAAI,KAAK,QAAQ;gBAAE,OAAO,IAAI,CAAC;YAC1C,IAAI,OAAO,CAAC,CAAC,CAAC;gBAAE,OAAO,MAAM,CAAC;YAC9B,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAChB,IAAI,CAAC;oBACH,OAAO,IAAI,CAAC,SAAS,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;gBAChE,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,IAAI,CAAC,CAAC,6DAA6D;gBAC5E,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC,CAAC,CACH,CAAC;QACF,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Where this platform's compute and per-tenant stores physically live (#537-adjacent ops
3
+ * ergonomics): the coordinates a staff surface turns into a link into the provider's own
4
+ * console — this scope's serving script, this tenant's database, this tenant's bucket.
5
+ *
6
+ * Substrate-agnostic on purpose. The control plane already knows a scope's `servingRef`,
7
+ * a store's `ref`, and a version's `deploymentRef`; what it has never told anyone is
8
+ * WHERE those refs resolve. That is one descriptor, not a Cloudflare dependency: this
9
+ * package still holds no provider SDK and no credential (D-34), and the pure adapter
10
+ * simply has no runtime to describe (`provider` absent ⇒ the console shows plain ids).
11
+ */
12
+ export interface PlatformRuntime {
13
+ /** The substrate these refs resolve in. Only Cloudflare exists today; the tag is what
14
+ * lets a consumer refuse to build links for a substrate it does not know. */
15
+ provider: 'cloudflare';
16
+ /** The account the platform's scripts, databases and buckets belong to. */
17
+ accountId: string;
18
+ /** The dispatch namespace pushed verticals run in (WfP) — where a `servingRef` or a
19
+ * version's `deploymentRef` is a script name. */
20
+ dispatchNamespace: string;
21
+ }
22
+ //# sourceMappingURL=platform-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-runtime.d.ts","sourceRoot":"","sources":["../src/platform-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,eAAe;IAC9B;kFAC8E;IAC9E,QAAQ,EAAE,YAAY,CAAC;IACvB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB;sDACkD;IAClD,iBAAiB,EAAE,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=platform-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-runtime.js","sourceRoot":"","sources":["../src/platform-runtime.ts"],"names":[],"mappings":""}