@substrat-run/adapter-cloudflare 0.125.0 → 0.127.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 +19 -6
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +89 -69
- package/dist/scope-do.js.map +1 -1
- package/package.json +9 -9
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, entityObjectRef, 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);
|
|
@@ -1245,16 +1259,17 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1245
1259
|
// BOTH stores, because a scope check consults both: rule 2 inheritance makes a
|
|
1246
1260
|
// tenant-level grant enforceable here exactly as a scope-level one is, and the
|
|
1247
1261
|
// projected `_substrat_tenant_tuples` is where this DO holds them. A read-back
|
|
1248
|
-
// 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.
|
|
1249
1264
|
const rows = [
|
|
1250
1265
|
...this.sql
|
|
1251
1266
|
.exec(`SELECT subject, relation, expires_at FROM _substrat_tuples
|
|
1252
|
-
WHERE subject
|
|
1267
|
+
WHERE subject GLOB 'connection:*' AND relation GLOB 'granted:*'
|
|
1253
1268
|
AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`, now)
|
|
1254
1269
|
.toArray(),
|
|
1255
1270
|
...this.sql
|
|
1256
1271
|
.exec(`SELECT subject, relation, expires_at FROM _substrat_tenant_tuples
|
|
1257
|
-
WHERE subject
|
|
1272
|
+
WHERE subject GLOB 'connection:*' AND relation GLOB 'granted:*'
|
|
1258
1273
|
AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`, now)
|
|
1259
1274
|
.toArray(),
|
|
1260
1275
|
];
|
|
@@ -2089,7 +2104,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2089
2104
|
(id, entity_type, entity_id, filename, content_type, size, sha256,
|
|
2090
2105
|
visibility, created_by, created_at)
|
|
2091
2106
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, parsed.id, parsed.entity.entityType, parsed.entity.entityId, parsed.filename, parsed.contentType, parsed.size, parsed.sha256, parsed.visibility, parsed.createdBy, parsed.createdAt);
|
|
2092
|
-
ctx
|
|
2107
|
+
kernelEmit(ctx, {
|
|
2093
2108
|
type: ATTACHMENT_ADDED,
|
|
2094
2109
|
schemaVersion: 1,
|
|
2095
2110
|
entity: parsed.entity,
|
|
@@ -2185,7 +2200,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2185
2200
|
const ctx = this.operationContext(principal, tenantId, scopeId, undefined, connectionId, undefined, undefined, undefined, 'attachments.remove');
|
|
2186
2201
|
assertAllowed(await ctx.check(gate.write, record.entity));
|
|
2187
2202
|
this.sql.exec('DELETE FROM _substrat_attachments WHERE id = ?', attachmentId);
|
|
2188
|
-
ctx
|
|
2203
|
+
kernelEmit(ctx, {
|
|
2189
2204
|
type: ATTACHMENT_REMOVED,
|
|
2190
2205
|
schemaVersion: 1,
|
|
2191
2206
|
entity: record.entity,
|
|
@@ -2592,7 +2607,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2592
2607
|
// Stamped `{ capability }` by the ordinary emit path, under the exchange's own
|
|
2593
2608
|
// pseudo-operation name — the actor is the capability, as on every event it
|
|
2594
2609
|
// goes on to cause.
|
|
2595
|
-
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),
|
|
2596
2611
|
}, secret, mode);
|
|
2597
2612
|
});
|
|
2598
2613
|
if (outcome)
|
|
@@ -3404,11 +3419,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3404
3419
|
this.sql.exec(stmt);
|
|
3405
3420
|
});
|
|
3406
3421
|
}
|
|
3407
|
-
async importDump(tables, destScopeId,
|
|
3408
|
-
/** The directory's recorded-off modules (#1742), switched off on `destScopeId` right after
|
|
3409
|
-
* the replay re-points the grants, in the same event: a dump from before the switch was
|
|
3410
|
-
* pulled carries the grants live and no marker. Needs `destScopeId`, the scope restored. */
|
|
3411
|
-
switchOff) {
|
|
3422
|
+
async importDump(tables, destScopeId, { switchOff, sourceScopeId, exact, } = {}) {
|
|
3412
3423
|
// The WHOLE drop-then-replay runs under deferred foreign keys, in one transaction.
|
|
3413
3424
|
//
|
|
3414
3425
|
// Two distinct FK hazards, and both need the deferral — the second is why it cannot
|
|
@@ -3437,6 +3448,10 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3437
3448
|
// only the first statement, so the text itself has to be the one statement.
|
|
3438
3449
|
// Pure input validation, so it runs before the first DROP: a refused dump touches nothing.
|
|
3439
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
|
+
}
|
|
3440
3455
|
const switched = await this.ctx.storage.transaction(async () => {
|
|
3441
3456
|
this.sql.exec('PRAGMA defer_foreign_keys = ON');
|
|
3442
3457
|
// Real tables only; `sqlite_*` internals are auto-managed and un-droppable.
|
|
@@ -3487,7 +3502,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3487
3502
|
// Re-point the restored grants at THIS scope (after the spine exists, so a dump
|
|
3488
3503
|
// that carried no tuples table still finds one here).
|
|
3489
3504
|
if (destScopeId)
|
|
3490
|
-
this.rewriteScopeTuples(destScopeId);
|
|
3505
|
+
this.rewriteScopeTuples(destScopeId, sourceScopeId && { scopeId: sourceScopeId, exact });
|
|
3491
3506
|
// #1742: the recorded-off modules go back off INSIDE the replay's transaction, with
|
|
3492
3507
|
// the spine and the re-point. A switch that throws rolls the whole restore back, so
|
|
3493
3508
|
// the dump's grants never commit live without the switch that should cover them.
|
|
@@ -3557,14 +3572,14 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3557
3572
|
* `_substrat_tenant_tuples`, which this does not attempt — a cross-tenant restore is a
|
|
3558
3573
|
* governed copy, not a repair.
|
|
3559
3574
|
*
|
|
3575
|
+
* Which rows move is `repointScopeGrants`'s (#1869): the dump's source scope, exactly.
|
|
3576
|
+
*
|
|
3560
3577
|
* `UPDATE OR REPLACE` because (subject, relation, object) is the primary key: if the
|
|
3561
3578
|
* dump already held a tuple for the destination scope, the rewritten row collapses
|
|
3562
3579
|
* onto it instead of failing the whole restore.
|
|
3563
3580
|
*/
|
|
3564
|
-
rewriteScopeTuples(destScopeId) {
|
|
3565
|
-
this.
|
|
3566
|
-
SET object = ?
|
|
3567
|
-
WHERE object LIKE 'scope:%' AND object <> ?`, `scope:${destScopeId}`, `scope:${destScopeId}`);
|
|
3581
|
+
rewriteScopeTuples(destScopeId, source) {
|
|
3582
|
+
repointScopeGrants(this.switchSql(), destScopeId, source);
|
|
3568
3583
|
}
|
|
3569
3584
|
/**
|
|
3570
3585
|
* Wipe this scope's storage — the reap half of deleteSnapshot (preview-and-
|
|
@@ -3936,6 +3951,51 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3936
3951
|
}
|
|
3937
3952
|
return checker.covers(subject, role.permissions, { tenantId, scopeId });
|
|
3938
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
|
+
};
|
|
3939
3999
|
const ctxRef = {
|
|
3940
4000
|
tenantId,
|
|
3941
4001
|
scopeId,
|
|
@@ -3957,44 +4017,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3957
4017
|
// log's text is not JSON.
|
|
3958
4018
|
redact: (text) => redactSecretText(text, minted),
|
|
3959
4019
|
}),
|
|
3960
|
-
emit: (event) =>
|
|
3961
|
-
assertImpersonationWrites(impersonation, 'ctx.emit');
|
|
3962
|
-
const parsed = domainEventInput.parse(event);
|
|
3963
|
-
// #1672: the COMPLETE parsed event — entity id, type and subject as well as payload.
|
|
3964
|
-
assertNoSecret('ctx.emit', parsed, minted);
|
|
3965
|
-
const full = domainEvent.parse({
|
|
3966
|
-
...parsed,
|
|
3967
|
-
// #956: from the operation's instant, not a second reading of the clock.
|
|
3968
|
-
// `ORDER BY id` is how the outbox and every timeline page, so an id whose
|
|
3969
|
-
// timestamp disagreed with its own `occurredAt` sorted the log by a clock
|
|
3970
|
-
// nothing else in the operation used.
|
|
3971
|
-
id: eventId.parse(mintEventId(Date.parse(at))),
|
|
3972
|
-
occurredAt: at,
|
|
3973
|
-
tenantId,
|
|
3974
|
-
scopeId,
|
|
3975
|
-
actor: systemActor ?? derivedActor,
|
|
3976
|
-
...(passed.length ? { authorization: passed.map((p) => ({ ...p })) } : {}),
|
|
3977
|
-
...(stamp ? { impersonation: stamp } : {}),
|
|
3978
|
-
// #1231: kernel-stamped like the two above — `domainEventInput.parse`
|
|
3979
|
-
// already stripped anything module code tried to smuggle under this key.
|
|
3980
|
-
...(operation ? { operation } : {}),
|
|
3981
|
-
});
|
|
3982
|
-
sql.exec(`INSERT INTO _substrat_outbox
|
|
3983
|
-
(id, type, schema_version, occurred_at, tenant_id, scope_id, actor,
|
|
3984
|
-
entity_type, entity_id, pii_class, subject_id, authorization,
|
|
3985
|
-
impersonation, operation, version, caused_by, invocation_id, payload)
|
|
3986
|
-
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,
|
|
3987
|
-
// #1242: script configuration, not envelope data — the version is a fact
|
|
3988
|
-
// about the deploy, so it never rides `DomainEvent` for module code to
|
|
3989
|
-
// branch on; it exists for the observability joins the column serves.
|
|
3990
|
-
this.env.SUBSTRAT_VERSION_ID ?? null,
|
|
3991
|
-
// #1237: whatever delivery is in flight, if any — read off the DO the same
|
|
3992
|
-
// way the version is read off its env. A fact about the surrounding
|
|
3993
|
-
// dispatch, never envelope data module code could set or branch on.
|
|
3994
|
-
this.causedBy,
|
|
3995
|
-
// #1237: a fact about the surrounding CALL, like the version above.
|
|
3996
|
-
this.invocationId, full.payload === undefined ? null : JSON.stringify(full.payload));
|
|
3997
|
-
},
|
|
4020
|
+
emit: (event) => writeEvent(event, 'module'),
|
|
3998
4021
|
requestPlatform: (request) => {
|
|
3999
4022
|
assertImpersonationWrites(impersonation, 'ctx.requestPlatform');
|
|
4000
4023
|
const input = platformRequestInput.parse(request);
|
|
@@ -4122,23 +4145,19 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4122
4145
|
subject: systemActor ? { kind: 'system', id: systemActor.system } : subject,
|
|
4123
4146
|
now: at,
|
|
4124
4147
|
check: runCheck,
|
|
4125
|
-
emit: (event) =>
|
|
4148
|
+
emit: (event) => writeEvent(event, 'kernel'),
|
|
4126
4149
|
isOperation: (name) => this.operations.has(name),
|
|
4127
4150
|
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
4128
4151
|
minted,
|
|
4129
4152
|
}),
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
}
|
|
4139
|
-
sql.exec(`INSERT OR IGNORE INTO _substrat_tuples (subject, relation, object)
|
|
4140
|
-
VALUES (?, 'parent', ?)`, `${child.entityType}:${child.entityId}`, `${parent.entityType}:${parent.entityId}`);
|
|
4141
|
-
},
|
|
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
|
+
}),
|
|
4142
4161
|
entitlement: async (key) => {
|
|
4143
4162
|
const held = await entitlementReader().listEntitlements(tenantId);
|
|
4144
4163
|
return held.find((e) => e.key === key) ?? null;
|
|
@@ -4162,6 +4181,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4162
4181
|
return sealTo({ keyId: row.key_id, publicKey: row.public_key }, plaintext);
|
|
4163
4182
|
},
|
|
4164
4183
|
};
|
|
4184
|
+
kernelEmitters.set(ctxRef, (event) => writeEvent(event, 'kernel'));
|
|
4165
4185
|
return ctxRef;
|
|
4166
4186
|
}
|
|
4167
4187
|
/** True once this scope has had entitlements projected at least once (#304) — the switch
|