@substrat-run/adapter-cloudflare 0.160.0 → 0.162.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/control-plane-do.d.ts +97 -14
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +279 -39
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +111 -26
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +294 -196
- package/dist/host.js.map +1 -1
- package/dist/r2.d.ts.map +1 -1
- package/dist/r2.js +13 -10
- package/dist/r2.js.map +1 -1
- package/dist/scope-do.d.ts +27 -0
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +290 -113
- package/dist/scope-do.js.map +1 -1
- package/package.json +9 -9
package/dist/control-plane-do.js
CHANGED
|
@@ -6,7 +6,7 @@ import { blankSqlComments, boundShapesColumn, executableSqlStatements, installsR
|
|
|
6
6
|
import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, COPY_MOVE_RENEW_SQL, copyMoveRenewParams, PENDING_COPY_KEEP_ALIVE_SQL, pendingCopyKeepAliveParams, copyBackfillParams, copyBackfillRefusal, SUBJECT_ERASURE_CLAIM_SQL, subjectErasureClaimParams, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SERVES_NOW_CONDITION, SERVES_NOW_PREDICATE, servesNowParams, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, } from '@substrat-run/kernel';
|
|
7
7
|
import { opsFailureReferenceWhere } from '@substrat-run/kernel';
|
|
8
8
|
import { doBuiltColumnsOf, doRedactionSql } from './sql.js';
|
|
9
|
-
import { assertReplayableDump, instant, opsFailureFingerprint, ROUTE_SCOPE_HOSTNAMES_MAX, SCOPE_GATE_REASONS, substratError } from '@substrat-run/contracts';
|
|
9
|
+
import { assertReplayableDump, AUTO_ADMISSION_NOTE, instant, opsFailureFingerprint, ROUTE_SCOPE_HOSTNAMES_MAX, SCOPE_GATE_REASONS, substratError } from '@substrat-run/contracts';
|
|
10
10
|
/**
|
|
11
11
|
* The router's per-request directory read (`readRoute`), lifted out so its plan can be asked for
|
|
12
12
|
* (#2005): the scope's hostname set is a correlated subquery on the hot path every request takes,
|
|
@@ -807,6 +807,28 @@ export const REPLIED_METHODS = [
|
|
|
807
807
|
// #2309: a fleet rebuild's lease write refuses with conflict or validation_failed.
|
|
808
808
|
'claimStoreDriverRebuild',
|
|
809
809
|
'advanceStoreDriverRebuild',
|
|
810
|
+
// #2528: the vertical writes that read their row's `before` and their guards in the unit that
|
|
811
|
+
// writes, refusing an unknown vertical or version with not_found and a failed guard with conflict.
|
|
812
|
+
'updateVerticalListed',
|
|
813
|
+
'updateVerticalPublishRequest',
|
|
814
|
+
'updateVerticalInstallsBlocked',
|
|
815
|
+
'updateVerticalTenantProvisioner',
|
|
816
|
+
'updateVerticalEmailSender',
|
|
817
|
+
'deleteVertical',
|
|
818
|
+
'admitVersion',
|
|
819
|
+
'rejectVersion',
|
|
820
|
+
'setVerticalServing',
|
|
821
|
+
// #2526: the role, grant and org writes at a tenant node, each with its admin row in its unit. A
|
|
822
|
+
// DO from before lists none of them, so the current coordinator fails closed against it. That is
|
|
823
|
+
// why the org write is `insertOrg` and not `createOrg` with a trailing row: an older DO lists
|
|
824
|
+
// `createOrg` and would drop the row and create the org.
|
|
825
|
+
'defineRoleOrRefusal',
|
|
826
|
+
'assignTenantRole',
|
|
827
|
+
'writeTenantTuple',
|
|
828
|
+
'writeOrgTenantTuple',
|
|
829
|
+
'writeTenantSystemGrant',
|
|
830
|
+
'grantConnectionInDirectory',
|
|
831
|
+
'insertOrg',
|
|
810
832
|
];
|
|
811
833
|
export class ControlPlaneDO extends DurableObject {
|
|
812
834
|
sql;
|
|
@@ -1340,8 +1362,10 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1340
1362
|
const slugOwner = this.sql
|
|
1341
1363
|
.exec('SELECT tenant_id FROM tenants WHERE slug = ?', slug)
|
|
1342
1364
|
.toArray()[0];
|
|
1365
|
+
// #2556: the refusal never names the owner. Another tenant holds it, and the text reaches
|
|
1366
|
+
// whoever asked (a manager vertical's intent journal among them).
|
|
1343
1367
|
if (slugOwner) {
|
|
1344
|
-
throw substratError('conflict', `tenant slug '${slug}' already taken
|
|
1368
|
+
throw substratError('conflict', `tenant slug '${slug}' is already taken (slugs are unique)`);
|
|
1345
1369
|
}
|
|
1346
1370
|
this.writeAndAudit(() => this.sql.exec(`INSERT INTO tenants (tenant_id, slug, name, status, created_at, provisioned_by_tenant)
|
|
1347
1371
|
VALUES (?, ?, ?, 'active', ?, ?)`, id, slug, name, createdAt, provisionedByTenant).rowsWritten > 0, audit && (() => ({ ...audit, after: this.readTenant(id) })));
|
|
@@ -1851,9 +1875,19 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1851
1875
|
* system-held definition of a key with no system-held definition stored (none at all included): its tenant-level seats are read here, in the unit, and
|
|
1852
1876
|
* `seatedInScopes` carries what the coordinator read off the tenant's scopes. A refusal is answered
|
|
1853
1877
|
* as data, so its text survives the hop, and writes nothing. The current coordinator calls this one
|
|
1854
|
-
* (the same split as `transitionScope` and `transitionScopeOrRefusal`)
|
|
1878
|
+
* (the same split as `transitionScope` and `transitionScopeOrRefusal`), through `reply` (#2526),
|
|
1879
|
+
* with `audit`: the row, its `before` the role read here, commits with the definition. A
|
|
1880
|
+
* coordinator from before #2526 passes none and records its own.
|
|
1855
1881
|
*/
|
|
1856
|
-
defineRoleOrRefusal(tenantId, role, reservation = { reserved: [], declared: [] }, seatedInScopes = false) {
|
|
1882
|
+
defineRoleOrRefusal(tenantId, role, reservation = { reserved: [], declared: [] }, seatedInScopes = false, audit) {
|
|
1883
|
+
return this.ctx.storage.transactionSync(() => {
|
|
1884
|
+
const outcome = this.defineRoleIn(tenantId, role, reservation, seatedInScopes);
|
|
1885
|
+
if (audit && 'before' in outcome)
|
|
1886
|
+
this.recordAdmin({ ...audit, before: outcome.before });
|
|
1887
|
+
return outcome;
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
defineRoleIn(tenantId, role, reservation, seatedInScopes) {
|
|
1857
1891
|
const tenantRoles = this.listRoles({ tenantId }).map((r) => ({
|
|
1858
1892
|
key: r.role_key,
|
|
1859
1893
|
permissions: JSON.parse(r.permissions),
|
|
@@ -1917,21 +1951,67 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
1917
1951
|
};
|
|
1918
1952
|
}
|
|
1919
1953
|
// -- tenant-level tuples (checker rules 1, 2, 4) ----------------------------
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1954
|
+
/**
|
|
1955
|
+
* A tenant tuple, upserted. With `audit` (#2526, `grant` at the tenant node, called through
|
|
1956
|
+
* `reply`), its admin row commits in the same transaction; without, as a coordinator from before
|
|
1957
|
+
* #2526 calls it, and as this object's own units do, it is the bare write.
|
|
1958
|
+
*/
|
|
1959
|
+
writeTenantTuple(tenantId, subject, relation, object, expiresAt, audit) {
|
|
1960
|
+
const write = () => {
|
|
1961
|
+
this.sql.exec(`INSERT OR REPLACE INTO _substrat_tenant_tuples
|
|
1962
|
+
(tenant_id, subject, relation, object, expires_at)
|
|
1963
|
+
VALUES (?, ?, ?, ?, ?)`, tenantId, subject, relation, object, expiresAt);
|
|
1964
|
+
return true;
|
|
1965
|
+
};
|
|
1966
|
+
if (audit)
|
|
1967
|
+
this.writeAndAudit(write, audit);
|
|
1968
|
+
else
|
|
1969
|
+
write();
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* #2526: `assignRole` at the tenant node — the role's reservation judged (`reserved`, what the
|
|
1973
|
+
* coordinator's modules reserve to a system principal: a role mixing them with other keys is
|
|
1974
|
+
* refused, #2430), the tuple and its admin row, in one unit. A refusal is answered as data and
|
|
1975
|
+
* writes nothing. A role not defined here is not judged, as before.
|
|
1976
|
+
*/
|
|
1977
|
+
assignTenantRole(tenantId, principal, roleKey, reserved, audit) {
|
|
1978
|
+
return this.ctx.storage.transactionSync(() => {
|
|
1979
|
+
const role = reserved.length > 0 ? this.getRole(tenantId, roleKey) : undefined;
|
|
1980
|
+
const refused = role ? reservedRoleRefusal(role, new Set(reserved), 'platform') : null;
|
|
1981
|
+
if (refused)
|
|
1982
|
+
return { refused };
|
|
1983
|
+
this.writeTenantTuple(tenantId, `principal:${principal}`, `role:${roleKey}`, `tenant:${tenantId}`, null);
|
|
1984
|
+
this.recordAdmin(audit);
|
|
1985
|
+
return {};
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* #2526: a tenant tuple that names an org — `grantToOrg` at the tenant node, and `addMember` —
|
|
1990
|
+
* with its admin row, in one unit, refused `not_found` when the org is not in the tenant.
|
|
1991
|
+
*/
|
|
1992
|
+
writeOrgTenantTuple(tenantId, orgId, subject, relation, object, expiresAt, audit) {
|
|
1993
|
+
this.writeAndAudit(() => {
|
|
1994
|
+
if (!this.readOrg(tenantId, orgId))
|
|
1995
|
+
throw substratError('not_found', `unknown org ${orgId} in tenant ${tenantId}`);
|
|
1996
|
+
this.writeTenantTuple(tenantId, subject, relation, object, expiresAt);
|
|
1997
|
+
return true;
|
|
1998
|
+
}, audit);
|
|
1924
1999
|
}
|
|
1925
2000
|
/**
|
|
1926
2001
|
* #1743: a tenant-level `system:<module>` grant, refused while the record holds the module
|
|
1927
2002
|
* off on any scope (`scopesSwitchedOffFor`). Synchronous, so the read and the write are one
|
|
1928
|
-
* DO unit. Answers the switched-off scopes; non-empty means nothing was written.
|
|
2003
|
+
* DO unit. Answers the switched-off scopes; non-empty means nothing was written. With `audit`
|
|
2004
|
+
* (#2526, through `reply`), the row commits with the tuple, and a refusal records none.
|
|
1929
2005
|
*/
|
|
1930
|
-
writeTenantSystemGrant(tenantId, moduleId, relation, expiresAt) {
|
|
1931
|
-
|
|
1932
|
-
|
|
2006
|
+
writeTenantSystemGrant(tenantId, moduleId, relation, expiresAt, audit) {
|
|
2007
|
+
let off = [];
|
|
2008
|
+
this.writeAndAudit(() => {
|
|
2009
|
+
off = scopesSwitchedOffFor(this.kernelSql, tenantId, moduleId);
|
|
2010
|
+
if (off.length > 0)
|
|
2011
|
+
return false;
|
|
1933
2012
|
this.writeTenantTuple(tenantId, `system:${moduleId}`, relation, `tenant:${tenantId}`, expiresAt);
|
|
1934
|
-
|
|
2013
|
+
return true;
|
|
2014
|
+
}, audit);
|
|
1935
2015
|
return off;
|
|
1936
2016
|
}
|
|
1937
2017
|
// -- the hostname map (K-26) ------------------------------------------------
|
|
@@ -2218,30 +2298,55 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2218
2298
|
this.sql.exec('UPDATE verticals SET listed = ? WHERE slug = ?', listed, slug);
|
|
2219
2299
|
}
|
|
2220
2300
|
}
|
|
2221
|
-
/**
|
|
2301
|
+
/**
|
|
2302
|
+
* Publish/unpublish a vertical to the public marketplace + resolve any pending request. Listing
|
|
2303
|
+
* refuses a prod version that is auto-admitted (only its author has read it), judged on the
|
|
2304
|
+
* channel as this unit reads it (#2528).
|
|
2305
|
+
*/
|
|
2222
2306
|
updateVerticalListed(slug, listed, audit) {
|
|
2223
|
-
this.
|
|
2224
|
-
|
|
2307
|
+
this.updateVertical(slug, 'listed = ?, publish_requested_at = NULL', [listed], (r) => {
|
|
2308
|
+
if (listed) {
|
|
2309
|
+
const prod = this.sql
|
|
2310
|
+
.exec(`SELECT v.id, v.admission_note FROM vertical_channels c JOIN vertical_versions v ON v.id = c.version_id
|
|
2311
|
+
WHERE c.vertical_slug = ? AND c.channel = 'prod'`, slug)
|
|
2312
|
+
.toArray()[0];
|
|
2313
|
+
if (prod?.admission_note === AUTO_ADMISSION_NOTE) {
|
|
2314
|
+
throw substratError('conflict', `vertical '${slug}' prod version ${prod.id} is auto-admitted (private self-serve) — ` +
|
|
2315
|
+
`a staff admit must vouch for it before listing`);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return { listed: !!r.listed };
|
|
2319
|
+
}, audit);
|
|
2225
2320
|
}
|
|
2226
2321
|
/** Record a builder's pending publish request (marketplace-publish.md §5). */
|
|
2227
2322
|
updateVerticalPublishRequest(slug, requestedAt, audit) {
|
|
2228
|
-
this.
|
|
2229
|
-
.rowsWritten > 0, audit);
|
|
2323
|
+
this.updateVertical(slug, 'publish_requested_at = ?', [requestedAt], () => null, audit);
|
|
2230
2324
|
}
|
|
2231
2325
|
/** Block/unblock new installs of a vertical (staff kill-switch). */
|
|
2232
2326
|
updateVerticalInstallsBlocked(slug, blocked, audit) {
|
|
2233
|
-
this.
|
|
2234
|
-
.rowsWritten > 0, audit);
|
|
2327
|
+
this.updateVertical(slug, 'installs_blocked = ?', [blocked], (r) => ({ installsBlocked: !!r.installs_blocked }), audit);
|
|
2235
2328
|
}
|
|
2236
2329
|
/** Grant/revoke the tenant-provisioner capability (#412, staff grant). */
|
|
2237
2330
|
updateVerticalTenantProvisioner(slug, granted, audit) {
|
|
2238
|
-
this.
|
|
2239
|
-
.rowsWritten > 0, audit);
|
|
2331
|
+
this.updateVertical(slug, 'tenant_provisioner = ?', [granted], (r) => ({ tenantProvisioner: !!r.tenant_provisioner }), audit);
|
|
2240
2332
|
}
|
|
2241
2333
|
/** Grant/revoke the email-sender capability (#303, staff grant). */
|
|
2242
2334
|
updateVerticalEmailSender(slug, granted, audit) {
|
|
2243
|
-
this.
|
|
2244
|
-
|
|
2335
|
+
this.updateVertical(slug, 'email_sender = ?', [granted], (r) => ({ emailSender: !!r.email_sender }), audit);
|
|
2336
|
+
}
|
|
2337
|
+
/**
|
|
2338
|
+
* #2528: one vertical row update whose row reads its `before` (and runs its guard, by throwing)
|
|
2339
|
+
* from the vertical as this unit reads it, so neither can be stale against the write.
|
|
2340
|
+
*/
|
|
2341
|
+
updateVertical(slug, set, args, judge, audit) {
|
|
2342
|
+
let before = null;
|
|
2343
|
+
this.writeAndAudit(() => {
|
|
2344
|
+
const r = this.readVertical(slug);
|
|
2345
|
+
if (!r)
|
|
2346
|
+
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
2347
|
+
before = judge(r);
|
|
2348
|
+
return this.sql.exec(`UPDATE verticals SET ${set} WHERE slug = ?`, ...args, slug).rowsWritten > 0;
|
|
2349
|
+
}, audit && (() => ({ ...audit, before })));
|
|
2245
2350
|
}
|
|
2246
2351
|
/**
|
|
2247
2352
|
* How many scopes a vertical still backs, split by liveness — deleteVertical's refusal
|
|
@@ -2260,8 +2365,10 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2260
2365
|
return { live: r?.live ?? 0, archived: r?.archived ?? 0 };
|
|
2261
2366
|
}
|
|
2262
2367
|
/**
|
|
2263
|
-
* Remove a vertical's registry presence: channels, versions, the row.
|
|
2264
|
-
*
|
|
2368
|
+
* Remove a vertical's registry presence: channels, versions, the row. Refused while any
|
|
2369
|
+
* restorable scope is bound (`countScopesForVertical`): a deleted registry row would strand
|
|
2370
|
+
* those scopes' version pins and routing. That refusal and `audit`'s `before` are read in the
|
|
2371
|
+
* unit that deletes (#2528).
|
|
2265
2372
|
*/
|
|
2266
2373
|
deleteVertical(slug, audit, until) {
|
|
2267
2374
|
// #2501: one transaction, so the row and its ownership period close together. `until` is the
|
|
@@ -2269,7 +2376,21 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2269
2376
|
// optional and trailing so a caller from before it, sending (slug, audit), still closes the
|
|
2270
2377
|
// period, on this DO's own reading of that clock; anything but an instant is refused.
|
|
2271
2378
|
const closedAt = until === undefined ? verticalOwnerNow() : instant.parse(until);
|
|
2379
|
+
let before = null;
|
|
2272
2380
|
this.writeAndAudit(() => {
|
|
2381
|
+
const existing = this.readVertical(slug);
|
|
2382
|
+
if (!existing)
|
|
2383
|
+
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
2384
|
+
// An `archived` scope (a deleted app) still blocks — unarchive can bring it back — but the
|
|
2385
|
+
// refusal names reap/restore, because the app itself is already gone.
|
|
2386
|
+
const bound = this.countScopesForVertical(slug);
|
|
2387
|
+
if (bound.live > 0) {
|
|
2388
|
+
throw substratError('conflict', `vertical '${slug}' still backs ${bound.live} scope(s) — delete or rebind them first`);
|
|
2389
|
+
}
|
|
2390
|
+
if (bound.archived > 0) {
|
|
2391
|
+
throw substratError('conflict', `vertical '${slug}' still backs ${bound.archived} archived scope(s) — reap or restore them first`);
|
|
2392
|
+
}
|
|
2393
|
+
before = { slug, source: existing.source, ownerTenant: existing.owner_tenant };
|
|
2273
2394
|
this.sql.exec('DELETE FROM vertical_channels WHERE vertical_slug = ?', slug);
|
|
2274
2395
|
this.sql.exec('DELETE FROM vertical_channel_history WHERE vertical_slug = ?', slug);
|
|
2275
2396
|
this.sql.exec(`DELETE FROM vertical_version_migrations
|
|
@@ -2281,7 +2402,7 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2281
2402
|
if (changed)
|
|
2282
2403
|
closeVerticalOwnerRow(doRedactionSql(this.sql), slug, closedAt);
|
|
2283
2404
|
return changed;
|
|
2284
|
-
}, audit);
|
|
2405
|
+
}, audit && (() => ({ ...audit, before })));
|
|
2285
2406
|
}
|
|
2286
2407
|
/** #2501: every period of a slug's ownership, oldest first — kernel `vertical-owners.ts`. */
|
|
2287
2408
|
listVerticalOwners(slug) {
|
|
@@ -2312,17 +2433,26 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2312
2433
|
* that carries none to show.
|
|
2313
2434
|
*/
|
|
2314
2435
|
insertVersion(v, audit) {
|
|
2436
|
+
// #2528: whether it self-admits is judged on the vertical as this unit reads it — a PRIVATE
|
|
2437
|
+
// vertical (tenant-owned, not listed) self-admits, noted so the publish seam can tell a staff
|
|
2438
|
+
// vouch from this shortcut — whatever the caller judged from its own earlier read.
|
|
2439
|
+
let admission = v.admission;
|
|
2315
2440
|
this.writeAndAudit(() => {
|
|
2441
|
+
const owning = this.readVertical(v.verticalSlug);
|
|
2442
|
+
if (!owning)
|
|
2443
|
+
throw substratError('not_found', `unknown vertical '${v.verticalSlug}'`);
|
|
2444
|
+
const selfAdmits = owning.owner_tenant !== null && !owning.listed;
|
|
2445
|
+
admission = selfAdmits ? 'admitted' : 'pending';
|
|
2316
2446
|
this.sql.exec(`INSERT INTO vertical_versions
|
|
2317
2447
|
(id, vertical_slug, version, manifest_digest, permission_digest,
|
|
2318
2448
|
migration_digest, deployment_ref, admission, admission_note, manifest_json,
|
|
2319
2449
|
origin_json, migration_count, migrations_split, migration_digest_derived, created_at)
|
|
2320
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?)`, v.id, v.verticalSlug, v.version, v.manifestDigest, v.permissionDigest, v.migrationDigest, v.deploymentRef,
|
|
2450
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?)`, v.id, v.verticalSlug, v.version, v.manifestDigest, v.permissionDigest, v.migrationDigest, v.deploymentRef, admission, selfAdmits ? AUTO_ADMISSION_NOTE : null, v.manifestJson, v.originJson, v.migrations?.length ?? null, v.createdAt);
|
|
2321
2451
|
writeVersionMigrations(this.kernelSql, v.id, v.migrations);
|
|
2322
2452
|
// #2286: the version's scope-store bindings, refusing one moved to another driver, in the insert's transaction.
|
|
2323
2453
|
indexVersionScopeStores(this.kernelSql, v.verticalSlug, v.id, v.manifestJson);
|
|
2324
2454
|
return true;
|
|
2325
|
-
}, audit);
|
|
2455
|
+
}, audit && (() => ({ ...audit, after: { ...audit.after, admission } })));
|
|
2326
2456
|
}
|
|
2327
2457
|
/** Every scope-store binding any version of the vertical declared (#2286); indexes versions not indexed yet. */
|
|
2328
2458
|
verticalScopeStores(slug) {
|
|
@@ -2332,18 +2462,37 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2332
2462
|
readVersionMigrations(id) {
|
|
2333
2463
|
return versionMigrationsOf(this.kernelSql, id);
|
|
2334
2464
|
}
|
|
2335
|
-
/**
|
|
2336
|
-
*
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2465
|
+
/**
|
|
2466
|
+
* Record what the serving script now runs — written only after a successful in-place upload, so
|
|
2467
|
+
* a failed serve leaves the trailing state visible. #2528: `expected` is the serving ref and
|
|
2468
|
+
* version the caller's #2178 gate judged the move from (nulls: nothing served); the write lands
|
|
2469
|
+
* only if the row still names both (`SERVES_NOW_CONDITION`), and `audit`'s `before` is the
|
|
2470
|
+
* serving state this unit read. False when either moved. Optional and trailing, so a caller from
|
|
2471
|
+
* before it still writes unconditionally.
|
|
2472
|
+
*/
|
|
2473
|
+
setVerticalServing(slug, s, audit, expected) {
|
|
2474
|
+
let before = null;
|
|
2475
|
+
let raced = false;
|
|
2476
|
+
this.writeAndAudit(() => {
|
|
2477
|
+
const r = this.readVertical(slug);
|
|
2478
|
+
if (!r)
|
|
2479
|
+
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
2480
|
+
before = r.serving_ref ? { ref: r.serving_ref, versionId: r.serving_version_id } : null;
|
|
2481
|
+
const moved = this.sql.exec(`UPDATE verticals SET serving_ref = ?, serving_version_id = ?,
|
|
2482
|
+
serving_do_classes = ?, serving_migration_tag = ?
|
|
2483
|
+
WHERE ${expected === undefined ? 'slug = ?' : SERVES_NOW_CONDITION}`, s.ref, s.versionId, s.doClassesJson, s.migrationTag, ...(expected === undefined ? [slug] : [slug, expected.ref, expected.versionId])).rowsWritten > 0;
|
|
2484
|
+
raced = !moved;
|
|
2485
|
+
return moved;
|
|
2486
|
+
}, audit && (() => ({ ...audit, before })));
|
|
2487
|
+
return !raced;
|
|
2340
2488
|
}
|
|
2341
|
-
/**
|
|
2489
|
+
/**
|
|
2490
|
+
* One upload's capability record (RFC wire-signing §7, Q8), replacing the script's earlier one. False,
|
|
2491
|
+
* with nothing written and no admin row, for a vertical the registry does not hold: the host answers
|
|
2492
|
+
* that `not_found` on its side of the RPC, so the code survives.
|
|
2493
|
+
*/
|
|
2342
2494
|
recordWireAuth(row, audit) {
|
|
2343
|
-
this.writeAndAudit(() =>
|
|
2344
|
-
writeDeploymentWireAuth(this.kernelSql, row);
|
|
2345
|
-
return true;
|
|
2346
|
-
}, audit);
|
|
2495
|
+
return this.writeAndAudit(() => writeDeploymentWireAuth(this.kernelSql, row), audit);
|
|
2347
2496
|
}
|
|
2348
2497
|
/**
|
|
2349
2498
|
* One page of the stored capability records the filter's reach confines it to (one tenant's
|
|
@@ -2391,9 +2540,55 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2391
2540
|
await deriveVersionMigrationDigest(this.kernelSql, r.id);
|
|
2392
2541
|
return read();
|
|
2393
2542
|
}
|
|
2543
|
+
/** Kept for a coordinator from before #2528, which judged the admission itself. */
|
|
2394
2544
|
setAdmission(id, admission, note, audit) {
|
|
2395
2545
|
this.writeAndAudit(() => this.sql.exec('UPDATE vertical_versions SET admission = ?, admission_note = ? WHERE id = ?', admission, note, id).rowsWritten > 0, audit);
|
|
2396
2546
|
}
|
|
2547
|
+
/**
|
|
2548
|
+
* Admit a version, judged on its admission as this unit reads it (#2528). Idempotent on an
|
|
2549
|
+
* admitted one — except an AUTO-admitted version, which this upgrades to a manual vouch by
|
|
2550
|
+
* clearing the note (what the publish seam requires). A rejected one is refused.
|
|
2551
|
+
*/
|
|
2552
|
+
admitVersion(id, audit) {
|
|
2553
|
+
let row = { before: null, after: null };
|
|
2554
|
+
this.writeAndAudit(() => {
|
|
2555
|
+
const v = this.versionAdmission(id);
|
|
2556
|
+
if (v.admission === 'admitted') {
|
|
2557
|
+
if (v.admission_note !== AUTO_ADMISSION_NOTE)
|
|
2558
|
+
return false;
|
|
2559
|
+
row = { before: { admission: v.admission, note: v.admission_note }, after: { admission: 'admitted', note: null } };
|
|
2560
|
+
}
|
|
2561
|
+
else if (v.admission === 'rejected') {
|
|
2562
|
+
throw substratError('conflict', `version ${id} was rejected — publish a new one`);
|
|
2563
|
+
}
|
|
2564
|
+
else {
|
|
2565
|
+
row = { before: { admission: v.admission }, after: { admission: 'admitted' } };
|
|
2566
|
+
}
|
|
2567
|
+
return this.sql.exec("UPDATE vertical_versions SET admission = 'admitted', admission_note = NULL WHERE id = ?", id)
|
|
2568
|
+
.rowsWritten > 0;
|
|
2569
|
+
}, audit && (() => ({ ...audit, ...row })));
|
|
2570
|
+
}
|
|
2571
|
+
/** Reject a version, judged as `admitVersion` is: an admitted one is refused, a rejected one is a no-op. */
|
|
2572
|
+
rejectVersion(id, note, audit) {
|
|
2573
|
+
let before = null;
|
|
2574
|
+
this.writeAndAudit(() => {
|
|
2575
|
+
const v = this.versionAdmission(id);
|
|
2576
|
+
if (v.admission === 'admitted')
|
|
2577
|
+
throw substratError('conflict', `version ${id} is already admitted — it may be bound`);
|
|
2578
|
+
if (v.admission === 'rejected')
|
|
2579
|
+
return false;
|
|
2580
|
+
before = { admission: v.admission };
|
|
2581
|
+
return this.sql.exec("UPDATE vertical_versions SET admission = 'rejected', admission_note = ? WHERE id = ?", note, id)
|
|
2582
|
+
.rowsWritten > 0;
|
|
2583
|
+
}, audit && (() => ({ ...audit, before, after: { admission: 'rejected', note } })));
|
|
2584
|
+
}
|
|
2585
|
+
versionAdmission(id) {
|
|
2586
|
+
const v = this.sql.exec('SELECT admission, admission_note FROM vertical_versions WHERE id = ?', id)
|
|
2587
|
+
.toArray()[0];
|
|
2588
|
+
if (!v)
|
|
2589
|
+
throw substratError('not_found', `unknown version ${id}`);
|
|
2590
|
+
return v;
|
|
2591
|
+
}
|
|
2397
2592
|
/**
|
|
2398
2593
|
* False when a conditional bind lost its compare-and-set (the host throws, as above). #2347:
|
|
2399
2594
|
* `servingRef` moves the route in the same write, and `expectedServing` holds it to what the
|
|
@@ -2677,6 +2872,15 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
2677
2872
|
this.sql.exec('INSERT INTO orgs (org_id, tenant_id, slug, name, created_at) VALUES (?, ?, ?, ?, ?)', orgId, tenantId, slug, name, createdAt);
|
|
2678
2873
|
return true;
|
|
2679
2874
|
}
|
|
2875
|
+
/**
|
|
2876
|
+
* #2526: `createOrg` with its admin row, in one unit; an org that already exists writes nothing,
|
|
2877
|
+
* the row included. A new name rather than a trailing argument on `createOrg`, which an older
|
|
2878
|
+
* ControlPlaneDO already lists in `REPLIED_METHODS`: there the argument would be dropped and the
|
|
2879
|
+
* org created with no row, where an unknown method fails closed.
|
|
2880
|
+
*/
|
|
2881
|
+
insertOrg(orgId, tenantId, slug, name, createdAt, audit) {
|
|
2882
|
+
return this.writeAndAudit(() => this.createOrg(orgId, tenantId, slug, name, createdAt), audit);
|
|
2883
|
+
}
|
|
2680
2884
|
listOrgs(tenantId) {
|
|
2681
2885
|
return this.sql
|
|
2682
2886
|
.exec('SELECT * FROM orgs WHERE tenant_id = ? ORDER BY slug', tenantId)
|
|
@@ -3210,6 +3414,42 @@ export class ControlPlaneDO extends DurableObject {
|
|
|
3210
3414
|
granted_by, granted_at, revoked_at)
|
|
3211
3415
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)`, row.connectionId, row.tenantId, row.vertical, row.permission, row.scopeId, row.expiresAt, row.grantedBy, row.grantedAt);
|
|
3212
3416
|
}
|
|
3417
|
+
/**
|
|
3418
|
+
* #2526: `grantToConnection`'s directory half as one unit — the connection (live, in the grant's
|
|
3419
|
+
* tenant) and, at a scope node, the scope (in that tenant, running the connection's vertical)
|
|
3420
|
+
* judged as they stand now, the record (`recordConnectionGrant`), the tenant tuple at the tenant
|
|
3421
|
+
* node, and the admin row, whose vertical and provider are the connection's as read here. A
|
|
3422
|
+
* scope node's tuple is delivered by the coordinator after this commits; the record is what a
|
|
3423
|
+
* reconcile re-seats it from when that delivery fails. Answers the connection's vertical.
|
|
3424
|
+
*/
|
|
3425
|
+
grantConnectionInDirectory(row, audit) {
|
|
3426
|
+
return this.ctx.storage.transactionSync(() => {
|
|
3427
|
+
const conn = this.readConnection(row.connectionId);
|
|
3428
|
+
if (!conn)
|
|
3429
|
+
throw new Error(`connection not found: ${row.connectionId}`);
|
|
3430
|
+
if (conn.revoked_at)
|
|
3431
|
+
throw new Error(`connection ${row.connectionId} is revoked — grant nothing to it`);
|
|
3432
|
+
if (conn.tenant_id !== row.tenantId) {
|
|
3433
|
+
throw new Error(`connection ${row.connectionId} belongs to tenant ${conn.tenant_id} and cannot be granted anything in ${row.tenantId}`);
|
|
3434
|
+
}
|
|
3435
|
+
if (row.scopeId) {
|
|
3436
|
+
const scope = this.getScopeRecord(row.tenantId, row.scopeId);
|
|
3437
|
+
if (!scope)
|
|
3438
|
+
throw substratError('not_found', `unknown scope ${row.scopeId} in tenant ${row.tenantId}`);
|
|
3439
|
+
if (scope.vertical !== conn.vertical) {
|
|
3440
|
+
throw new Error(`connection ${row.connectionId} is for vertical '${conn.vertical}' and scope ` +
|
|
3441
|
+
`${row.scopeId} runs '${scope.vertical ?? 'none'}'`);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
this.recordConnectionGrant({ ...row, vertical: conn.vertical });
|
|
3445
|
+
if (!row.scopeId) {
|
|
3446
|
+
this.writeTenantTuple(row.tenantId, `connection:${row.connectionId}`, `granted:${row.permission}`, `tenant:${row.tenantId}`, row.expiresAt);
|
|
3447
|
+
}
|
|
3448
|
+
const after = (audit.after ?? {});
|
|
3449
|
+
this.recordAdmin({ ...audit, vertical: conn.vertical, after: { ...after, provider: conn.provider } });
|
|
3450
|
+
return { vertical: conn.vertical };
|
|
3451
|
+
});
|
|
3452
|
+
}
|
|
3213
3453
|
// -- the kill switches' record (#1674, #2029) — `system-switch-record.ts` is the whole rule ---
|
|
3214
3454
|
/** OFF's write, before the scope's switch moves (#1823). Answers the row as it was. */
|
|
3215
3455
|
recordSwitchedOff(row) {
|