@substrat-run/adapter-cloudflare 0.108.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 +14 -1
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +86 -22
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +110 -1
- package/dist/scope-do.js.map +1 -1
- package/package.json +4 -4
package/dist/host.js
CHANGED
|
@@ -497,6 +497,10 @@ 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
|
+
}
|
|
500
504
|
/** One record's event history (#1235) on this host's own scope — the vertical-host read. */
|
|
501
505
|
async entityHistoryLocal(scopeId, input) {
|
|
502
506
|
return this.scopeStub(scopeId).entityHistory(input);
|
|
@@ -2238,26 +2242,51 @@ export class CloudflareScopeHost {
|
|
|
2238
2242
|
listScopeTables: async (actor, tenantId, scopeId) => {
|
|
2239
2243
|
// K-3 cross-check on the shared directory BEFORE reaching the scope DO: a pair
|
|
2240
2244
|
// that does not resolve is unreachable, never another tenant's database.
|
|
2241
|
-
|
|
2242
|
-
if (!row)
|
|
2243
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2245
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2244
2246
|
const tables = await this.scopeStub(scopeId).introspectTables();
|
|
2245
2247
|
await this.recordAccess(actor, 'listScopeTables', { tenantId, scopeId }, null, tables.length);
|
|
2246
2248
|
return tables;
|
|
2247
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
|
+
},
|
|
2248
2281
|
entityHistory: async (actor, tenantId, scopeId, input) => {
|
|
2249
2282
|
// K-3 cross-check on the directory BEFORE the scope DO, like every read here.
|
|
2250
|
-
|
|
2251
|
-
if (!row)
|
|
2252
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2283
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2253
2284
|
const page = await this.scopeStub(scopeId).entityHistory(input);
|
|
2254
2285
|
await this.recordAccess(actor, 'entityHistory', { tenantId, scopeId }, { entityType: input.entityType, entityId: input.entityId }, page.entries.length);
|
|
2255
2286
|
return page;
|
|
2256
2287
|
},
|
|
2257
2288
|
readScopeTable: async (actor, tenantId, scopeId, input) => {
|
|
2258
|
-
|
|
2259
|
-
if (!row)
|
|
2260
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2289
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2261
2290
|
const page = await this.scopeStub(scopeId).introspectTable(input.table, input.limit, input.offset);
|
|
2262
2291
|
await this.recordAccess(actor, 'readScopeTable', { tenantId, scopeId }, { table: input.table, limit: page.limit, offset: page.offset }, page.rows.length);
|
|
2263
2292
|
return page;
|
|
@@ -2266,25 +2295,19 @@ export class CloudflareScopeHost {
|
|
|
2266
2295
|
// K-3 cross-check on the shared directory BEFORE reaching the scope DO, as
|
|
2267
2296
|
// every read here does: an unresolved pair is unreachable, never another
|
|
2268
2297
|
// tenant's log.
|
|
2269
|
-
|
|
2270
|
-
if (!row)
|
|
2271
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2298
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2272
2299
|
const rows = await this.scopeStub(scopeId).listDenials(filter);
|
|
2273
2300
|
await this.recordAccess(actor, 'listDenials', { tenantId, scopeId }, filter ?? null, rows.length);
|
|
2274
2301
|
return rows;
|
|
2275
2302
|
},
|
|
2276
2303
|
summarizeDenials: async (actor, tenantId, scopeId, filter) => {
|
|
2277
|
-
|
|
2278
|
-
if (!row)
|
|
2279
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2304
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2280
2305
|
const summary = await this.scopeStub(scopeId).summarizeDenials(filter);
|
|
2281
2306
|
await this.recordAccess(actor, 'summarizeDenials', { tenantId, scopeId }, filter ?? null, summary.buckets.length);
|
|
2282
2307
|
return summary;
|
|
2283
2308
|
},
|
|
2284
2309
|
queryScope: async (actor, tenantId, scopeId, input) => {
|
|
2285
|
-
|
|
2286
|
-
if (!row)
|
|
2287
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2310
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2288
2311
|
const result = await this.scopeStub(scopeId).introspectQuery(input.sql);
|
|
2289
2312
|
// The statement is the logged argument: the access log is the evidence trail,
|
|
2290
2313
|
// and for a console read the SQL is the whole story.
|
|
@@ -2296,9 +2319,7 @@ export class CloudflareScopeHost {
|
|
|
2296
2319
|
// as the introspection reads: an unresolved pair is unreachable, never another
|
|
2297
2320
|
// tenant's database. The DO returns the tables; the coordinator, which knows the
|
|
2298
2321
|
// scope's identity, stamps the dump.
|
|
2299
|
-
|
|
2300
|
-
if (!row)
|
|
2301
|
-
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2322
|
+
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2302
2323
|
const tables = await this.scopeStub(scopeId).exportDump();
|
|
2303
2324
|
await this.recordAccess(actor, 'exportScope', { tenantId, scopeId }, null, tables.length);
|
|
2304
2325
|
return { tenantId, scopeId, capturedAt: new Date().toISOString(), tables };
|
|
@@ -2944,6 +2965,24 @@ export class CloudflareScopeHost {
|
|
|
2944
2965
|
if (!rec)
|
|
2945
2966
|
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2946
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
|
+
}
|
|
2947
2986
|
/**
|
|
2948
2987
|
* This scope's per-subject keys (#37). The crypto lives in the kernel
|
|
2949
2988
|
* (`createSubjectKeys`); the adapter supplies only the three row operations, which here
|
|
@@ -3165,7 +3204,32 @@ export class CloudflareScopeHost {
|
|
|
3165
3204
|
if (!this.scopeLocalPermissions)
|
|
3166
3205
|
return;
|
|
3167
3206
|
const { roles, tuples, entitlements, identities } = await this.tenantProjection(tenantId);
|
|
3168
|
-
|
|
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
|
+
});
|
|
3169
3233
|
// #687: connection keys are per (tenant, vertical), so they are resolved per scope
|
|
3170
3234
|
// rather than once for the tenant — memoized, because a fan-out over twenty scopes
|
|
3171
3235
|
// of one vertical must not mint or re-read twenty times.
|