@substrat-run/control-plane-api 0.49.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +27 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +332 -4
- package/dist/api.js.map +1 -1
- package/dist/backups.d.ts +93 -0
- package/dist/backups.d.ts.map +1 -0
- package/dist/backups.js +30 -0
- package/dist/backups.js.map +1 -0
- package/dist/directory-backup.d.ts +57 -0
- package/dist/directory-backup.d.ts.map +1 -0
- package/dist/directory-backup.js +68 -0
- package/dist/directory-backup.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mask.d.ts +15 -0
- package/dist/mask.d.ts.map +1 -1
- package/dist/mask.js +25 -1
- package/dist/mask.js.map +1 -1
- package/dist/r2-backups.d.ts +24 -0
- package/dist/r2-backups.d.ts.map +1 -0
- package/dist/r2-backups.js +175 -0
- package/dist/r2-backups.js.map +1 -0
- package/package.json +6 -6
|
@@ -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"}
|
package/dist/backups.js
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,4 +18,8 @@ export type { CustomHostnameProvisioner, CustomHostnameProvisionerOptions, Custo
|
|
|
18
18
|
export type { ObservabilityReader, ServiceMetricsRow, RecentLogEvent } from './observability.js';
|
|
19
19
|
export { createCfObservabilityReader } from './cf-observability.js';
|
|
20
20
|
export type { CfObservabilityOptions } from './cf-observability.js';
|
|
21
|
+
export type { ScopeBackup, ScopeBackupStore, DirectoryBackup, DirectoryBackupStore, } from './backups.js';
|
|
22
|
+
export { createR2BackupStore, createR2DirectoryBackupStore } from './r2-backups.js';
|
|
23
|
+
export { backupDirectoryIfDue } from './directory-backup.js';
|
|
24
|
+
export type { DirectoryBackupOptions, DirectoryBackupResult } from './directory-backup.js';
|
|
21
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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":"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;AACpE,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,oBAAoB,GACrB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAIpF,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -9,4 +9,9 @@ export { createWfpUploader, createWfpModulesFetcher, createWfpBindingsPatcher }
|
|
|
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
11
|
export { createCfObservabilityReader } from './cf-observability.js';
|
|
12
|
+
export { createR2BackupStore, createR2DirectoryBackupStore } from './r2-backups.js';
|
|
13
|
+
// #40 — the scheduled directory copy. Exported as a function the CP worker's cron calls,
|
|
14
|
+
// not as a route with a timer behind it: this package stays library-only, exactly as the
|
|
15
|
+
// platform-request drain does (the recurrence has to come from a deployment, #444).
|
|
16
|
+
export { backupDirectoryIfDue } from './directory-backup.js';
|
|
12
17
|
//# 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;AAQ/B,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
|
package/dist/mask.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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,
|
|
1
|
+
{"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cloudflare R2 implementation of the `ScopeBackupStore` seam (#493) — the platform's
|
|
3
|
+
* own backup bucket, bound on the control-plane worker.
|
|
4
|
+
*
|
|
5
|
+
* Structurally typed against the binding (`R2BucketLike`) rather than importing a
|
|
6
|
+
* Cloudflare SDK, the same way `adapter-cloudflare/src/r2.ts` wraps a tenant blob store:
|
|
7
|
+
* this package stays SDK-free and the fake in tests is an object literal.
|
|
8
|
+
*
|
|
9
|
+
* One bucket, held by the PLATFORM, not by the tenant. A tenant-held bucket would be
|
|
10
|
+
* deleted by the very teardown these copies exist to survive.
|
|
11
|
+
*/
|
|
12
|
+
import type { DirectoryBackupStore, ScopeBackupStore } from './backups.js';
|
|
13
|
+
export declare function createR2BackupStore(bucket: unknown): ScopeBackupStore;
|
|
14
|
+
/**
|
|
15
|
+
* The Cloudflare R2 implementation of `DirectoryBackupStore` (#40).
|
|
16
|
+
*
|
|
17
|
+
* Deliberately a separate factory over the SAME binding shape rather than a mode flag on
|
|
18
|
+
* `createR2BackupStore`: the two stores answer different questions and only this one can
|
|
19
|
+
* delete. A deployment may point them at one bucket or two — the prefixes keep them apart
|
|
20
|
+
* either way, and pointing THIS one at a bucket in another account is the upgrade path
|
|
21
|
+
* from "survives losing the directory" to "survives losing the account".
|
|
22
|
+
*/
|
|
23
|
+
export declare function createR2DirectoryBackupStore(bucket: unknown): DirectoryBackupStore;
|
|
24
|
+
//# sourceMappingURL=r2-backups.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2-backups.d.ts","sourceRoot":"","sources":["../src/r2-backups.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH,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"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cloudflare R2 implementation of the `ScopeBackupStore` seam (#493) — the platform's
|
|
3
|
+
* own backup bucket, bound on the control-plane worker.
|
|
4
|
+
*
|
|
5
|
+
* Structurally typed against the binding (`R2BucketLike`) rather than importing a
|
|
6
|
+
* Cloudflare SDK, the same way `adapter-cloudflare/src/r2.ts` wraps a tenant blob store:
|
|
7
|
+
* this package stays SDK-free and the fake in tests is an object literal.
|
|
8
|
+
*
|
|
9
|
+
* One bucket, held by the PLATFORM, not by the tenant. A tenant-held bucket would be
|
|
10
|
+
* deleted by the very teardown these copies exist to survive.
|
|
11
|
+
*/
|
|
12
|
+
import { directoryDump, scopeDump, } from '@substrat-run/contracts';
|
|
13
|
+
/**
|
|
14
|
+
* Keys are `scopes/<tenantId>/<scopeId>/<capturedAt>.json`, so a scope's copies are one
|
|
15
|
+
* prefix list and a tenant's are one level up. The scheme is private to this module —
|
|
16
|
+
* callers address a backup by (tenant, scope, capturedAt) and never build a path.
|
|
17
|
+
*/
|
|
18
|
+
function backupKey(tenantId, scopeId, capturedAt) {
|
|
19
|
+
return `scopes/${tenantId}/${scopeId}/${capturedAt}.json`;
|
|
20
|
+
}
|
|
21
|
+
function scopePrefix(tenantId, scopeId) {
|
|
22
|
+
return `scopes/${tenantId}/${scopeId}/`;
|
|
23
|
+
}
|
|
24
|
+
export function createR2BackupStore(bucket) {
|
|
25
|
+
const r2 = bucket;
|
|
26
|
+
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 } : {}),
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
return backup;
|
|
53
|
+
},
|
|
54
|
+
async list({ tenantId, scopeId }) {
|
|
55
|
+
const prefix = scopePrefix(tenantId, scopeId);
|
|
56
|
+
const out = [];
|
|
57
|
+
let cursor;
|
|
58
|
+
do {
|
|
59
|
+
const page = await r2.list({
|
|
60
|
+
prefix,
|
|
61
|
+
include: ['customMetadata'],
|
|
62
|
+
...(cursor ? { cursor } : {}),
|
|
63
|
+
});
|
|
64
|
+
for (const o of page.objects) {
|
|
65
|
+
const meta = o.customMetadata ?? {};
|
|
66
|
+
// The key is the fallback address: an object written before the metadata
|
|
67
|
+
// existed (or by a manual upload) still lists rather than vanishing.
|
|
68
|
+
const capturedAt = meta.capturedAt ?? o.key.slice(prefix.length).replace(/\.json$/, '');
|
|
69
|
+
out.push({
|
|
70
|
+
tenantId,
|
|
71
|
+
scopeId,
|
|
72
|
+
vertical: meta.vertical ?? null,
|
|
73
|
+
capturedAt,
|
|
74
|
+
size: o.size,
|
|
75
|
+
tables: meta.tables ? Number(meta.tables) : 0,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
cursor = page.truncated ? page.cursor : undefined;
|
|
79
|
+
} while (cursor);
|
|
80
|
+
// Newest first. ISO 8601 sorts lexicographically, which is why `capturedAt` is
|
|
81
|
+
// the key's last segment.
|
|
82
|
+
return out.sort((a, b) => (a.capturedAt < b.capturedAt ? 1 : -1));
|
|
83
|
+
},
|
|
84
|
+
async get({ tenantId, scopeId, capturedAt }) {
|
|
85
|
+
const obj = await r2.get(backupKey(tenantId, scopeId, capturedAt));
|
|
86
|
+
if (!obj)
|
|
87
|
+
return null;
|
|
88
|
+
// 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()));
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Keys are `directory/<capturedAt>.json` — one flat prefix, because there is exactly one
|
|
96
|
+
* directory per deployment and `capturedAt` is a copy's whole address. Sharing a bucket
|
|
97
|
+
* with the scope backups above is safe by construction: `scopes/` and `directory/` cannot
|
|
98
|
+
* collide, and neither prefix is ever built by a caller.
|
|
99
|
+
*/
|
|
100
|
+
function directoryKey(capturedAt) {
|
|
101
|
+
return `directory/${capturedAt}.json`;
|
|
102
|
+
}
|
|
103
|
+
const DIRECTORY_PREFIX = 'directory/';
|
|
104
|
+
/**
|
|
105
|
+
* The Cloudflare R2 implementation of `DirectoryBackupStore` (#40).
|
|
106
|
+
*
|
|
107
|
+
* Deliberately a separate factory over the SAME binding shape rather than a mode flag on
|
|
108
|
+
* `createR2BackupStore`: the two stores answer different questions and only this one can
|
|
109
|
+
* delete. A deployment may point them at one bucket or two — the prefixes keep them apart
|
|
110
|
+
* either way, and pointing THIS one at a bucket in another account is the upgrade path
|
|
111
|
+
* from "survives losing the directory" to "survives losing the account".
|
|
112
|
+
*/
|
|
113
|
+
export function createR2DirectoryBackupStore(bucket) {
|
|
114
|
+
const r2 = bucket;
|
|
115
|
+
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),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return backup;
|
|
135
|
+
},
|
|
136
|
+
async list() {
|
|
137
|
+
const out = [];
|
|
138
|
+
let cursor;
|
|
139
|
+
do {
|
|
140
|
+
const page = await r2.list({
|
|
141
|
+
prefix: DIRECTORY_PREFIX,
|
|
142
|
+
include: ['customMetadata'],
|
|
143
|
+
...(cursor ? { cursor } : {}),
|
|
144
|
+
});
|
|
145
|
+
for (const o of page.objects) {
|
|
146
|
+
const meta = o.customMetadata ?? {};
|
|
147
|
+
// The key is the fallback address, as in the scope store: an object written
|
|
148
|
+
// before the metadata existed still lists rather than vanishing.
|
|
149
|
+
const capturedAt = meta.capturedAt ?? o.key.slice(DIRECTORY_PREFIX.length).replace(/\.json$/, '');
|
|
150
|
+
out.push({
|
|
151
|
+
capturedAt,
|
|
152
|
+
size: o.size,
|
|
153
|
+
tables: meta.tables ? Number(meta.tables) : 0,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
cursor = page.truncated ? page.cursor : undefined;
|
|
157
|
+
} while (cursor);
|
|
158
|
+
// Newest first — ISO 8601 sorts lexicographically, which is why `capturedAt` is
|
|
159
|
+
// the key's last segment. The daily-cadence guard reads element 0.
|
|
160
|
+
return out.sort((a, b) => (a.capturedAt < b.capturedAt ? 1 : -1));
|
|
161
|
+
},
|
|
162
|
+
async get({ capturedAt }) {
|
|
163
|
+
const obj = await r2.get(directoryKey(capturedAt));
|
|
164
|
+
if (!obj)
|
|
165
|
+
return null;
|
|
166
|
+
// Parsed through the contract on the way out, so a corrupted object fails HERE
|
|
167
|
+
// rather than half-way through the restore that trusted it.
|
|
168
|
+
return directoryDump.parse(JSON.parse(await obj.text()));
|
|
169
|
+
},
|
|
170
|
+
async delete({ capturedAt }) {
|
|
171
|
+
await r2.delete(directoryKey(capturedAt));
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=r2-backups.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"r2-backups.js","sourceRoot":"","sources":["../src/r2-backups.ts"],"names":[],"mappings":"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"}
|