@substrat-run/adapter-cloudflare 0.107.0 → 0.109.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.map +1 -1
- package/dist/control-plane-do.js +21 -0
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +25 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +102 -25
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +121 -0
- package/dist/scope-do.js.map +1 -1
- package/package.json +4 -4
package/dist/host.js
CHANGED
|
@@ -497,15 +497,23 @@ export class CloudflareScopeHost {
|
|
|
497
497
|
async exportScopeLocal(scopeId) {
|
|
498
498
|
return this.scopeStub(scopeId).exportDump();
|
|
499
499
|
}
|
|
500
|
+
/** Facet this host's own scope's outbox (#1239) — the vertical-host read. */
|
|
501
|
+
async facetEventsLocal(scopeId, input) {
|
|
502
|
+
return this.scopeStub(scopeId).facetEvents(input);
|
|
503
|
+
}
|
|
504
|
+
/** One record's event history (#1235) on this host's own scope — the vertical-host read. */
|
|
505
|
+
async entityHistoryLocal(scopeId, input) {
|
|
506
|
+
return this.scopeStub(scopeId).entityHistory(input);
|
|
507
|
+
}
|
|
508
|
+
/** When this host's own scope applied each migration (#1236) — the vertical-host read. */
|
|
509
|
+
async appliedMigrationsLocal(scopeId) {
|
|
510
|
+
return this.scopeStub(scopeId).appliedMigrations();
|
|
511
|
+
}
|
|
500
512
|
/**
|
|
501
513
|
* The PITR bookmarks one scope recorded before its migration passes (#286) — the
|
|
502
514
|
* rewind points a backout UI offers. Behind the vertical's platform-gated
|
|
503
515
|
* `/internal/bookmarks`; the control plane is the gate and the auditor.
|
|
504
516
|
*/
|
|
505
|
-
/** When this host's own scope applied each migration (#1236) — the vertical-host read. */
|
|
506
|
-
async appliedMigrationsLocal(scopeId) {
|
|
507
|
-
return this.scopeStub(scopeId).appliedMigrations();
|
|
508
|
-
}
|
|
509
517
|
async migrationBookmarksLocal(scopeId) {
|
|
510
518
|
return this.scopeStub(scopeId).migrationBookmarks();
|
|
511
519
|
}
|
|
@@ -2234,17 +2242,51 @@ export class CloudflareScopeHost {
|
|
|
2234
2242
|
listScopeTables: async (actor, tenantId, scopeId) => {
|
|
2235
2243
|
// K-3 cross-check on the shared directory BEFORE reaching the scope DO: a pair
|
|
2236
2244
|
// that does not resolve is unreachable, never another tenant's database.
|
|
2237
|
-
|
|
2238
|
-
if (!row)
|
|
2239
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2245
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2240
2246
|
const tables = await this.scopeStub(scopeId).introspectTables();
|
|
2241
2247
|
await this.recordAccess(actor, 'listScopeTables', { tenantId, scopeId }, null, tables.length);
|
|
2242
2248
|
return tables;
|
|
2243
2249
|
},
|
|
2250
|
+
readUndrainedEvents: async (actor, tenantId, scopeId, limit) => {
|
|
2251
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2252
|
+
const events = await this.scopeStub(scopeId).undrainedEvents(Math.min(Math.max(limit ?? 200, 1), 1000));
|
|
2253
|
+
await this.recordAccess(actor, 'readUndrainedEvents', { tenantId, scopeId }, { limit }, events.length);
|
|
2254
|
+
return events;
|
|
2255
|
+
},
|
|
2256
|
+
markEventsDrained: async (actor, tenantId, scopeId, eventIds) => {
|
|
2257
|
+
// The same refusal the reads carry: a reaped scope's storage is gone, and
|
|
2258
|
+
// addressing its DO would construct an empty one to stamp nothing in.
|
|
2259
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2260
|
+
if (eventIds.length === 0)
|
|
2261
|
+
return 0;
|
|
2262
|
+
const drainedAt = new Date().toISOString();
|
|
2263
|
+
const drained = await this.scopeStub(scopeId).markEventsDrained(eventIds, drainedAt);
|
|
2264
|
+
// K-24's rule, one tier down (#1334): declaring domain payloads shipped is an
|
|
2265
|
+
// EGRESS, and the admin log is where "these events left the platform, at this
|
|
2266
|
+
// time, on this actor's say-so" is recorded. `drainAccessLog` audits the same
|
|
2267
|
+
// act for the smaller class of data; this one carries the larger. Only a
|
|
2268
|
+
// NONZERO change is recorded, so a retried pass that re-marks a batch it
|
|
2269
|
+
// already shipped writes no row claiming an egress that never happened.
|
|
2270
|
+
if (drained > 0) {
|
|
2271
|
+
await this.recordAdmin(actor, 'drainEvents', { tenantId, scopeId }, null, { drained, requested: eventIds.length, drainedAt });
|
|
2272
|
+
}
|
|
2273
|
+
return drained;
|
|
2274
|
+
},
|
|
2275
|
+
facetEvents: async (actor, tenantId, scopeId, input) => {
|
|
2276
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2277
|
+
const result = await this.scopeStub(scopeId).facetEvents(input);
|
|
2278
|
+
await this.recordAccess(actor, 'facetEvents', { tenantId, scopeId }, input, result.buckets.length);
|
|
2279
|
+
return result;
|
|
2280
|
+
},
|
|
2281
|
+
entityHistory: async (actor, tenantId, scopeId, input) => {
|
|
2282
|
+
// K-3 cross-check on the directory BEFORE the scope DO, like every read here.
|
|
2283
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2284
|
+
const page = await this.scopeStub(scopeId).entityHistory(input);
|
|
2285
|
+
await this.recordAccess(actor, 'entityHistory', { tenantId, scopeId }, { entityType: input.entityType, entityId: input.entityId }, page.entries.length);
|
|
2286
|
+
return page;
|
|
2287
|
+
},
|
|
2244
2288
|
readScopeTable: async (actor, tenantId, scopeId, input) => {
|
|
2245
|
-
|
|
2246
|
-
if (!row)
|
|
2247
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2289
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2248
2290
|
const page = await this.scopeStub(scopeId).introspectTable(input.table, input.limit, input.offset);
|
|
2249
2291
|
await this.recordAccess(actor, 'readScopeTable', { tenantId, scopeId }, { table: input.table, limit: page.limit, offset: page.offset }, page.rows.length);
|
|
2250
2292
|
return page;
|
|
@@ -2253,25 +2295,19 @@ export class CloudflareScopeHost {
|
|
|
2253
2295
|
// K-3 cross-check on the shared directory BEFORE reaching the scope DO, as
|
|
2254
2296
|
// every read here does: an unresolved pair is unreachable, never another
|
|
2255
2297
|
// tenant's log.
|
|
2256
|
-
|
|
2257
|
-
if (!row)
|
|
2258
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2298
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2259
2299
|
const rows = await this.scopeStub(scopeId).listDenials(filter);
|
|
2260
2300
|
await this.recordAccess(actor, 'listDenials', { tenantId, scopeId }, filter ?? null, rows.length);
|
|
2261
2301
|
return rows;
|
|
2262
2302
|
},
|
|
2263
2303
|
summarizeDenials: async (actor, tenantId, scopeId, filter) => {
|
|
2264
|
-
|
|
2265
|
-
if (!row)
|
|
2266
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2304
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2267
2305
|
const summary = await this.scopeStub(scopeId).summarizeDenials(filter);
|
|
2268
2306
|
await this.recordAccess(actor, 'summarizeDenials', { tenantId, scopeId }, filter ?? null, summary.buckets.length);
|
|
2269
2307
|
return summary;
|
|
2270
2308
|
},
|
|
2271
2309
|
queryScope: async (actor, tenantId, scopeId, input) => {
|
|
2272
|
-
|
|
2273
|
-
if (!row)
|
|
2274
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2310
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2275
2311
|
const result = await this.scopeStub(scopeId).introspectQuery(input.sql);
|
|
2276
2312
|
// The statement is the logged argument: the access log is the evidence trail,
|
|
2277
2313
|
// and for a console read the SQL is the whole story.
|
|
@@ -2283,9 +2319,7 @@ export class CloudflareScopeHost {
|
|
|
2283
2319
|
// as the introspection reads: an unresolved pair is unreachable, never another
|
|
2284
2320
|
// tenant's database. The DO returns the tables; the coordinator, which knows the
|
|
2285
2321
|
// scope's identity, stamps the dump.
|
|
2286
|
-
|
|
2287
|
-
if (!row)
|
|
2288
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2322
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2289
2323
|
const tables = await this.scopeStub(scopeId).exportDump();
|
|
2290
2324
|
await this.recordAccess(actor, 'exportScope', { tenantId, scopeId }, null, tables.length);
|
|
2291
2325
|
return { tenantId, scopeId, capturedAt: new Date().toISOString(), tables };
|
|
@@ -2931,6 +2965,24 @@ export class CloudflareScopeHost {
|
|
|
2931
2965
|
if (!rec)
|
|
2932
2966
|
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2933
2967
|
}
|
|
2968
|
+
/**
|
|
2969
|
+
* The same K-3 cross-check, for a read that then opens the scope's STORAGE — every
|
|
2970
|
+
* introspection verb below. It adds one refusal the bare existence check cannot make:
|
|
2971
|
+
* a REAPED scope keeps its directory row as a tombstone (§4.4) while its storage is
|
|
2972
|
+
* gone, so the row resolves and the read looks legal. Addressing the DO anyway
|
|
2973
|
+
* CONSTRUCTS an empty one, and the answer comes back as a scope with no tables, no
|
|
2974
|
+
* events and no denials rather than as a scope that no longer exists — the read
|
|
2975
|
+
* quietly contradicting the reap that was meant to be irreversible. `archived` still
|
|
2976
|
+
* reads: its bytes are there, which is the whole distinction between the two states.
|
|
2977
|
+
*/
|
|
2978
|
+
async scopeRecordForRead(tenantId, scopeId) {
|
|
2979
|
+
const row = await this.cp.getScopeRecord(tenantId, scopeId);
|
|
2980
|
+
if (!row)
|
|
2981
|
+
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2982
|
+
if (row.status === 'reaped') {
|
|
2983
|
+
throw new Error(`scope ${scopeId} is reaped — its storage is gone and cannot be read`);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2934
2986
|
/**
|
|
2935
2987
|
* This scope's per-subject keys (#37). The crypto lives in the kernel
|
|
2936
2988
|
* (`createSubjectKeys`); the adapter supplies only the three row operations, which here
|
|
@@ -3020,8 +3072,8 @@ export class CloudflareScopeHost {
|
|
|
3020
3072
|
* The connection's CURRENT public sealing key, minted on first ask (#687).
|
|
3021
3073
|
*
|
|
3022
3074
|
* Mint-on-read rather than mint-only-at-connect, because the fleet already holds
|
|
3023
|
-
* live connections older than this feature —
|
|
3024
|
-
* years of real contracts, and "reconnect to acquire a keypair" is not a
|
|
3075
|
+
* live connections older than this feature — a Scrive credential in the fleet
|
|
3076
|
+
* carries years of real contracts, and "reconnect to acquire a keypair" is not a
|
|
3025
3077
|
* migration anyone should have to run against production. Asking IS the
|
|
3026
3078
|
* back-fill, and it is idempotent: the DO's insert loses silently against the
|
|
3027
3079
|
* partial-unique index and re-reads the winner, so a race yields one key.
|
|
@@ -3152,7 +3204,32 @@ export class CloudflareScopeHost {
|
|
|
3152
3204
|
if (!this.scopeLocalPermissions)
|
|
3153
3205
|
return;
|
|
3154
3206
|
const { roles, tuples, entitlements, identities } = await this.tenantProjection(tenantId);
|
|
3155
|
-
|
|
3207
|
+
// Only scopes that can still EVALUATE a permission. Unfiltered, this projected
|
|
3208
|
+
// into `archived` and `reaped` rows too — and a reaped scope's storage was
|
|
3209
|
+
// deliberately `deleteAll()`d ("the bytes are gone, so there is no restore",
|
|
3210
|
+
// tenancy.ts), so writing a projection into its DO recreated storage for a scope
|
|
3211
|
+
// the platform believes dead. Silent, unbounded in the number of apps a tenant
|
|
3212
|
+
// has ever archived, and paid for on every membership change.
|
|
3213
|
+
//
|
|
3214
|
+
// `provisioning` is included: a scope mid-provision pulls the projection itself
|
|
3215
|
+
// at creation, and including it costs one idempotent write while excluding it
|
|
3216
|
+
// could race that pull. `suspended` is included because suspension is reversible
|
|
3217
|
+
// and a suspended scope must not come back with a stale projection.
|
|
3218
|
+
// Only scopes that can still EVALUATE a permission. Unfiltered, this projected
|
|
3219
|
+
// into `archived` and `reaped` rows too — and a reaped scope's storage was
|
|
3220
|
+
// deliberately `deleteAll()`d ("the bytes are gone, so there is no restore",
|
|
3221
|
+
// tenancy.ts), so writing a projection into its DO recreated storage for a scope
|
|
3222
|
+
// the platform believes dead. Silent, unbounded in the number of apps a tenant
|
|
3223
|
+
// has ever archived, and paid for on every membership change.
|
|
3224
|
+
//
|
|
3225
|
+
// `provisioning` is included: a scope mid-provision pulls the projection itself
|
|
3226
|
+
// at creation, and including it costs one idempotent write while excluding it
|
|
3227
|
+
// could race that pull. `suspended` is included because suspension is reversible
|
|
3228
|
+
// and a suspended scope must not come back with a stale projection.
|
|
3229
|
+
const scopes = await this.cp.listScopes({
|
|
3230
|
+
tenantId,
|
|
3231
|
+
status: ['provisioning', 'active', 'suspended'],
|
|
3232
|
+
});
|
|
3156
3233
|
// #687: connection keys are per (tenant, vertical), so they are resolved per scope
|
|
3157
3234
|
// rather than once for the tenant — memoized, because a fan-out over twenty scopes
|
|
3158
3235
|
// of one vertical must not mint or re-read twenty times.
|