@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 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":"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"}
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
- return c.json(result);
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
- return c.json({ error: e.message }, e.status);
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