@smithers-orchestrator/control-plane 0.25.0 → 0.25.2
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/package.json +2 -2
- package/src/index.js +50 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/control-plane",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.2",
|
|
4
4
|
"description": "Durable organization, project, billing, usage, secret-reference, and audit primitives for hosted Smithers control planes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"src/"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@smithers-orchestrator/errors": "0.25.
|
|
23
|
+
"@smithers-orchestrator/errors": "0.25.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/index.js
CHANGED
|
@@ -19,6 +19,12 @@ import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
|
|
|
19
19
|
|
|
20
20
|
const SLUG_RE = /^(?:[a-z0-9]|[a-z0-9][a-z0-9-]{0,62}[a-z0-9])$/;
|
|
21
21
|
const ID_RE = /^[A-Za-z0-9:_-]{1,128}$/;
|
|
22
|
+
// The project_key column folds org-wide (project_id IS NULL) rows under this
|
|
23
|
+
// sentinel. ID_RE happens to accept it, so a project literally named "__org__"
|
|
24
|
+
// would collide with the org-wide scope in the (org_id, project_key, …) primary
|
|
25
|
+
// keys — silently overwriting and cross-leaking org-wide secrets/usage-limits.
|
|
26
|
+
// It is therefore a reserved id, rejected by the id validators below.
|
|
27
|
+
const ORG_WIDE_SENTINEL = "__org__";
|
|
22
28
|
const USAGE_LIMIT_PERIODS = new Map([
|
|
23
29
|
["daily", 24 * 60 * 60 * 1000],
|
|
24
30
|
["weekly", 7 * 24 * 60 * 60 * 1000],
|
|
@@ -180,15 +186,35 @@ CREATE INDEX IF NOT EXISTS _smithers_cp_audit_org_time_idx
|
|
|
180
186
|
/**
|
|
181
187
|
* @param {ControlPlaneSqlite} sqlite
|
|
182
188
|
*/
|
|
189
|
+
function tableExists(sqlite, name) {
|
|
190
|
+
return Boolean(sqlite.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1").get(name));
|
|
191
|
+
}
|
|
192
|
+
|
|
183
193
|
function migrateSecretRefsProjectKey(sqlite) {
|
|
194
|
+
const legacyExists = tableExists(sqlite, "_smithers_cp_secret_refs_legacy");
|
|
184
195
|
const columns = sqlite.query("PRAGMA table_info(_smithers_cp_secret_refs)").all();
|
|
185
|
-
|
|
196
|
+
const hasProjectKey = columns.some((column) => String(column.name) === "project_key");
|
|
197
|
+
// Already migrated and no interrupted migration to finish.
|
|
198
|
+
if (hasProjectKey && !legacyExists) {
|
|
186
199
|
return;
|
|
187
200
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
201
|
+
// Run the whole migration atomically (SQLite DDL is transactional), AND make
|
|
202
|
+
// it self-healing. The previous version ran RENAME -> CREATE -> INSERT -> DROP
|
|
203
|
+
// as four auto-committed statements: a crash between RENAME and DROP left the
|
|
204
|
+
// data in `_legacy` while `ensureControlPlaneTables` (which already ran its
|
|
205
|
+
// `CREATE TABLE IF NOT EXISTS` for the project_key shape) recreated an EMPTY
|
|
206
|
+
// table. Gating recovery on the column alone then short-circuited and orphaned
|
|
207
|
+
// every secret ref forever. Keying off the legacy table too finishes the copy.
|
|
208
|
+
sqlite.transaction(() => {
|
|
209
|
+
if (!hasProjectKey) {
|
|
210
|
+
// Fresh migration from the pre-project_key schema: move it aside so we
|
|
211
|
+
// can recreate it with the new shape. (A pre-existing `_legacy` here
|
|
212
|
+
// would be a doubly-corrupt state; let the RENAME fail loudly rather
|
|
213
|
+
// than silently guess which table holds the truth.)
|
|
214
|
+
sqlite.exec(`ALTER TABLE _smithers_cp_secret_refs RENAME TO _smithers_cp_secret_refs_legacy;`);
|
|
215
|
+
}
|
|
216
|
+
sqlite.exec(`
|
|
217
|
+
CREATE TABLE IF NOT EXISTS _smithers_cp_secret_refs (
|
|
192
218
|
org_id TEXT NOT NULL,
|
|
193
219
|
project_key TEXT NOT NULL,
|
|
194
220
|
project_id TEXT,
|
|
@@ -208,7 +234,7 @@ INSERT OR REPLACE INTO _smithers_cp_secret_refs (
|
|
|
208
234
|
)
|
|
209
235
|
SELECT
|
|
210
236
|
org_id,
|
|
211
|
-
COALESCE(project_id, '
|
|
237
|
+
COALESCE(project_id, '${ORG_WIDE_SENTINEL}') AS project_key,
|
|
212
238
|
project_id,
|
|
213
239
|
name,
|
|
214
240
|
provider,
|
|
@@ -221,6 +247,7 @@ ORDER BY created_at_ms;
|
|
|
221
247
|
|
|
222
248
|
DROP TABLE _smithers_cp_secret_refs_legacy;
|
|
223
249
|
`);
|
|
250
|
+
})();
|
|
224
251
|
}
|
|
225
252
|
|
|
226
253
|
/**
|
|
@@ -243,6 +270,9 @@ function optionalId(field, value) {
|
|
|
243
270
|
if (!ID_RE.test(id)) {
|
|
244
271
|
throw new SmithersError("INVALID_INPUT", `${field} must match ${ID_RE}.`, { field });
|
|
245
272
|
}
|
|
273
|
+
if (id === ORG_WIDE_SENTINEL) {
|
|
274
|
+
throw new SmithersError("INVALID_INPUT", `${field} must not be the reserved value "${ORG_WIDE_SENTINEL}".`, { field });
|
|
275
|
+
}
|
|
246
276
|
return id;
|
|
247
277
|
}
|
|
248
278
|
|
|
@@ -255,6 +285,9 @@ function requiredId(field, value) {
|
|
|
255
285
|
if (!ID_RE.test(id)) {
|
|
256
286
|
throw new SmithersError("INVALID_INPUT", `${field} must match ${ID_RE}.`, { field });
|
|
257
287
|
}
|
|
288
|
+
if (id === ORG_WIDE_SENTINEL) {
|
|
289
|
+
throw new SmithersError("INVALID_INPUT", `${field} must not be the reserved value "${ORG_WIDE_SENTINEL}".`, { field });
|
|
290
|
+
}
|
|
258
291
|
return id;
|
|
259
292
|
}
|
|
260
293
|
|
|
@@ -340,7 +373,7 @@ function usageLimitPeriod(value) {
|
|
|
340
373
|
* @param {string | null} projectId
|
|
341
374
|
*/
|
|
342
375
|
function projectKey(projectId) {
|
|
343
|
-
return projectId ??
|
|
376
|
+
return projectId ?? ORG_WIDE_SENTINEL;
|
|
344
377
|
}
|
|
345
378
|
|
|
346
379
|
/**
|
|
@@ -994,13 +1027,19 @@ WHERE org_id = ? AND metric = ? AND unit = ? AND observed_at_ms >= ? AND observe
|
|
|
994
1027
|
const provider = nonEmptyString("provider", input.provider);
|
|
995
1028
|
const ref = nonEmptyString("ref", input.ref);
|
|
996
1029
|
const createdBy = input.createdBy ? requiredId("createdBy", input.createdBy) : null;
|
|
997
|
-
|
|
998
|
-
DELETE
|
|
999
|
-
|
|
1000
|
-
`).run(orgId, secretProjectKey, name);
|
|
1030
|
+
// Atomic upsert (matches every other replace-on-write method). The prior
|
|
1031
|
+
// DELETE-then-INSERT was two statements: a crash between them destroyed the
|
|
1032
|
+
// existing secret ref outright.
|
|
1001
1033
|
this.sqlite.query(`
|
|
1002
1034
|
INSERT INTO _smithers_cp_secret_refs (org_id, project_key, project_id, name, provider, ref, created_by, created_at_ms, rotated_at_ms)
|
|
1003
1035
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1036
|
+
ON CONFLICT(org_id, project_key, name) DO UPDATE SET
|
|
1037
|
+
project_id = excluded.project_id,
|
|
1038
|
+
provider = excluded.provider,
|
|
1039
|
+
ref = excluded.ref,
|
|
1040
|
+
created_by = excluded.created_by,
|
|
1041
|
+
created_at_ms = excluded.created_at_ms,
|
|
1042
|
+
rotated_at_ms = excluded.rotated_at_ms
|
|
1004
1043
|
`).run(
|
|
1005
1044
|
orgId,
|
|
1006
1045
|
secretProjectKey,
|