@substrat-run/control-plane-api 0.81.0 → 0.83.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 +10 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +81 -7
- package/dist/api.js.map +1 -1
- package/dist/connection-grants.d.ts +65 -0
- package/dist/connection-grants.d.ts.map +1 -0
- package/dist/connection-grants.js +0 -0
- package/dist/connection-grants.js.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +22 -9
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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 +1 -0
- package/dist/vertical-client.js.map +1 -1
- package/package.json +6 -6
package/dist/api.d.ts
CHANGED
|
@@ -55,6 +55,16 @@ export interface ControlPlaneApiOptions {
|
|
|
55
55
|
* the same Workers-for-Platforms swap later.
|
|
56
56
|
*/
|
|
57
57
|
verticals?: Record<string, VerticalClient>;
|
|
58
|
+
/**
|
|
59
|
+
* The standing grants each connector declares, by provider (#726 gap 2) — e.g.
|
|
60
|
+
* `{ scrive: SCRIVE_CONNECTION_GRANTS }`.
|
|
61
|
+
*
|
|
62
|
+
* Every reconcile heals a connection toward this floor before gathering, which is what
|
|
63
|
+
* makes a missing capability repairable by a PUSH rather than by re-typing a working
|
|
64
|
+
* credential. Absent ⇒ nothing is healed and the gather behaves exactly as before, so a
|
|
65
|
+
* host that configures no connectors is unaffected.
|
|
66
|
+
*/
|
|
67
|
+
connectorGrants?: Readonly<Record<string, readonly string[]>>;
|
|
58
68
|
/**
|
|
59
69
|
* Resolves a vertical dynamically — the dispatch swap for provisioning (orchestration.md
|
|
60
70
|
* §5.4), the mirror of the router's `verticalFor`. Given a slug, the host looks up the
|
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;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|
package/dist/api.js
CHANGED
|
@@ -3,6 +3,7 @@ import { adminAction, ASSET_PART_PREFIX, assetHash, channelName, connectionActiv
|
|
|
3
3
|
import { migrationProgress, ulid } from '@substrat-run/kernel';
|
|
4
4
|
import { TENANT_HEADER } from './auth.js';
|
|
5
5
|
import { connectionGrantsForScope } from './vertical-client.js';
|
|
6
|
+
import { reconcileConnectionGrants } from './connection-grants.js';
|
|
6
7
|
import { ConnectionRelayError, relayConnectionUpsert } from './connection-relay.js';
|
|
7
8
|
import { ControlPlaneError } from './client.js';
|
|
8
9
|
import { provisionSiblingScope } from './platform-drain.js';
|
|
@@ -1240,6 +1241,21 @@ export function createControlPlaneApi(options) {
|
|
|
1240
1241
|
// #592: connection grants ride the same authoritative gather — the back-fill for a
|
|
1241
1242
|
// scope provisioned before `grantToConnection` ran, and how a revoked connection's
|
|
1242
1243
|
// grants stop being delivered (they are absent from the directory's live rows).
|
|
1244
|
+
// #726 gap 2: heal FIRST, gather second. The gather only ever delivered grants that
|
|
1245
|
+
// already existed as directory rows, so a capability the connector requires and the
|
|
1246
|
+
// connection never received was repairable only by re-submitting the credential. This
|
|
1247
|
+
// is the operator's existing lever (`substrat scope provision`, the idempotent
|
|
1248
|
+
// repair), so a missing grant is now fixed by the same command that fixes everything
|
|
1249
|
+
// else — and by a push, once a connector's declaration changes.
|
|
1250
|
+
//
|
|
1251
|
+
// Best-effort: healing reaches the directory and must never take the reconcile down
|
|
1252
|
+
// with it. A failure here leaves exactly the behaviour that shipped before it existed.
|
|
1253
|
+
try {
|
|
1254
|
+
await reconcileConnectionGrants({ admin, actor, declared: options.connectorGrants ?? {} }, tenantId, scope.vertical);
|
|
1255
|
+
}
|
|
1256
|
+
catch {
|
|
1257
|
+
// The next reconcile tries again; the read-back still shows what is missing.
|
|
1258
|
+
}
|
|
1243
1259
|
const connectionGrants = connectionGrantsForScope(await admin.listConnectionGrants(actor, tenantId), scope.vertical, scopeId);
|
|
1244
1260
|
// #687: connection sealing keys ride the same gather — the back-fill for a scope
|
|
1245
1261
|
// provisioned before its connection existed, and the channel a connection made AFTER
|
|
@@ -1257,23 +1273,63 @@ export function createControlPlaneApi(options) {
|
|
|
1257
1273
|
// re-run by construction: both collectors are idempotent on (tenant, vertical, binding)
|
|
1258
1274
|
// and the binding attach is additive, so a scope whose stores already exist re-resolves
|
|
1259
1275
|
// the same refs and changes nothing.
|
|
1276
|
+
//
|
|
1277
|
+
// BEST-EFFORT, and each substrate independently (#828). Minting reaches Cloudflare, so
|
|
1278
|
+
// it fails for reasons that have nothing to do with this scope — a credential missing a
|
|
1279
|
+
// permission, a store API refusing, a client the deployment never configured. Letting
|
|
1280
|
+
// that throw took the whole call down, which cost the caller the RECONCILE they came
|
|
1281
|
+
// for: the owner re-grant and role re-projection that is this lever's original job
|
|
1282
|
+
// (#332) and that has no relation to stores. It also answered a bare 500, so the
|
|
1283
|
+
// operator could not tell a missing credential from a bad scope id.
|
|
1284
|
+
//
|
|
1285
|
+
// This is the posture the promote-path backfill already has (`backfillFleetStores`) and
|
|
1286
|
+
// for the same reason: the repair must report what it could not do, not refuse to do
|
|
1287
|
+
// the rest. The two paths now agree. What does NOT get this treatment is a NEW install
|
|
1288
|
+
// (`POST /verticals/:slug/instances`, below): there the store is handed into the K-31
|
|
1289
|
+
// ready-gate and a fresh install without it is simply broken, so a failed mint must
|
|
1290
|
+
// fail the provision rather than hand back a half-built scope.
|
|
1291
|
+
const storeErrors = [];
|
|
1292
|
+
const mintBestEffort = async (stage, mint) => {
|
|
1293
|
+
try {
|
|
1294
|
+
return await mint();
|
|
1295
|
+
}
|
|
1296
|
+
catch (e) {
|
|
1297
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1298
|
+
console.error('stores.provision.failed', { tenantId, scopeId, vertical: scope.vertical, stage, detail: message });
|
|
1299
|
+
recordFailure({
|
|
1300
|
+
actor,
|
|
1301
|
+
operation: 'scope.provision.stores',
|
|
1302
|
+
stage,
|
|
1303
|
+
tenantId,
|
|
1304
|
+
scopeId,
|
|
1305
|
+
vertical: scope.vertical,
|
|
1306
|
+
status: e instanceof ControlPlaneError ? e.status : null,
|
|
1307
|
+
message,
|
|
1308
|
+
});
|
|
1309
|
+
storeErrors.push(message);
|
|
1310
|
+
return undefined;
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
// Independently, so a D1 refusal does not silently skip the R2 mint (and vice versa):
|
|
1314
|
+
// a scope declaring both would otherwise report one fault, get half its stores, and
|
|
1315
|
+
// need a second run to discover the other.
|
|
1260
1316
|
const tenantStores = scope.vertical
|
|
1261
|
-
? await collectTenantStoreHandles({
|
|
1317
|
+
? ((await mintBestEffort('tenant-stores', () => collectTenantStoreHandles({
|
|
1262
1318
|
host: options.host,
|
|
1263
1319
|
actor,
|
|
1264
1320
|
slug: scope.vertical,
|
|
1265
1321
|
tenantId,
|
|
1266
1322
|
patchBindings: options.patchScriptBindings,
|
|
1267
|
-
})
|
|
1323
|
+
}))) ?? [])
|
|
1268
1324
|
: [];
|
|
1269
1325
|
if (scope.vertical) {
|
|
1270
|
-
await collectBlobStoreHandles({
|
|
1326
|
+
await mintBestEffort('blob-stores', () => collectBlobStoreHandles({
|
|
1271
1327
|
host: options.host,
|
|
1272
1328
|
actor,
|
|
1273
1329
|
slug: scope.vertical,
|
|
1274
1330
|
tenantId,
|
|
1275
1331
|
patchBindings: options.patchScriptBindings,
|
|
1276
|
-
});
|
|
1332
|
+
}));
|
|
1277
1333
|
}
|
|
1278
1334
|
try {
|
|
1279
1335
|
const result = await vertical.reconcileInstance({
|
|
@@ -1288,11 +1344,20 @@ export function createControlPlaneApi(options) {
|
|
|
1288
1344
|
// but unmigrated database fails as loudly as an absent one.
|
|
1289
1345
|
...(tenantStores.length ? { tenantStores } : {}),
|
|
1290
1346
|
});
|
|
1291
|
-
|
|
1347
|
+
// The reconcile succeeded; a store that did not mint rides back as a diagnosis
|
|
1348
|
+
// (`substrat scope provision` prints it, and `/health` keeps reporting the gap
|
|
1349
|
+
// until it closes). Absent when nothing failed, so the response is unchanged for
|
|
1350
|
+
// every scope that has no declared store or already has them all.
|
|
1351
|
+
return c.json(storeErrors.length ? { ...result, storeError: storeErrors.join('; ') } : result);
|
|
1292
1352
|
}
|
|
1293
1353
|
catch (e) {
|
|
1294
|
-
if (e instanceof ControlPlaneError)
|
|
1295
|
-
|
|
1354
|
+
if (e instanceof ControlPlaneError) {
|
|
1355
|
+
// Both halves failed. The store fault is usually the CAUSE (an unmigrated or
|
|
1356
|
+
// unbound store is exactly what makes a vertical refuse its own reconcile), so
|
|
1357
|
+
// it must not be the half that gets dropped.
|
|
1358
|
+
const detail = storeErrors.length ? `${e.message} (stores: ${storeErrors.join('; ')})` : e.message;
|
|
1359
|
+
return c.json({ error: detail }, e.status);
|
|
1360
|
+
}
|
|
1296
1361
|
throw e;
|
|
1297
1362
|
}
|
|
1298
1363
|
});
|
|
@@ -2175,6 +2240,15 @@ export function createControlPlaneApi(options) {
|
|
|
2175
2240
|
// install provisioned after `grantToConnection` holds the same `connection:<id>` tuple
|
|
2176
2241
|
// as one provisioned before it, and the connector return path works without a human
|
|
2177
2242
|
// replaying grants per install.
|
|
2243
|
+
// #726 gap 2: heal before gathering here too, so a NEW install of a vertical whose
|
|
2244
|
+
// connector has since declared another grant receives it — rather than inheriting the
|
|
2245
|
+
// gap the tenant's first install was created with.
|
|
2246
|
+
try {
|
|
2247
|
+
await reconcileConnectionGrants({ admin, actor: c.get('actor'), declared: options.connectorGrants ?? {} }, input.tenantId, slug);
|
|
2248
|
+
}
|
|
2249
|
+
catch {
|
|
2250
|
+
// Best-effort, exactly as on the reconcile route.
|
|
2251
|
+
}
|
|
2178
2252
|
const connectionGrants = connectionGrantsForScope(await admin.listConnectionGrants(c.get('actor'), input.tenantId), slug, input.scopeId);
|
|
2179
2253
|
// #687: and the public sealing keys of this tenant's live connections for the same
|
|
2180
2254
|
// vertical, so the new install can seal a value TO a connector from its first
|