@substrat-run/adapter-cloudflare 0.138.0 → 0.139.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 +40 -22
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +148 -44
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +25 -7
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +103 -32
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +121 -64
- package/dist/scope-do.js.map +1 -1
- package/dist/sql.d.ts +5 -1
- package/dist/sql.d.ts.map +1 -1
- package/dist/sql.js +6 -2
- package/dist/sql.js.map +1 -1
- package/package.json +12 -10
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":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAyDnD,OAAO,
|
|
1
|
+
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAyDnD,OAAO,EAoDL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EA8FtB,MAAM,sBAAsB,CAAC;AAwC9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AA8EtD;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAwED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAyhB5E;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;AAgDtD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,SAAS,CAOxD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDxD;AAkBD,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,CAotMjE"}
|
package/dist/scope-do.js
CHANGED
|
@@ -2,12 +2,12 @@ import { REWIND_REFUSED } from './rewind-refusal.js';
|
|
|
2
2
|
import { SYSTEM_DOOR_MOVED } from './system-door.js';
|
|
3
3
|
import { DurableObject } from 'cloudflare:workers';
|
|
4
4
|
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, errorCodeOf, assertReplayableDump, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
5
|
-
import { ulid, DO_SQL_LIMITS, unknownRoleError, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, redactSubjectScopeText, assertRowLimit, assertRowOffset, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_LEASE_EXPIRED_NOTE, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, recordAttachmentText, searchAttachments, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, 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, emptyImportResult, 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, JOB_RUN_DUE_AT, jobRunListLimit, SYSTEM_DOOR_WAIT, ancestorsWithin, } from '@substrat-run/kernel';
|
|
5
|
+
import { ulid, DO_SQL_LIMITS, unknownRoleError, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, redactSubjectScopeText, assertRowLimit, assertRowOffset, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_LEASE_EXPIRED_NOTE, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexDdl, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, queueAttachmentTextBackfill, startAttachmentTextBackfill, recordAttachmentText, searchAttachments, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, 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, emptyImportResult, 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, JOB_RUN_DUE_AT, jobRunListLimit, SYSTEM_DOOR_WAIT, ancestorsWithin, } from '@substrat-run/kernel';
|
|
6
6
|
import { isUpgradeRequest, readSubscription, LIVE_FANOUT_LIMIT, LIVE_MODE_HEADER, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, LIVE_WITHIN_HEADER, decodeLiveWithin, } from './live-reads.js';
|
|
7
7
|
import { replyOf } from './do-reply.js';
|
|
8
8
|
import { OperationQueue } from './serialization.js';
|
|
9
9
|
import { doScopedSql, doBuiltColumnsOf, doRedactionSql, doSpineSql } from './sql.js';
|
|
10
|
-
import { actorOf, admitPeer, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, COPY_ORIGIN_DDL, SWITCH_FENCES_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, asyncInvocationId, asyncLinePass, resolveCapabilitySession, revokeCapabilityAsPlatform, readCapabilityPage, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readOperationSeries, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
|
|
10
|
+
import { actorOf, admitPeer, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, SWITCH_FENCES_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, searchStateWhere, uncheckedView, addStatePlans, entityStateTriggerDdl, statefulTablesOf, assertEntityStateIntact, stateListIndexNames, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, asyncInvocationId, asyncLinePass, resolveCapabilitySession, revokeCapabilityAsPlatform, readCapabilityPage, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readOperationSeries, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
|
|
11
11
|
import { createDoTupleChecker, createLocalControlPlaneReader, scopeTupleReader } from './checker.js';
|
|
12
12
|
import { CARRIED_AWAY_KEY, COPY_MARK_CLEARED_KEY, KEPT_COPY_REFUSAL, KEPT_DIVERGENT_KEY, LOAD_STAMP_KEY, STORE_LOCAL_META_KEYS, WRITE_REVISION_KEY, carriedAwayDump, isCopyMarkInsert, isWriteStatement, assertNoSpineReference, assertSpineTablesBuilt, capabilitiesForLoad, clearCopyMarker, dumpRowsInsert, isSpineTable, markCopyOrigin, repointScopeGrants, settleCopiedWork, emittedHere, IS_COPY_SQL, isCopyLoad, isLifecycleWrite, readLifecycle, settleLifecycleAfterLoad, writeLifecycle, spineColumnAdditions } from '@substrat-run/kernel';
|
|
13
13
|
/**
|
|
@@ -349,6 +349,9 @@ const KERNEL_DDL = `
|
|
|
349
349
|
-- #1686: where a copied scope's data came from, and the event id its own events start above.
|
|
350
350
|
-- Shared with the other adapter from @substrat-run/kernel; the column comments are there.
|
|
351
351
|
${COPY_ORIGIN_DDL}
|
|
352
|
+
-- #119: the kernel's authorization for one archive/trash move, read by the derived update
|
|
353
|
+
-- trigger. Shared with the other adapter from @substrat-run/kernel; the comments are there.
|
|
354
|
+
${ENTITY_STATE_MOVES_DDL}
|
|
352
355
|
`;
|
|
353
356
|
/**
|
|
354
357
|
* Workers RPC carries a plain `Error`'s MESSAGE faithfully and nothing else. A custom
|
|
@@ -708,6 +711,8 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
708
711
|
searchPlans = new Map();
|
|
709
712
|
/** #811: the paged lists modules declare, by entity type. Same one-owner rule. */
|
|
710
713
|
listPlans = new Map();
|
|
714
|
+
/** #119: entity type → its archive/trash plan, from every registered module. */
|
|
715
|
+
statePlans = new Map();
|
|
711
716
|
/** entityType → the declared attachment gate (#473): read key + write key (default: read). */
|
|
712
717
|
attachmentTargets = new Map();
|
|
713
718
|
/**
|
|
@@ -819,8 +824,10 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
819
824
|
}
|
|
820
825
|
this.searchPlans.set(plan.entityType, plan);
|
|
821
826
|
}
|
|
827
|
+
// #119: the archive/trash plans, refusing a second owner of one entity type.
|
|
828
|
+
addStatePlans(this.statePlans, manifest.id, manifest.entityStates, manifest.permissions);
|
|
822
829
|
// #811: the list indexes `lists` declares, same placement and same reason.
|
|
823
|
-
for (const plan of listIndexPlans(manifest.id, manifest.lists)) {
|
|
830
|
+
for (const plan of listIndexPlans(manifest.id, manifest.lists, manifest.entityStates)) {
|
|
824
831
|
const existing = this.listPlans.get(plan.entityType);
|
|
825
832
|
if (existing) {
|
|
826
833
|
throw new Error(`list: '${plan.entityType}' declares a paged list in both '${existing.moduleId}' and ` +
|
|
@@ -1306,13 +1313,12 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1306
1313
|
const { hops: _hops, ...fact } = e;
|
|
1307
1314
|
const event = structuredClone({ ...fact, source });
|
|
1308
1315
|
const unit = unitOf(imp.moduleId, e.id, e.type);
|
|
1309
|
-
this.causedBy = e.id;
|
|
1310
|
-
this.unitInvocationId = unit.invocationId;
|
|
1311
1316
|
try {
|
|
1312
1317
|
await this.revision.transaction(async () => {
|
|
1313
1318
|
// As the producer's principal: real checks against the grants this vertical's
|
|
1314
1319
|
// `peers` gave it; its emits carry `{ vertical, scope }` and what they passed.
|
|
1315
|
-
|
|
1320
|
+
// What it emits was emitted BECAUSE of the producer's event (#1237).
|
|
1321
|
+
await imp.handler(this.importContext(tenantId, scopeId, peerSubject, { causedBy: e.id, invocationId: unit.invocationId }), event);
|
|
1316
1322
|
this.sql.exec(`INSERT INTO _substrat_deliveries (event_id, consumer_module, delivered_at, invocation_id)
|
|
1317
1323
|
VALUES (?, ?, ?, NULL)`, e.id, imp.moduleId, new Date().toISOString());
|
|
1318
1324
|
});
|
|
@@ -1325,10 +1331,6 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1325
1331
|
result.deadLettered += 1;
|
|
1326
1332
|
lines.write({ ...unit, outcome: 'dead-lettered', error: err });
|
|
1327
1333
|
}
|
|
1328
|
-
finally {
|
|
1329
|
-
this.causedBy = null;
|
|
1330
|
-
this.unitInvocationId = null;
|
|
1331
|
-
}
|
|
1332
1334
|
}
|
|
1333
1335
|
if (!ran)
|
|
1334
1336
|
result.duplicates += 1;
|
|
@@ -1345,8 +1347,12 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1345
1347
|
* passes it (#1706). `principal` is a placeholder that the subject never reads, and there is
|
|
1346
1348
|
* no operation, so the emitted rows' `operation` is NULL, as it is for any consumer.
|
|
1347
1349
|
*/
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
+
/** A module consumer's context: the system override, and the delivery it runs for (#2055). */
|
|
1351
|
+
consumerContext(tenantId, scopeId, moduleId, delivery) {
|
|
1352
|
+
return this.operationContext(this.systemPrincipal, tenantId, scopeId, { system: moduleId }, undefined, undefined, undefined, undefined, undefined, undefined, [], undefined, undefined, delivery);
|
|
1353
|
+
}
|
|
1354
|
+
importContext(tenantId, scopeId, peerSubject, delivery) {
|
|
1355
|
+
return this.operationContext(principalId.parse(ulid()), tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, undefined, undefined, [], undefined, peerSubject, delivery);
|
|
1350
1356
|
}
|
|
1351
1357
|
async markEventsDrained(eventIds, at) {
|
|
1352
1358
|
if (eventIds.length === 0)
|
|
@@ -2653,6 +2659,20 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2653
2659
|
await this.ensureMigrations();
|
|
2654
2660
|
return this.queue.enqueue(async () => this.revision.transactionSync(() => recordAttachmentText(doSpineSql(this.sql), attachmentId, outcome, new Date().toISOString())));
|
|
2655
2661
|
}
|
|
2662
|
+
/**
|
|
2663
|
+
* #1575: start the scope's one-shot backfill, unless it is marked or holds no
|
|
2664
|
+
* attachments (`startAttachmentTextBackfill` reads before it writes, so a marked scope
|
|
2665
|
+
* writes nothing — and moves no write revision — on every drive).
|
|
2666
|
+
*/
|
|
2667
|
+
async attachmentTextBackfillStart() {
|
|
2668
|
+
await this.ensureMigrations();
|
|
2669
|
+
await this.queue.enqueue(async () => this.revision.transactionSync(() => startAttachmentTextBackfill(doSpineSql(this.sql), ulid(), new Date().toISOString())));
|
|
2670
|
+
}
|
|
2671
|
+
/** #1575: one backfill batch, in one transaction (`queueAttachmentTextBackfill`). */
|
|
2672
|
+
async attachmentTextBackfillBatch(after) {
|
|
2673
|
+
await this.ensureMigrations();
|
|
2674
|
+
return this.queue.enqueue(async () => this.revision.transactionSync(() => queueAttachmentTextBackfill(doSpineSql(this.sql), after, ulid, new Date().toISOString())));
|
|
2675
|
+
}
|
|
2656
2676
|
// -- attachments through a capability (#1686) -------------------------------
|
|
2657
2677
|
// The coordinator hashed the session token; only the hash arrives. Each verb resolves it
|
|
2658
2678
|
// INSIDE the queue, as `invoke` does, so nothing can revoke between the resolution and
|
|
@@ -2931,12 +2951,33 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2931
2951
|
return this.queue.enqueue(async () => {
|
|
2932
2952
|
const bound = await this.assignmentBound({ kind: 'principal', id: caller }, tenantId, scopeId, roleKey);
|
|
2933
2953
|
if (bound?.covered) {
|
|
2934
|
-
this.
|
|
2935
|
-
VALUES (?, ?, ?, NULL)`, `principal:${assignee}`, `role:${roleKey}`, `scope:${scopeId}`);
|
|
2954
|
+
applyScopeRoleChange(this.switchSql(), scopeId, assignee, { revoke: [], grant: roleKey }, new Date().toISOString());
|
|
2936
2955
|
}
|
|
2937
2956
|
return bound;
|
|
2938
2957
|
});
|
|
2939
2958
|
}
|
|
2959
|
+
/** The scope's live scope-level role assignments, or one principal's (#1150). */
|
|
2960
|
+
async scopeRoleHoldersFor(scopeId, principal) {
|
|
2961
|
+
await this.ensureMigrations();
|
|
2962
|
+
return this.queue.enqueue(() => scopeRoleHolders(this.switchSql(), scopeId, new Date().toISOString(), principal));
|
|
2963
|
+
}
|
|
2964
|
+
/**
|
|
2965
|
+
* The kernel's `changeScopeRole` in one serialized scope task (#1150). Its two refusals come
|
|
2966
|
+
* back as values: an error thrown here crosses the RPC flattened, so the coordinator types them.
|
|
2967
|
+
*/
|
|
2968
|
+
async changeScopeRoleBoundedFor(tenantId, scopeId, caller, principal, from, to) {
|
|
2969
|
+
await this.ensureMigrations();
|
|
2970
|
+
return this.queue.enqueue(() => changeScopeRole(this.switchSql(), scopeId, principal, from, to, new Date().toISOString(), this.roleBound(caller, tenantId, scopeId), (run) => this.revision.transactionSync(run)));
|
|
2971
|
+
}
|
|
2972
|
+
/** The kernel's `revokeScopeRoles` in one serialized scope task (#1150). */
|
|
2973
|
+
async revokeScopeRolesBoundedFor(tenantId, scopeId, caller, principal) {
|
|
2974
|
+
await this.ensureMigrations();
|
|
2975
|
+
return this.queue.enqueue(() => revokeScopeRoles(this.switchSql(), scopeId, principal, new Date().toISOString(), this.roleBound(caller, tenantId, scopeId), (run) => this.revision.transactionSync(run)));
|
|
2976
|
+
}
|
|
2977
|
+
/** The caller's bound per role at the scope; `null` for a role the tenant does not define. */
|
|
2978
|
+
roleBound(caller, tenantId, scopeId) {
|
|
2979
|
+
return (roleKey) => this.assignmentBound({ kind: 'principal', id: caller }, tenantId, scopeId, roleKey);
|
|
2980
|
+
}
|
|
2940
2981
|
/**
|
|
2941
2982
|
* §5.1's assignment bound, resolved against the SAME role table the checker
|
|
2942
2983
|
* expands — the tenant's projected role, not a vertical's compile-time `ROLES`
|
|
@@ -4464,6 +4505,23 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4464
4505
|
for (const stmt of splitSqlStatements(searchIndexDdl(plan)))
|
|
4465
4506
|
this.sql.exec(stmt);
|
|
4466
4507
|
}
|
|
4508
|
+
// #119: the never-born-archived trigger went with the dropped table. Put back AFTER the
|
|
4509
|
+
// rows, which may legitimately arrive archived or trashed.
|
|
4510
|
+
for (const plan of this.statePlans.values()) {
|
|
4511
|
+
if (!present.has(plan.table))
|
|
4512
|
+
continue;
|
|
4513
|
+
for (const stmt of splitSqlStatements(entityStateTriggerDdl(plan)))
|
|
4514
|
+
this.sql.exec(stmt);
|
|
4515
|
+
}
|
|
4516
|
+
// #811 / #119: the derived list indexes went with the dropped table too, and a load never
|
|
4517
|
+
// put them back — an archivable entity's partial indexes are part of what the kernel
|
|
4518
|
+
// checks after DDL.
|
|
4519
|
+
for (const plan of this.listPlans.values()) {
|
|
4520
|
+
if (!present.has(plan.table))
|
|
4521
|
+
continue;
|
|
4522
|
+
for (const stmt of splitSqlStatements(listIndexDdl(plan)))
|
|
4523
|
+
this.sql.exec(stmt);
|
|
4524
|
+
}
|
|
4467
4525
|
// #1335 / #1686: the outbox arrived with the dump, so this DO's event ids resume above it,
|
|
4468
4526
|
// as on a wake. A copy's own events then sort above every copied one, which is what
|
|
4469
4527
|
// `emittedHere()` relies on. A dump whose top id is no ULID leaves the floor where it was.
|
|
@@ -4792,36 +4850,17 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4792
4850
|
return redacted;
|
|
4793
4851
|
}
|
|
4794
4852
|
// -- event dispatch (port of dispatch) ------------------------------------
|
|
4795
|
-
/**
|
|
4796
|
-
* #1237: the event currently being delivered, or null.
|
|
4797
|
-
*
|
|
4798
|
-
* The host carries a field of the same name for the admin log, and it is
|
|
4799
|
-
* unreachable from here — it lives in the worker that holds the stub, while every
|
|
4800
|
-
* emit runs inside this Durable Object. So the DO keeps its own, and the two are
|
|
4801
|
-
* deliberately separate rather than one passed across the hop.
|
|
4802
|
-
*
|
|
4803
|
-
* One instance IS one scope, so this field is per-scope by construction. The
|
|
4804
|
-
* SQLite twin has to say so explicitly (`ScopeRuntime.causedBy`): that host holds
|
|
4805
|
-
* every scope in the process, so the same field on the host would be read by
|
|
4806
|
-
* another scope's emit the moment a consumer awaits.
|
|
4807
|
-
*/
|
|
4808
|
-
causedBy = null;
|
|
4809
4853
|
/**
|
|
4810
4854
|
* #1237: the invocation currently running in this DO, or null.
|
|
4811
4855
|
*
|
|
4812
|
-
* DO-local
|
|
4813
|
-
*
|
|
4814
|
-
*
|
|
4815
|
-
*
|
|
4856
|
+
* DO-local: a Durable Object IS one scope, so there is no other scope's call to
|
|
4857
|
+
* confuse it with, and `invoke` sets and clears it inside its queued body, which no
|
|
4858
|
+
* other emitting body interleaves with. It still has to be cleared, because the DO
|
|
4859
|
+
* outlives the request and a value left set would stamp a later alarm-driven drain
|
|
4860
|
+
* with a call it had nothing to do with. A consumer's own cause is not kept here: it is
|
|
4861
|
+
* passed into the consumer's context (`ConsumerDelivery`, #2055).
|
|
4816
4862
|
*/
|
|
4817
4863
|
invocationId = null;
|
|
4818
|
-
/**
|
|
4819
|
-
* #1901: the id a unit of async work with no call around it logs under — a consumer a
|
|
4820
|
-
* seed or an import delivered — set for its handler's duration and cleared after. Read
|
|
4821
|
-
* by `ctx.log` only, never the spine: an event a consumer emits outside a call still
|
|
4822
|
-
* records no invocation (#1525), while its log lines join the consumer's own line.
|
|
4823
|
-
*/
|
|
4824
|
-
unitInvocationId = null;
|
|
4825
4864
|
async dispatch(tenantId, scopeId,
|
|
4826
4865
|
/**
|
|
4827
4866
|
* #1525: the call this drain is running in, or null. Every round of the loop
|
|
@@ -4884,16 +4923,15 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4884
4923
|
lines.write({ ...unit, outcome: 'dead-lettered' });
|
|
4885
4924
|
continue;
|
|
4886
4925
|
}
|
|
4887
|
-
// #1237: anything this consumer emits was emitted BECAUSE of this event
|
|
4888
|
-
// — the step a backwards walk used to stop dead at, since a consumer
|
|
4889
|
-
// emit records no operation either.
|
|
4890
|
-
this.causedBy = event.id;
|
|
4891
|
-
// #1901: the handler's `ctx.log` lines join this delivery's line.
|
|
4892
|
-
this.unitInvocationId = unit.invocationId;
|
|
4893
4926
|
try {
|
|
4894
4927
|
await this.revision.transaction(async () => {
|
|
4895
|
-
|
|
4896
|
-
|
|
4928
|
+
// #1237: anything this consumer emits was emitted BECAUSE of this event —
|
|
4929
|
+
// the step a backwards walk used to stop dead at, since a consumer emit
|
|
4930
|
+
// records no operation either. #1901: its `ctx.log` lines join this
|
|
4931
|
+
// delivery's line.
|
|
4932
|
+
const ctx = this.consumerContext(tenantId, scopeId, mod.id, {
|
|
4933
|
+
causedBy: event.id,
|
|
4934
|
+
invocationId: unit.invocationId,
|
|
4897
4935
|
});
|
|
4898
4936
|
await consumer.handler(ctx, event);
|
|
4899
4937
|
this.sql.exec(`INSERT INTO _substrat_deliveries
|
|
@@ -4911,14 +4949,6 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4911
4949
|
VALUES (?, ?, ?, ?, ?)`, event.id, mod.id, new Date().toISOString(), String(err), invocationId);
|
|
4912
4950
|
lines.write({ ...unit, outcome: 'dead-lettered', error: err });
|
|
4913
4951
|
}
|
|
4914
|
-
finally {
|
|
4915
|
-
this.unitInvocationId = null;
|
|
4916
|
-
// Cleared on BOTH paths. Left set, the id leaks onto every later emit
|
|
4917
|
-
// this DO makes — an operation's own event stamped as caused by
|
|
4918
|
-
// whatever was delivered last, which is worse than recording nothing
|
|
4919
|
-
// because it reads as a fact.
|
|
4920
|
-
this.causedBy = null;
|
|
4921
|
-
}
|
|
4922
4952
|
}
|
|
4923
4953
|
}
|
|
4924
4954
|
}
|
|
@@ -5033,11 +5063,14 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
5033
5063
|
* the queued body. Mutually exclusive with the other non-person callers; `principal` is
|
|
5034
5064
|
* then a placeholder that the subject below never reads.
|
|
5035
5065
|
*/
|
|
5036
|
-
peerSubject
|
|
5066
|
+
peerSubject,
|
|
5067
|
+
/** #2055: the delivery a consumer's or an import's context runs for; absent otherwise. */
|
|
5068
|
+
delivery) {
|
|
5037
5069
|
const checker = this.checker;
|
|
5038
5070
|
const relations = this.relations;
|
|
5039
5071
|
const searchPlans = this.searchPlans;
|
|
5040
5072
|
const listPlans = this.listPlans;
|
|
5073
|
+
const statePlans = this.statePlans;
|
|
5041
5074
|
const sql = this.sql;
|
|
5042
5075
|
const mintEventId = this.mintEventId;
|
|
5043
5076
|
/**
|
|
@@ -5160,10 +5193,10 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
5160
5193
|
// about the deploy, so it never rides `DomainEvent` for module code to
|
|
5161
5194
|
// branch on; it exists for the observability joins the column serves.
|
|
5162
5195
|
this.env.SUBSTRAT_VERSION_ID ?? null,
|
|
5163
|
-
// #1237:
|
|
5164
|
-
//
|
|
5165
|
-
//
|
|
5166
|
-
|
|
5196
|
+
// #1237: the delivery this context was built for, if any — passed in, never read
|
|
5197
|
+
// off the DO (#2055). A fact about the surrounding dispatch, never envelope data
|
|
5198
|
+
// module code could set or branch on.
|
|
5199
|
+
delivery?.causedBy ?? null,
|
|
5167
5200
|
// #1237: a fact about the surrounding CALL, like the version above.
|
|
5168
5201
|
this.invocationId, full.payload === undefined ? null : JSON.stringify(full.payload));
|
|
5169
5202
|
};
|
|
@@ -5173,7 +5206,11 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
5173
5206
|
// #1672: a capability's own id stands in so the type holds — it is not a person, and
|
|
5174
5207
|
// the event actor says what it is instead. Every other door passes its own value.
|
|
5175
5208
|
principal: capabilityId ? capabilityId : principal,
|
|
5176
|
-
sql: guardSecrets(doScopedSql(sql
|
|
5209
|
+
sql: guardSecrets(doScopedSql(sql, statefulTablesOf(statePlans),
|
|
5210
|
+
// #119: after runtime DDL, the stateful tables must still carry what the kernel derived.
|
|
5211
|
+
statePlans.size
|
|
5212
|
+
? () => assertEntityStateIntact(doSpineSql(sql), statePlans, stateListIndexNames(listPlans.values()))
|
|
5213
|
+
: undefined), minted),
|
|
5177
5214
|
now: () => at,
|
|
5178
5215
|
// #1746/#1747: the host stamps who and where; module code supplies only the template
|
|
5179
5216
|
// and its fields. A consumer runs under the system override, so it logs as `system`.
|
|
@@ -5182,7 +5219,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
5182
5219
|
tenantId,
|
|
5183
5220
|
scopeId,
|
|
5184
5221
|
operation: operation ?? null,
|
|
5185
|
-
invocationId: () => this.invocationId ??
|
|
5222
|
+
invocationId: () => this.invocationId ?? delivery?.invocationId ?? null,
|
|
5186
5223
|
principalKind: systemActor ? 'system' : subject.kind,
|
|
5187
5224
|
// The string-safe redaction: `redactSecrets` parses its serialization back, and a
|
|
5188
5225
|
// log's text is not JSON.
|
|
@@ -5240,7 +5277,9 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
5240
5277
|
const plan = searchPlans.get(entityType);
|
|
5241
5278
|
if (!plan)
|
|
5242
5279
|
throw new NotSearchable(entityType);
|
|
5243
|
-
const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit)
|
|
5280
|
+
const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit),
|
|
5281
|
+
// #119: active rows unless the archive is asked for.
|
|
5282
|
+
searchStateWhere(statePlans, entityType, options?.view));
|
|
5244
5283
|
return sql.exec(q.sql, ...q.params).toArray().map((row) => ({ entityType, id: row.id, rank: row.rank }));
|
|
5245
5284
|
},
|
|
5246
5285
|
/**
|
|
@@ -5260,10 +5299,12 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
5260
5299
|
order: params.order,
|
|
5261
5300
|
cursor: params.cursor,
|
|
5262
5301
|
filters: params.filters,
|
|
5302
|
+
// #119: the bin is the checked reader's, never this one's.
|
|
5303
|
+
view: uncheckedView('ctx.page', entityType, params.view),
|
|
5263
5304
|
});
|
|
5264
5305
|
const rows = sql.exec(q.sql, ...q.params).toArray();
|
|
5265
5306
|
const last = rows.length >= limit ? rows[rows.length - 1] : undefined;
|
|
5266
|
-
const nextCursor = last === undefined ? null : cursorOf(last, q.sortColumn, plan.idColumn, q.order);
|
|
5307
|
+
const nextCursor = last === undefined ? null : cursorOf(last, q.sortColumn, plan.idColumn, q.order, q.view);
|
|
5267
5308
|
const page = { entries: rows, nextCursor };
|
|
5268
5309
|
if (!params.total)
|
|
5269
5310
|
return page;
|
|
@@ -5329,6 +5370,22 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
5329
5370
|
emit: (event) => writeEvent(event, 'kernel'),
|
|
5330
5371
|
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
5331
5372
|
}),
|
|
5373
|
+
// #119: archive and trash — the pure adapter's wiring, over the raw spine seam.
|
|
5374
|
+
...createEntityStateVerbs({
|
|
5375
|
+
sql: doSpineSql(sql),
|
|
5376
|
+
plans: statePlans,
|
|
5377
|
+
now: at,
|
|
5378
|
+
check: runCheck,
|
|
5379
|
+
emit: (event) => writeEvent(event, 'kernel'),
|
|
5380
|
+
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
5381
|
+
}),
|
|
5382
|
+
...createTrashedReads({
|
|
5383
|
+
query: (q, params) => sql.exec(q, ...params).toArray(),
|
|
5384
|
+
listPlans,
|
|
5385
|
+
searchPlans,
|
|
5386
|
+
statePlans,
|
|
5387
|
+
check: runCheck,
|
|
5388
|
+
}),
|
|
5332
5389
|
entitlement: async (key) => {
|
|
5333
5390
|
const held = await entitlementReader().listEntitlements(tenantId);
|
|
5334
5391
|
return held.find((e) => e.key === key) ?? null;
|