@substrat-run/control-plane-api 0.79.0 → 0.81.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.map +1 -1
- package/dist/api.js +131 -2
- package/dist/api.js.map +1 -1
- package/dist/tenant-stores.d.ts +74 -2
- package/dist/tenant-stores.d.ts.map +1 -1
- package/dist/tenant-stores.js +134 -49
- package/dist/tenant-stores.js.map +1 -1
- package/dist/vertical-client.d.ts +6 -0
- package/dist/vertical-client.d.ts.map +1 -1
- package/dist/vertical-client.js.map +1 -1
- package/package.json +6 -6
package/dist/api.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAuC5B,OAAO,KAAK,EACV,UAAU,EACV,kBAAkB,EAClB,wBAAwB,EACxB,oBAAoB,EACpB,eAAe,EAGf,eAAe,EAMhB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGvE,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3E,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAiBrF,OAAO,KAAK,EAEV,gBAAgB,EAChB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -10,7 +10,7 @@ import { mapError } from './errors.js';
|
|
|
10
10
|
import { maskDump, maskRecords } from './mask.js';
|
|
11
11
|
import { openDump, sealDump } from './seal.js';
|
|
12
12
|
import { assertSandboxContract, deployManifest, storedDeployManifest, deploymentRefFor, stableDeploymentRefFor, nextMigrationTag, upstreamStatusOf, } from './deploy.js';
|
|
13
|
-
import { blobStoreBindings, collectBlobStoreHandles, collectTenantStoreHandles, tenantStoreBindings, } from './tenant-stores.js';
|
|
13
|
+
import { backfillDeclaredStores, blobStoreBindings, collectBlobStoreHandles, collectTenantStoreHandles, missingStoresForTenant, tenantStoreBindings, } from './tenant-stores.js';
|
|
14
14
|
import { mintPushToken, pushActorFor } from './push-token.js';
|
|
15
15
|
import { namespacesForScript } from './do-namespaces.js';
|
|
16
16
|
import { backupDirectoryIfDue } from './directory-backup.js';
|
|
@@ -1128,6 +1128,15 @@ export function createControlPlaneApi(options) {
|
|
|
1128
1128
|
// a new scope-DO route. `roleProjectionEmpty` on an active scope is the flag a console
|
|
1129
1129
|
// fleet view raises; a scope whose roles live off-DO (adapter-sqlite's directory) reports
|
|
1130
1130
|
// a null count and is not flagged.
|
|
1131
|
+
//
|
|
1132
|
+
// `missingStores` (#825) is the second silent condition, and the same shape of failure:
|
|
1133
|
+
// per-tenant stores are minted in the TENANT-creation lifecycle, so a tenant that predates
|
|
1134
|
+
// a newly declared `runtimeNeeds` store never gets one — the vertical's declaration and the
|
|
1135
|
+
// tenant's ledger simply disagree, forever, and the only signal was a runtime throw at
|
|
1136
|
+
// first use (an attachment upload refusing in production, long after the deploy that
|
|
1137
|
+
// introduced the need). Comparing DECLARED against MINTED here makes a scope that cannot
|
|
1138
|
+
// serve its own declared surface report unhealthy at the moment the need ships, and
|
|
1139
|
+
// re-provisioning the scope is the repair (that path now mints what this reports).
|
|
1131
1140
|
app.get('/tenants/:tenantId/scopes/:scopeId/health', async (c) => {
|
|
1132
1141
|
const tenantId = tenantIdSchema.parse(c.req.param('tenantId'));
|
|
1133
1142
|
const scopeId = scopeIdSchema.parse(c.req.param('scopeId'));
|
|
@@ -1146,12 +1155,21 @@ export function createControlPlaneApi(options) {
|
|
|
1146
1155
|
const roles = tables.find((t) => t.name === '_substrat_roles');
|
|
1147
1156
|
const roleCount = roles ? roles.rowCount : null;
|
|
1148
1157
|
const roleProjectionEmpty = scope.status === 'active' && roleCount === 0;
|
|
1158
|
+
const missingStores = scope.vertical
|
|
1159
|
+
? await missingStoresForTenant({
|
|
1160
|
+
host: options.host,
|
|
1161
|
+
actor: c.get('actor'),
|
|
1162
|
+
slug: scope.vertical,
|
|
1163
|
+
tenantId,
|
|
1164
|
+
})
|
|
1165
|
+
: [];
|
|
1149
1166
|
return c.json({
|
|
1150
1167
|
scopeId,
|
|
1151
1168
|
status: scope.status,
|
|
1152
1169
|
servingRef: scope.servingRef ?? null,
|
|
1153
1170
|
roleCount,
|
|
1154
1171
|
roleProjectionEmpty,
|
|
1172
|
+
missingStores,
|
|
1155
1173
|
});
|
|
1156
1174
|
});
|
|
1157
1175
|
/**
|
|
@@ -1230,6 +1248,33 @@ export function createControlPlaneApi(options) {
|
|
|
1230
1248
|
const connectionKeys = scope.vertical
|
|
1231
1249
|
? await admin.connectionSealingKeys(tenantId, scope.vertical)
|
|
1232
1250
|
: [];
|
|
1251
|
+
// #825: the BACKFILL seam for per-tenant stores. Minting lived only in the
|
|
1252
|
+
// tenant-creation lifecycle, so a tenant created before its vertical declared a
|
|
1253
|
+
// `runtimeNeeds` store never got one and nothing ever gave it one — the vertical then
|
|
1254
|
+
// fails at first use, in production, with nothing an operator can reach. This is the
|
|
1255
|
+
// lever they already reach for (`substrat scope provision`, the idempotent repair), so
|
|
1256
|
+
// it mints and attaches what the serving version declares before reconciling. Safe to
|
|
1257
|
+
// re-run by construction: both collectors are idempotent on (tenant, vertical, binding)
|
|
1258
|
+
// and the binding attach is additive, so a scope whose stores already exist re-resolves
|
|
1259
|
+
// the same refs and changes nothing.
|
|
1260
|
+
const tenantStores = scope.vertical
|
|
1261
|
+
? await collectTenantStoreHandles({
|
|
1262
|
+
host: options.host,
|
|
1263
|
+
actor,
|
|
1264
|
+
slug: scope.vertical,
|
|
1265
|
+
tenantId,
|
|
1266
|
+
patchBindings: options.patchScriptBindings,
|
|
1267
|
+
})
|
|
1268
|
+
: [];
|
|
1269
|
+
if (scope.vertical) {
|
|
1270
|
+
await collectBlobStoreHandles({
|
|
1271
|
+
host: options.host,
|
|
1272
|
+
actor,
|
|
1273
|
+
slug: scope.vertical,
|
|
1274
|
+
tenantId,
|
|
1275
|
+
patchBindings: options.patchScriptBindings,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1233
1278
|
try {
|
|
1234
1279
|
const result = await vertical.reconcileInstance({
|
|
1235
1280
|
tenantId,
|
|
@@ -1238,6 +1283,10 @@ export function createControlPlaneApi(options) {
|
|
|
1238
1283
|
identityLinks,
|
|
1239
1284
|
connectionGrants,
|
|
1240
1285
|
connectionKeys,
|
|
1286
|
+
// Handed over exactly as at provision: a store minted HERE has never been migrated
|
|
1287
|
+
// by the vertical, so the reconcile must carry it into the same ready-gate — a bound
|
|
1288
|
+
// but unmigrated database fails as loudly as an absent one.
|
|
1289
|
+
...(tenantStores.length ? { tenantStores } : {}),
|
|
1241
1290
|
});
|
|
1242
1291
|
return c.json(result);
|
|
1243
1292
|
}
|
|
@@ -2530,6 +2579,56 @@ export function createControlPlaneApi(options) {
|
|
|
2530
2579
|
: 'v1',
|
|
2531
2580
|
});
|
|
2532
2581
|
};
|
|
2582
|
+
/**
|
|
2583
|
+
* Reconcile the INSTALLED FLEET's per-tenant stores to what the version now being served
|
|
2584
|
+
* declares (#825). Runs on promote, after the in-place serve, because that is the act
|
|
2585
|
+
* that makes a declaration real: until the serve, `verticalServing` still names the old
|
|
2586
|
+
* version and the declaration being backfilled would be the previous one.
|
|
2587
|
+
*
|
|
2588
|
+
* Without this, minting existed only in the tenant-creation lifecycle — a gate every
|
|
2589
|
+
* installed tenant passed long before the declaration was written — so declaring a store
|
|
2590
|
+
* in version N+1 gave it to nobody, and adopting it meant an operator remembering to
|
|
2591
|
+
* re-provision each install by hand. They forget, and the vertical then fails at first
|
|
2592
|
+
* use, in production, arbitrarily long after the deploy that introduced the need.
|
|
2593
|
+
*
|
|
2594
|
+
* Best-effort by construction, and deliberately NOT part of the serve's success: a
|
|
2595
|
+
* minting failure (the platform's Cloudflare credential, a store API refusing) must not
|
|
2596
|
+
* make a promote report failure when the new code is already live and serving. It lands
|
|
2597
|
+
* an ops-failure row, rides back in the promote response so the builder sees it at the
|
|
2598
|
+
* terminal, and the scope's own `/health` keeps reporting the gap until it closes —
|
|
2599
|
+
* `substrat scope provision` remains the per-scope retry.
|
|
2600
|
+
*/
|
|
2601
|
+
const backfillFleetStores = async (c, slug) => {
|
|
2602
|
+
const actor = c.get('actor');
|
|
2603
|
+
try {
|
|
2604
|
+
// Every tenant with an install that can still serve. Archived/reaped scopes are
|
|
2605
|
+
// excluded — minting a bucket for storage that is gone is pure waste.
|
|
2606
|
+
const scopes = await admin.listScopes(actor, {
|
|
2607
|
+
vertical: slug,
|
|
2608
|
+
status: ['provisioning', 'active', 'suspended'],
|
|
2609
|
+
});
|
|
2610
|
+
const tenantIds = [...new Set(scopes.map((s) => s.tenantId))];
|
|
2611
|
+
if (tenantIds.length === 0)
|
|
2612
|
+
return { minted: [] };
|
|
2613
|
+
const minted = await backfillDeclaredStores({
|
|
2614
|
+
host: options.host,
|
|
2615
|
+
actor,
|
|
2616
|
+
slug,
|
|
2617
|
+
tenantIds,
|
|
2618
|
+
patchBindings: options.patchScriptBindings,
|
|
2619
|
+
});
|
|
2620
|
+
if (minted.length) {
|
|
2621
|
+
console.log('stores.backfilled', { slug, count: minted.length });
|
|
2622
|
+
}
|
|
2623
|
+
return { minted };
|
|
2624
|
+
}
|
|
2625
|
+
catch (e) {
|
|
2626
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
2627
|
+
console.error('stores.backfill.failed', { slug, detail: message });
|
|
2628
|
+
recordFailure({ actor, operation: 'promote.store-backfill', vertical: slug, message });
|
|
2629
|
+
return { minted: [], error: message };
|
|
2630
|
+
}
|
|
2631
|
+
};
|
|
2533
2632
|
app.post('/verticals/:slug/channels/:channel/promote', async (c) => {
|
|
2534
2633
|
const p = c.get('principal');
|
|
2535
2634
|
const slug = await resolveVerticalId(c, c.req.param('slug'));
|
|
@@ -2566,6 +2665,7 @@ export function createControlPlaneApi(options) {
|
|
|
2566
2665
|
// uploading first would deploy to live scopes before the acknowledgement check.
|
|
2567
2666
|
// A failed serve is NOT a failed promote: the channel moved (audited), old code
|
|
2568
2667
|
// still serves, and promoting again retries the upload.
|
|
2668
|
+
let backfill = { minted: [] };
|
|
2569
2669
|
if (channel === 'prod') {
|
|
2570
2670
|
try {
|
|
2571
2671
|
await serveVersionInPlace(c.get('actor'), slug, versionId);
|
|
@@ -2575,6 +2675,9 @@ export function createControlPlaneApi(options) {
|
|
|
2575
2675
|
// so a legacy scope's data survives the promote instead of being stranded on a
|
|
2576
2676
|
// fresh per-version script. Retry-safe: nothing rebound these scopes yet.
|
|
2577
2677
|
await adoptAndRebindOwnedScopes(c, slug, versionId);
|
|
2678
|
+
// #825: and mint whatever THIS version newly declares for the tenants already
|
|
2679
|
+
// installed — after the serve, so the declaration being read is the one now live.
|
|
2680
|
+
backfill = await backfillFleetStores(c, slug);
|
|
2578
2681
|
}
|
|
2579
2682
|
catch (e) {
|
|
2580
2683
|
const detail = e instanceof Error ? e.message : String(e);
|
|
@@ -2585,7 +2688,33 @@ export function createControlPlaneApi(options) {
|
|
|
2585
2688
|
}, 502);
|
|
2586
2689
|
}
|
|
2587
2690
|
}
|
|
2588
|
-
|
|
2691
|
+
const promoted = (await admin.listChannels(c.get('actor'), slug)).find((ch) => ch.channel === channel);
|
|
2692
|
+
// The store backfill rides the response only when it has something to say (#825), so
|
|
2693
|
+
// nothing changes for the overwhelming majority of promotes — and when a store WAS
|
|
2694
|
+
// minted, or could not be, the builder reads it at the terminal rather than finding out
|
|
2695
|
+
// from a runtime throw weeks later.
|
|
2696
|
+
//
|
|
2697
|
+
// Named tenants are narrowed to what the caller may see: the sweep is fleet-wide (it has
|
|
2698
|
+
// to be — every install needs the store), but a BUILDER reads only its own tenant's
|
|
2699
|
+
// directory rows everywhere else (K-3, the forced filter on `GET /scopes`), and a promote
|
|
2700
|
+
// report must not be the one place that hands them the tenant ids of everyone who
|
|
2701
|
+
// installed their vertical. The rest is a count, which tells them the fleet was covered
|
|
2702
|
+
// without naming who is in it.
|
|
2703
|
+
const p2 = c.get('principal');
|
|
2704
|
+
const visible = p2.kind === 'builder' ? backfill.minted.filter((m) => m.tenantId === p2.tenantId) : backfill.minted;
|
|
2705
|
+
const otherTenants = new Set(backfill.minted.filter((m) => !visible.includes(m)).map((m) => m.tenantId)).size;
|
|
2706
|
+
return c.json({
|
|
2707
|
+
...promoted,
|
|
2708
|
+
...(backfill.minted.length || backfill.error
|
|
2709
|
+
? {
|
|
2710
|
+
storeBackfill: {
|
|
2711
|
+
minted: visible,
|
|
2712
|
+
...(otherTenants ? { otherTenants } : {}),
|
|
2713
|
+
...(backfill.error ? { error: backfill.error } : {}),
|
|
2714
|
+
},
|
|
2715
|
+
}
|
|
2716
|
+
: {}),
|
|
2717
|
+
});
|
|
2589
2718
|
});
|
|
2590
2719
|
// The deploy seam (self-serve-deploy.md): a `substrat push` uploads a built bundle
|
|
2591
2720
|
// here. The order is upload → record, deliberately: a failed record leaves an
|