@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,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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/control-plane-api",
3
- "version": "0.49.0",
3
+ "version": "0.51.0",
4
4
  "description": "HTTP surface over HostAdmin — the audited control-plane transport (control-plane.md §4.5)",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -23,20 +23,20 @@
23
23
  ],
24
24
  "dependencies": {
25
25
  "hono": "^4.6.0",
26
- "@substrat-run/contracts": "^0.49.0",
27
- "@substrat-run/kernel": "^0.49.0",
28
- "@substrat-run/psl": "^0.2.0"
26
+ "@substrat-run/contracts": "^0.51.0",
27
+ "@substrat-run/psl": "^0.2.0",
28
+ "@substrat-run/kernel": "^0.51.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@hono/node-server": "^1.13.0",
32
- "@types/better-sqlite3": "^7.6.0",
32
+ "@types/better-sqlite3": "^9.6.0",
33
33
  "better-auth": "^1.6.23",
34
- "better-sqlite3": "^12.0.0",
34
+ "better-sqlite3": "^13.0.3",
35
35
  "hono": "^4.6.0",
36
36
  "tsx": "^4.19.0",
37
37
  "typescript": "^7.0.0",
38
38
  "vitest": "^3.0.0",
39
- "@substrat-run/adapter-sqlite": "^0.49.0"
39
+ "@substrat-run/adapter-sqlite": "^0.51.0"
40
40
  },
41
41
  "publishConfig": {
42
42
  "access": "public"