@substrat-run/adapter-cloudflare 0.124.0 → 0.126.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/host.d.ts +8 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +24 -9
- package/dist/host.js.map +1 -1
- package/dist/live-reads.d.ts +7 -9
- package/dist/live-reads.d.ts.map +1 -1
- package/dist/live-reads.js +7 -7
- package/dist/live-reads.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +113 -67
- package/dist/scope-do.js.map +1 -1
- package/package.json +9 -9
package/dist/live-reads.d.ts
CHANGED
|
@@ -39,8 +39,12 @@ export declare const LIVE_SCOPE_HEADER = "x-substrat-live-scope";
|
|
|
39
39
|
* is how somebody reports "live updates are broken" two months later and nobody can
|
|
40
40
|
* establish whether they ever worked on that hostname. One header makes the fallback a
|
|
41
41
|
* fact the client can log, display, and report.
|
|
42
|
+
*
|
|
43
|
+
* Defined in the kernel (#1859), because the pure host's `501` names it too and a
|
|
44
|
+
* vertical's dev server should not import this adapter to spell it. Re-exported here so
|
|
45
|
+
* `host.ts` and `scope-do.ts` keep reading every wire name from this one file.
|
|
42
46
|
*/
|
|
43
|
-
export
|
|
47
|
+
export { LIVE_MODE_HEADER, type LiveRefusal } from '@substrat-run/kernel';
|
|
44
48
|
/**
|
|
45
49
|
* Cloudflare's own per-request marker for orange-to-orange routing.
|
|
46
50
|
*
|
|
@@ -58,12 +62,6 @@ export declare const LIVE_MODE_HEADER = "x-substrat-live";
|
|
|
58
62
|
* cheaper than a matrix and correct on the request after the tenant changes their DNS.
|
|
59
63
|
*/
|
|
60
64
|
export declare const O2O_HEADER = "cf-connecting-o2o";
|
|
61
|
-
/** Why a subscription was refused — the value of `LIVE_MODE_HEADER` on a refusal. */
|
|
62
|
-
export type LiveRefusal =
|
|
63
|
-
/** This connection cannot carry a WebSocket; the client should keep polling. */
|
|
64
|
-
'poll'
|
|
65
|
-
/** The request was not an upgrade at all — a programming error at the caller. */
|
|
66
|
-
| 'not-an-upgrade';
|
|
67
65
|
/**
|
|
68
66
|
* What a subscribed socket remembers about itself across a hibernation.
|
|
69
67
|
*
|
|
@@ -103,8 +101,8 @@ export declare function readSubscription(attachment: unknown): LiveSubscription
|
|
|
103
101
|
* reaching it means something unusual happened rather than something normal being cut off.
|
|
104
102
|
*/
|
|
105
103
|
export declare const LIVE_FANOUT_LIMIT = 200;
|
|
106
|
-
/** Is this request asking to be upgraded to a WebSocket? */
|
|
107
|
-
export
|
|
104
|
+
/** Is this request asking to be upgraded to a WebSocket? Defined in the kernel (#1859). */
|
|
105
|
+
export { isUpgradeRequest } from '@substrat-run/kernel';
|
|
108
106
|
/**
|
|
109
107
|
* Is this request arriving over an orange-to-orange hop?
|
|
110
108
|
*
|
package/dist/live-reads.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-reads.d.ts","sourceRoot":"","sources":["../src/live-reads.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,8BAA8B,CAAC;AACjE,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,eAAO,MAAM,iBAAiB,0BAA0B,CAAC;AAEzD
|
|
1
|
+
{"version":3,"file":"live-reads.d.ts","sourceRoot":"","sources":["../src/live-reads.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,8BAA8B,CAAC;AACjE,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,eAAO,MAAM,iBAAiB,0BAA0B,CAAC;AAEzD;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE1E;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,sBAAsB,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAa7E;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,2FAA2F;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE1D"}
|
package/dist/live-reads.js
CHANGED
|
@@ -27,8 +27,12 @@ export const LIVE_SCOPE_HEADER = 'x-substrat-live-scope';
|
|
|
27
27
|
* is how somebody reports "live updates are broken" two months later and nobody can
|
|
28
28
|
* establish whether they ever worked on that hostname. One header makes the fallback a
|
|
29
29
|
* fact the client can log, display, and report.
|
|
30
|
+
*
|
|
31
|
+
* Defined in the kernel (#1859), because the pure host's `501` names it too and a
|
|
32
|
+
* vertical's dev server should not import this adapter to spell it. Re-exported here so
|
|
33
|
+
* `host.ts` and `scope-do.ts` keep reading every wire name from this one file.
|
|
30
34
|
*/
|
|
31
|
-
export
|
|
35
|
+
export { LIVE_MODE_HEADER } from '@substrat-run/kernel';
|
|
32
36
|
/**
|
|
33
37
|
* Cloudflare's own per-request marker for orange-to-orange routing.
|
|
34
38
|
*
|
|
@@ -87,12 +91,8 @@ export function readSubscription(attachment) {
|
|
|
87
91
|
* reaching it means something unusual happened rather than something normal being cut off.
|
|
88
92
|
*/
|
|
89
93
|
export const LIVE_FANOUT_LIMIT = 200;
|
|
90
|
-
/** Is this request asking to be upgraded to a WebSocket? */
|
|
91
|
-
export
|
|
92
|
-
// Case-insensitive: the header is `Upgrade: websocket` by the RFC, but the token is
|
|
93
|
-
// compared case-insensitively there too, and browsers are not the only clients.
|
|
94
|
-
return (request.headers.get('Upgrade') ?? '').toLowerCase() === 'websocket';
|
|
95
|
-
}
|
|
94
|
+
/** Is this request asking to be upgraded to a WebSocket? Defined in the kernel (#1859). */
|
|
95
|
+
export { isUpgradeRequest } from '@substrat-run/kernel';
|
|
96
96
|
/**
|
|
97
97
|
* Is this request arriving over an orange-to-orange hop?
|
|
98
98
|
*
|
package/dist/live-reads.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live-reads.js","sourceRoot":"","sources":["../src/live-reads.ts"],"names":[],"mappings":"AAaA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,2BAA2B,CAAC;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAC3D,MAAM,CAAC,MAAM,iBAAiB,GAAG,uBAAuB,CAAC;AAEzD
|
|
1
|
+
{"version":3,"file":"live-reads.js","sourceRoot":"","sources":["../src/live-reads.ts"],"names":[],"mappings":"AAaA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,2BAA2B,CAAC;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAC3D,MAAM,CAAC,MAAM,iBAAiB,GAAG,uBAAuB,CAAC;AAEzD;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,gBAAgB,EAAoB,MAAM,sBAAsB,CAAC;AAE1E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAAC;AAmB9C;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAmB;IAClD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACvE,MAAM,CAAC,GAAG,UAA8D,CAAC;IACzE,IAAI,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACvE,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACrE,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACnE,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,OAAO;QACL,SAAS,EAAE,CAAC,CAAC,SAAwB;QACrC,QAAQ,EAAE,CAAC,CAAC,QAAoB;QAChC,OAAO,EAAE,CAAC,CAAC,OAAkB;QAC7B,KAAK,EAAE,CAAC,CAAC,KAAK;KACf,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,2FAA2F;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC;AACjD,CAAC"}
|
package/dist/scope-do.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,
|
|
1
|
+
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAcL,KAAK,WAAW,EAqCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAmCL,KAAK,SAAS,EAmBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAoEtB,MAAM,sBAAsB,CAAC;AAmF9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAsbD;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAgDxG;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,SAAS,CAOxD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDxD;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAs7JjE"}
|
package/dist/scope-do.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { REWIND_REFUSED } from './rewind-refusal.js';
|
|
2
2
|
import { DurableObject } from 'cloudflare:workers';
|
|
3
|
-
import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, domainEvent, domainEventInput, eventId, instant, objectRef, toWireFailure, grantRefFromProof, principalId, scopeId as scopeIdOf, tenantId as tenantIdOf, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, MAX_PENDING_SWEEP_RUNS, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, listLimitOf, requestFingerprint, substratError, assertReplayableDump, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
3
|
+
import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, domainEvent, domainEventInput, eventId, instant, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, toWireFailure, grantRefFromProof, principalId, scopeId as scopeIdOf, tenantId as tenantIdOf, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, MAX_PENDING_SWEEP_RUNS, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, listLimitOf, requestFingerprint, substratError, assertReplayableDump, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
4
4
|
import { ulid, DO_SQL_LIMITS, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, JOB_RUN_PATCH_SQL, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, IDEMPOTENCY_DDL, assertIdempotencyKey, assertPermissionKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, exportedSinceQuery, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, jobRunListLimit, } from '@substrat-run/kernel';
|
|
5
5
|
import { isUpgradeRequest, readSubscription, LIVE_FANOUT_LIMIT, LIVE_MODE_HEADER, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, } from './live-reads.js';
|
|
6
6
|
import { OperationQueue } from './serialization.js';
|
|
7
7
|
import { doScopedSql, doSpineSql } from './sql.js';
|
|
8
|
-
import { actorOf, admitPeer, collectPeers, switchPeer, assertNoSecret, CAPABILITY_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, resolveCapabilitySession, revokeCapabilityAsPlatform, domainEventOf, facetEvents, readDeadLetters, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
|
|
8
|
+
import { actorOf, admitPeer, collectPeers, switchPeer, assertNoSecret, CAPABILITY_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, resolveCapabilitySession, revokeCapabilityAsPlatform, domainEventOf, facetEvents, readDeadLetters, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
|
|
9
9
|
import { createDoTupleChecker, createLocalControlPlaneReader } from './checker.js';
|
|
10
|
+
import { repointScopeGrants } from '@substrat-run/kernel';
|
|
10
11
|
/**
|
|
11
12
|
* The key marking a scope DO whose storage was destroyed (`destroyStorage`).
|
|
12
13
|
* Written after `deleteAll()` so it survives the wipe, and never cleared: `reaped`
|
|
@@ -341,6 +342,19 @@ const KERNEL_DDL = `
|
|
|
341
342
|
* In-process — the SQLite adapter, a handler in the same isolate — the real class
|
|
342
343
|
* arrives and `errorCodeOf` reads it directly.
|
|
343
344
|
*/
|
|
345
|
+
/**
|
|
346
|
+
* #1864: the kernel's own events (attachments, capability exchanges) written through the
|
|
347
|
+
* context an operation runs in, WITHOUT `ctx.emit`'s refusal of kernel-authored types. Keyed
|
|
348
|
+
* by the context object and never exported, so module code — which can reach `ctx` but not
|
|
349
|
+
* this map — cannot write one. Registered as each context is built.
|
|
350
|
+
*/
|
|
351
|
+
const kernelEmitters = new WeakMap();
|
|
352
|
+
function kernelEmit(ctx, event) {
|
|
353
|
+
const write = kernelEmitters.get(ctx);
|
|
354
|
+
if (!write)
|
|
355
|
+
throw new Error('kernelEmit: not an operation context this host built');
|
|
356
|
+
write(event);
|
|
357
|
+
}
|
|
344
358
|
function toRpcError(err) {
|
|
345
359
|
if (err instanceof Error) {
|
|
346
360
|
return err.constructor === Error ? err : new Error(err.message);
|
|
@@ -556,8 +570,20 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
556
570
|
lastFailure = null;
|
|
557
571
|
/** Passes of `applyPendingMigrations` that had work to do — see `migrationAttemptsOnInstance`. */
|
|
558
572
|
migrationRuns = 0;
|
|
573
|
+
/**
|
|
574
|
+
* TEST-ONLY (#1860): no production code reads this. An exact `'ping'` is answered
|
|
575
|
+
* by `setWebSocketAutoResponse` without reaching `webSocketMessage` at all, and
|
|
576
|
+
* this count is how a test proves that from outside the object — read through
|
|
577
|
+
* `runInDurableObject` — rather than assuming the runtime's documented behaviour.
|
|
578
|
+
*/
|
|
579
|
+
webSocketMessagesHandled = 0;
|
|
559
580
|
constructor(ctx, env) {
|
|
560
581
|
super(ctx, env);
|
|
582
|
+
// #1860: the client's 45s keep-alive (demos/ticket0/app/src/feed.ts) answered at
|
|
583
|
+
// the runtime level, before this object wakes for it. Without this, every idle
|
|
584
|
+
// subscriber's ping still reaches `webSocketMessage` and pins the DO in memory —
|
|
585
|
+
// exactly the hibernation cost `acceptWebSocket` (below) exists to avoid.
|
|
586
|
+
ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair('ping', 'pong'));
|
|
561
587
|
this.sql = ctx.storage.sql;
|
|
562
588
|
for (const stmt of splitSqlStatements(KERNEL_DDL)) {
|
|
563
589
|
this.sql.exec(stmt);
|
|
@@ -1233,16 +1259,17 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1233
1259
|
// BOTH stores, because a scope check consults both: rule 2 inheritance makes a
|
|
1234
1260
|
// tenant-level grant enforceable here exactly as a scope-level one is, and the
|
|
1235
1261
|
// projected `_substrat_tenant_tuples` is where this DO holds them. A read-back
|
|
1236
|
-
// that disagreed with enforcement would be worse than none.
|
|
1262
|
+
// that disagreed with enforcement would be worse than none. GLOB, not LIKE (#1869):
|
|
1263
|
+
// case-sensitive, as the checker's match is.
|
|
1237
1264
|
const rows = [
|
|
1238
1265
|
...this.sql
|
|
1239
1266
|
.exec(`SELECT subject, relation, expires_at FROM _substrat_tuples
|
|
1240
|
-
WHERE subject
|
|
1267
|
+
WHERE subject GLOB 'connection:*' AND relation GLOB 'granted:*'
|
|
1241
1268
|
AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`, now)
|
|
1242
1269
|
.toArray(),
|
|
1243
1270
|
...this.sql
|
|
1244
1271
|
.exec(`SELECT subject, relation, expires_at FROM _substrat_tenant_tuples
|
|
1245
|
-
WHERE subject
|
|
1272
|
+
WHERE subject GLOB 'connection:*' AND relation GLOB 'granted:*'
|
|
1246
1273
|
AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`, now)
|
|
1247
1274
|
.toArray(),
|
|
1248
1275
|
];
|
|
@@ -1827,8 +1854,14 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1827
1854
|
* Deliberately NOT a place to let a client narrow or widen what it receives: a
|
|
1828
1855
|
* filter the client chooses is a filter the client can choose wrongly, and the
|
|
1829
1856
|
* only filter that matters here is the one it does not control.
|
|
1857
|
+
*
|
|
1858
|
+
* #1860: an exact `'ping'` is now answered by `setWebSocketAutoResponse` in the
|
|
1859
|
+
* constructor, at the runtime level, without waking this object — so this branch no
|
|
1860
|
+
* longer runs for it. Kept as the documented fallback for a client on an older
|
|
1861
|
+
* build, or a hop where the runtime's auto-response is unavailable.
|
|
1830
1862
|
*/
|
|
1831
1863
|
webSocketMessage(ws, message) {
|
|
1864
|
+
this.webSocketMessagesHandled++;
|
|
1832
1865
|
if (message === 'ping')
|
|
1833
1866
|
ws.send('pong');
|
|
1834
1867
|
}
|
|
@@ -2071,7 +2104,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2071
2104
|
(id, entity_type, entity_id, filename, content_type, size, sha256,
|
|
2072
2105
|
visibility, created_by, created_at)
|
|
2073
2106
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, parsed.id, parsed.entity.entityType, parsed.entity.entityId, parsed.filename, parsed.contentType, parsed.size, parsed.sha256, parsed.visibility, parsed.createdBy, parsed.createdAt);
|
|
2074
|
-
ctx
|
|
2107
|
+
kernelEmit(ctx, {
|
|
2075
2108
|
type: ATTACHMENT_ADDED,
|
|
2076
2109
|
schemaVersion: 1,
|
|
2077
2110
|
entity: parsed.entity,
|
|
@@ -2167,7 +2200,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2167
2200
|
const ctx = this.operationContext(principal, tenantId, scopeId, undefined, connectionId, undefined, undefined, undefined, 'attachments.remove');
|
|
2168
2201
|
assertAllowed(await ctx.check(gate.write, record.entity));
|
|
2169
2202
|
this.sql.exec('DELETE FROM _substrat_attachments WHERE id = ?', attachmentId);
|
|
2170
|
-
ctx
|
|
2203
|
+
kernelEmit(ctx, {
|
|
2171
2204
|
type: ATTACHMENT_REMOVED,
|
|
2172
2205
|
schemaVersion: 1,
|
|
2173
2206
|
entity: record.entity,
|
|
@@ -2574,7 +2607,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2574
2607
|
// Stamped `{ capability }` by the ordinary emit path, under the exchange's own
|
|
2575
2608
|
// pseudo-operation name — the actor is the capability, as on every event it
|
|
2576
2609
|
// goes on to cause.
|
|
2577
|
-
emit: (capability, event) => this.operationContext(principalId.parse(ulid()), tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, CAPABILITY_EXCHANGE_OPERATION, capability, [], now)
|
|
2610
|
+
emit: (capability, event) => kernelEmit(this.operationContext(principalId.parse(ulid()), tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, CAPABILITY_EXCHANGE_OPERATION, capability, [], now), event),
|
|
2578
2611
|
}, secret, mode);
|
|
2579
2612
|
});
|
|
2580
2613
|
if (outcome)
|
|
@@ -3386,11 +3419,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3386
3419
|
this.sql.exec(stmt);
|
|
3387
3420
|
});
|
|
3388
3421
|
}
|
|
3389
|
-
async importDump(tables, destScopeId,
|
|
3390
|
-
/** The directory's recorded-off modules (#1742), switched off on `destScopeId` right after
|
|
3391
|
-
* the replay re-points the grants, in the same event: a dump from before the switch was
|
|
3392
|
-
* pulled carries the grants live and no marker. Needs `destScopeId`, the scope restored. */
|
|
3393
|
-
switchOff) {
|
|
3422
|
+
async importDump(tables, destScopeId, { switchOff, sourceScopeId, exact, } = {}) {
|
|
3394
3423
|
// The WHOLE drop-then-replay runs under deferred foreign keys, in one transaction.
|
|
3395
3424
|
//
|
|
3396
3425
|
// Two distinct FK hazards, and both need the deferral — the second is why it cannot
|
|
@@ -3419,6 +3448,10 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3419
3448
|
// only the first statement, so the text itself has to be the one statement.
|
|
3420
3449
|
// Pure input validation, so it runs before the first DROP: a refused dump touches nothing.
|
|
3421
3450
|
assertReplayableDump(replayable, { maxColumns: DO_SQL_LIMITS.columns });
|
|
3451
|
+
// #1869: `exact` vouches for a named source; without one it would silently fall back.
|
|
3452
|
+
if (exact && !sourceScopeId) {
|
|
3453
|
+
throw substratError('validation_failed', 'restore refused: `exact` needs the scope the dump came from');
|
|
3454
|
+
}
|
|
3422
3455
|
const switched = await this.ctx.storage.transaction(async () => {
|
|
3423
3456
|
this.sql.exec('PRAGMA defer_foreign_keys = ON');
|
|
3424
3457
|
// Real tables only; `sqlite_*` internals are auto-managed and un-droppable.
|
|
@@ -3469,7 +3502,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3469
3502
|
// Re-point the restored grants at THIS scope (after the spine exists, so a dump
|
|
3470
3503
|
// that carried no tuples table still finds one here).
|
|
3471
3504
|
if (destScopeId)
|
|
3472
|
-
this.rewriteScopeTuples(destScopeId);
|
|
3505
|
+
this.rewriteScopeTuples(destScopeId, sourceScopeId && { scopeId: sourceScopeId, exact });
|
|
3473
3506
|
// #1742: the recorded-off modules go back off INSIDE the replay's transaction, with
|
|
3474
3507
|
// the spine and the re-point. A switch that throws rolls the whole restore back, so
|
|
3475
3508
|
// the dump's grants never commit live without the switch that should cover them.
|
|
@@ -3539,14 +3572,14 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3539
3572
|
* `_substrat_tenant_tuples`, which this does not attempt — a cross-tenant restore is a
|
|
3540
3573
|
* governed copy, not a repair.
|
|
3541
3574
|
*
|
|
3575
|
+
* Which rows move is `repointScopeGrants`'s (#1869): the dump's source scope, exactly.
|
|
3576
|
+
*
|
|
3542
3577
|
* `UPDATE OR REPLACE` because (subject, relation, object) is the primary key: if the
|
|
3543
3578
|
* dump already held a tuple for the destination scope, the rewritten row collapses
|
|
3544
3579
|
* onto it instead of failing the whole restore.
|
|
3545
3580
|
*/
|
|
3546
|
-
rewriteScopeTuples(destScopeId) {
|
|
3547
|
-
this.
|
|
3548
|
-
SET object = ?
|
|
3549
|
-
WHERE object LIKE 'scope:%' AND object <> ?`, `scope:${destScopeId}`, `scope:${destScopeId}`);
|
|
3581
|
+
rewriteScopeTuples(destScopeId, source) {
|
|
3582
|
+
repointScopeGrants(this.switchSql(), destScopeId, source);
|
|
3550
3583
|
}
|
|
3551
3584
|
/**
|
|
3552
3585
|
* Wipe this scope's storage — the reap half of deleteSnapshot (preview-and-
|
|
@@ -3918,6 +3951,51 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3918
3951
|
}
|
|
3919
3952
|
return checker.covers(subject, role.permissions, { tenantId, scopeId });
|
|
3920
3953
|
};
|
|
3954
|
+
// ctx.emit's writer, and the kernel's own events' (#1864): one path to the outbox, with the
|
|
3955
|
+
// reserved-type refusal applied to module code only. Never handed to module code as-is.
|
|
3956
|
+
const writeEvent = (event, author) => {
|
|
3957
|
+
assertImpersonationWrites(impersonation, 'ctx.emit');
|
|
3958
|
+
const parsed = domainEventInput.parse(event);
|
|
3959
|
+
// #1864: a kernel-authored type is the kernel's to write — module code cannot forge one.
|
|
3960
|
+
if (author === 'module')
|
|
3961
|
+
assertModuleEmittableType(parsed.type);
|
|
3962
|
+
else
|
|
3963
|
+
assertKernelAuthoredType(parsed.type); // a new kernel event must join the reserved set
|
|
3964
|
+
// #1672: the COMPLETE parsed event — entity id, type and subject as well as payload.
|
|
3965
|
+
assertNoSecret('ctx.emit', parsed, minted);
|
|
3966
|
+
const full = domainEvent.parse({
|
|
3967
|
+
...parsed,
|
|
3968
|
+
// #956: from the operation's instant, not a second reading of the clock.
|
|
3969
|
+
// `ORDER BY id` is how the outbox and every timeline page, so an id whose
|
|
3970
|
+
// timestamp disagreed with its own `occurredAt` sorted the log by a clock
|
|
3971
|
+
// nothing else in the operation used.
|
|
3972
|
+
id: eventId.parse(mintEventId(Date.parse(at))),
|
|
3973
|
+
occurredAt: at,
|
|
3974
|
+
tenantId,
|
|
3975
|
+
scopeId,
|
|
3976
|
+
actor: systemActor ?? derivedActor,
|
|
3977
|
+
...(passed.length ? { authorization: passed.map((p) => ({ ...p })) } : {}),
|
|
3978
|
+
...(stamp ? { impersonation: stamp } : {}),
|
|
3979
|
+
// #1231: kernel-stamped like the two above — `domainEventInput.parse`
|
|
3980
|
+
// already stripped anything module code tried to smuggle under this key.
|
|
3981
|
+
...(operation ? { operation } : {}),
|
|
3982
|
+
});
|
|
3983
|
+
sql.exec(`INSERT INTO _substrat_outbox
|
|
3984
|
+
(id, type, schema_version, occurred_at, tenant_id, scope_id, actor,
|
|
3985
|
+
entity_type, entity_id, pii_class, subject_id, authorization,
|
|
3986
|
+
impersonation, operation, version, caused_by, invocation_id, payload)
|
|
3987
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, full.id, full.type, full.schemaVersion, full.occurredAt, full.tenantId, full.scopeId, JSON.stringify(full.actor), full.entity.entityType, full.entity.entityId, full.piiClass, full.subjectId ?? null, full.authorization ? JSON.stringify(full.authorization) : null, full.impersonation ? JSON.stringify(full.impersonation) : null, full.operation ?? null,
|
|
3988
|
+
// #1242: script configuration, not envelope data — the version is a fact
|
|
3989
|
+
// about the deploy, so it never rides `DomainEvent` for module code to
|
|
3990
|
+
// branch on; it exists for the observability joins the column serves.
|
|
3991
|
+
this.env.SUBSTRAT_VERSION_ID ?? null,
|
|
3992
|
+
// #1237: whatever delivery is in flight, if any — read off the DO the same
|
|
3993
|
+
// way the version is read off its env. A fact about the surrounding
|
|
3994
|
+
// dispatch, never envelope data module code could set or branch on.
|
|
3995
|
+
this.causedBy,
|
|
3996
|
+
// #1237: a fact about the surrounding CALL, like the version above.
|
|
3997
|
+
this.invocationId, full.payload === undefined ? null : JSON.stringify(full.payload));
|
|
3998
|
+
};
|
|
3921
3999
|
const ctxRef = {
|
|
3922
4000
|
tenantId,
|
|
3923
4001
|
scopeId,
|
|
@@ -3939,44 +4017,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3939
4017
|
// log's text is not JSON.
|
|
3940
4018
|
redact: (text) => redactSecretText(text, minted),
|
|
3941
4019
|
}),
|
|
3942
|
-
emit: (event) =>
|
|
3943
|
-
assertImpersonationWrites(impersonation, 'ctx.emit');
|
|
3944
|
-
const parsed = domainEventInput.parse(event);
|
|
3945
|
-
// #1672: the COMPLETE parsed event — entity id, type and subject as well as payload.
|
|
3946
|
-
assertNoSecret('ctx.emit', parsed, minted);
|
|
3947
|
-
const full = domainEvent.parse({
|
|
3948
|
-
...parsed,
|
|
3949
|
-
// #956: from the operation's instant, not a second reading of the clock.
|
|
3950
|
-
// `ORDER BY id` is how the outbox and every timeline page, so an id whose
|
|
3951
|
-
// timestamp disagreed with its own `occurredAt` sorted the log by a clock
|
|
3952
|
-
// nothing else in the operation used.
|
|
3953
|
-
id: eventId.parse(mintEventId(Date.parse(at))),
|
|
3954
|
-
occurredAt: at,
|
|
3955
|
-
tenantId,
|
|
3956
|
-
scopeId,
|
|
3957
|
-
actor: systemActor ?? derivedActor,
|
|
3958
|
-
...(passed.length ? { authorization: passed.map((p) => ({ ...p })) } : {}),
|
|
3959
|
-
...(stamp ? { impersonation: stamp } : {}),
|
|
3960
|
-
// #1231: kernel-stamped like the two above — `domainEventInput.parse`
|
|
3961
|
-
// already stripped anything module code tried to smuggle under this key.
|
|
3962
|
-
...(operation ? { operation } : {}),
|
|
3963
|
-
});
|
|
3964
|
-
sql.exec(`INSERT INTO _substrat_outbox
|
|
3965
|
-
(id, type, schema_version, occurred_at, tenant_id, scope_id, actor,
|
|
3966
|
-
entity_type, entity_id, pii_class, subject_id, authorization,
|
|
3967
|
-
impersonation, operation, version, caused_by, invocation_id, payload)
|
|
3968
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, full.id, full.type, full.schemaVersion, full.occurredAt, full.tenantId, full.scopeId, JSON.stringify(full.actor), full.entity.entityType, full.entity.entityId, full.piiClass, full.subjectId ?? null, full.authorization ? JSON.stringify(full.authorization) : null, full.impersonation ? JSON.stringify(full.impersonation) : null, full.operation ?? null,
|
|
3969
|
-
// #1242: script configuration, not envelope data — the version is a fact
|
|
3970
|
-
// about the deploy, so it never rides `DomainEvent` for module code to
|
|
3971
|
-
// branch on; it exists for the observability joins the column serves.
|
|
3972
|
-
this.env.SUBSTRAT_VERSION_ID ?? null,
|
|
3973
|
-
// #1237: whatever delivery is in flight, if any — read off the DO the same
|
|
3974
|
-
// way the version is read off its env. A fact about the surrounding
|
|
3975
|
-
// dispatch, never envelope data module code could set or branch on.
|
|
3976
|
-
this.causedBy,
|
|
3977
|
-
// #1237: a fact about the surrounding CALL, like the version above.
|
|
3978
|
-
this.invocationId, full.payload === undefined ? null : JSON.stringify(full.payload));
|
|
3979
|
-
},
|
|
4020
|
+
emit: (event) => writeEvent(event, 'module'),
|
|
3980
4021
|
requestPlatform: (request) => {
|
|
3981
4022
|
assertImpersonationWrites(impersonation, 'ctx.requestPlatform');
|
|
3982
4023
|
const input = platformRequestInput.parse(request);
|
|
@@ -4070,6 +4111,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4070
4111
|
*/
|
|
4071
4112
|
grant: async (principal, permission, entity) => {
|
|
4072
4113
|
assertImpersonationWrites(impersonation, 'ctx.grant');
|
|
4114
|
+
entityObjectRef(entity, 'ctx.grant'); // #1856: a tuple the walk can read back
|
|
4073
4115
|
const held = await runCheck(permission, entity);
|
|
4074
4116
|
if (!held.allowed) {
|
|
4075
4117
|
throw new PermissionDenied(`cannot grant '${permission}' on ${entity.entityType}:${entity.entityId} — ` +
|
|
@@ -4077,6 +4119,11 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4077
4119
|
}
|
|
4078
4120
|
sql.exec(`INSERT OR IGNORE INTO _substrat_tuples (subject, relation, object) VALUES (?, ?, ?)`, `principal:${principal}`, `granted:${permission}`, `${entity.entityType}:${entity.entityId}`);
|
|
4079
4121
|
},
|
|
4122
|
+
/**
|
|
4123
|
+
* Deliberately NOT the #1856 grammar check `grant` and `link` make: a revoke writes
|
|
4124
|
+
* nothing the walk must read back, and a grant stored before that check existed must
|
|
4125
|
+
* stay removable.
|
|
4126
|
+
*/
|
|
4080
4127
|
revoke: async (principal, permission, entity) => {
|
|
4081
4128
|
assertImpersonationWrites(impersonation, 'ctx.revoke');
|
|
4082
4129
|
const held = await runCheck(permission, entity);
|
|
@@ -4098,21 +4145,19 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4098
4145
|
subject: systemActor ? { kind: 'system', id: systemActor.system } : subject,
|
|
4099
4146
|
now: at,
|
|
4100
4147
|
check: runCheck,
|
|
4101
|
-
emit: (event) =>
|
|
4148
|
+
emit: (event) => writeEvent(event, 'kernel'),
|
|
4102
4149
|
isOperation: (name) => this.operations.has(name),
|
|
4103
4150
|
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
4104
4151
|
minted,
|
|
4105
4152
|
}),
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
VALUES (?, 'parent', ?)`, `${child.entityType}:${child.entityId}`, `${parent.entityType}:${parent.entityId}`);
|
|
4115
|
-
},
|
|
4153
|
+
// K-16 / #1864: link and relink, written once in the kernel over the raw spine seam.
|
|
4154
|
+
...createEntityEdgeVerbs({
|
|
4155
|
+
sql: doSpineSql(sql),
|
|
4156
|
+
relations,
|
|
4157
|
+
now: at,
|
|
4158
|
+
emit: (event) => writeEvent(event, 'kernel'),
|
|
4159
|
+
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
4160
|
+
}),
|
|
4116
4161
|
entitlement: async (key) => {
|
|
4117
4162
|
const held = await entitlementReader().listEntitlements(tenantId);
|
|
4118
4163
|
return held.find((e) => e.key === key) ?? null;
|
|
@@ -4136,6 +4181,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4136
4181
|
return sealTo({ keyId: row.key_id, publicKey: row.public_key }, plaintext);
|
|
4137
4182
|
},
|
|
4138
4183
|
};
|
|
4184
|
+
kernelEmitters.set(ctxRef, (event) => writeEvent(event, 'kernel'));
|
|
4139
4185
|
return ctxRef;
|
|
4140
4186
|
}
|
|
4141
4187
|
/** True once this scope has had entitlements projected at least once (#304) — the switch
|