@substrat-run/adapter-sqlite 0.89.0 → 0.90.1
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/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +172 -19
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BlobStoreHandle, type ConnectionId, type ModuleId, type DomainEvent, type PlatformRequestId, type PlatformRequest, type PlatformRequestFilter, type PlatformRequestStatus, type PlatformRequestFailure, type ProjectedConnectionGrant, type PlatformActorId, type PrincipalId, type ScopeDump, type ScopeId, type TenantId, type TenantStoreHandle } from '@substrat-run/contracts';
|
|
1
|
+
import { type BlobStoreHandle, type ConnectionId, type ModuleId, type DomainEvent, type PlatformRequestId, type PlatformRequest, type PlatformRequestFilter, type PlatformRequestStatus, type PlatformRequestFailure, type ProjectedConnectionGrant, type PlatformActorId, type PrincipalId, type ScopeDump, type ScopeId, type ImpersonationSessionId, type TenantId, type TenantStoreHandle } from '@substrat-run/contracts';
|
|
2
2
|
import { type BlobStoreProvisionInput, type ScopeAttachments, type ExecutorDeadLetter, type ExecutorDrainReport, type MigrateScopeOutcome, type MigrationFrontier, type ExecutorHandler, type ExecutorRetryPolicy, type ConnectorHandler, type ConnectorOptions, type Clock, type FetchLike, type SecretBox, type HostAdmin, type ModuleRegistration, type OperationHandler, type PermissionChecker, type ProvisionScopeInput, type ScheduleRegistration, type ScheduleRunReport, type ScopeHost, type ScopeStub, type ScopeStubOptions, type TenantRelationalStore, type TenantStoreProvisionInput } from '@substrat-run/kernel';
|
|
3
3
|
export interface SqliteScopeHostOptions {
|
|
4
4
|
/** Directory holding one SQLite file per scope plus the directory database. */
|
|
@@ -171,10 +171,21 @@ export declare class SqliteScopeHost implements ScopeHost {
|
|
|
171
171
|
*/
|
|
172
172
|
getConnectorScope(connectionId: ConnectionId, scopeId: ScopeId): Promise<ScopeStub>;
|
|
173
173
|
getConnectorAttachments(connectionId: ConnectionId, scopeId: ScopeId): Promise<ScopeAttachments>;
|
|
174
|
+
/** Read one session, or say so. The door and every invoke through it come here. */
|
|
175
|
+
private readImpersonation;
|
|
176
|
+
/**
|
|
177
|
+
* The impersonation door (K-42) — mirror of `getConnectorScope`/`getSystemScope`.
|
|
178
|
+
*
|
|
179
|
+
* The session is the authority and it is read from the directory, never from
|
|
180
|
+
* the caller: a stub cannot be minted by describing a session, only by naming
|
|
181
|
+
* one somebody opened and the admin log recorded. Everything after that is an
|
|
182
|
+
* ordinary invoke against the impersonated principal.
|
|
183
|
+
*/
|
|
184
|
+
getImpersonatedScope(session: ImpersonationSessionId, tenantId: TenantId, scopeId: ScopeId, options?: ScopeStubOptions): Promise<ScopeStub>;
|
|
174
185
|
getSystemScope(moduleId: ModuleId, tenantId: TenantId, scopeId: ScopeId): Promise<ScopeStub>;
|
|
175
186
|
registeredSchedules(): ScheduleRegistration[];
|
|
176
187
|
runDueSchedules(moduleId: ModuleId, tenantId: TenantId, scopeId: ScopeId): Promise<ScheduleRunReport>;
|
|
177
|
-
/** The stub body, shared by the principal and
|
|
188
|
+
/** The stub body, shared by the principal, connection, system and impersonation doors. */
|
|
178
189
|
private buildStub;
|
|
179
190
|
/**
|
|
180
191
|
* The entity a guarded operation's precondition is about (#129).
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAQL,KAAK,eAAe,EAsDpB,KAAK,YAAY,EAGjB,KAAK,QAAQ,EAOb,KAAK,WAAW,EAIhB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAI3B,KAAK,wBAAwB,EAW7B,KAAK,eAAe,EACpB,KAAK,WAAW,EAehB,KAAK,SAAS,EAEd,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
4
4
|
import { accessLogEntry, adminLogEntry, opsFailureEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, platformRequest, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, connection, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, subjectRef, requestFingerprint, createConnectionInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, outboundOfManifestJson, listLimitOf, substratError, } from '@substrat-run/contracts';
|
|
5
|
-
import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, OPS_FAILURE_RETENTION_DAYS, backoffAt, platformRequestHistoryQuery, PLATFORM_REQUEST_COLUMNS, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialBucketRow, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, IDEMPOTENCY_DDL, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, } from '@substrat-run/kernel';
|
|
5
|
+
import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, OPS_FAILURE_RETENTION_DAYS, backoffAt, platformRequestHistoryQuery, PLATFORM_REQUEST_COLUMNS, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialBucketRow, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, IDEMPOTENCY_DDL, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, } from '@substrat-run/kernel';
|
|
6
6
|
import { ScopeActor } from './actor.js';
|
|
7
7
|
import { createTupleChecker } from './checker.js';
|
|
8
8
|
const KERNEL_DDL = `
|
|
@@ -22,6 +22,10 @@ const KERNEL_DDL = `
|
|
|
22
22
|
-- K-34: the checks the emitting operation passed (JSON [{permission, grant?}]).
|
|
23
23
|
-- NULL on rows written before the field existed — honestly unrecorded, not empty.
|
|
24
24
|
authorization TEXT,
|
|
25
|
+
-- K-42: the staff actor + session an impersonated operation ran under (JSON
|
|
26
|
+
-- session/by). NULL is the ordinary case — nobody was impersonating — and the
|
|
27
|
+
-- actor column above stays the principal the permission model answered about.
|
|
28
|
+
impersonation TEXT,
|
|
25
29
|
drained_at TEXT
|
|
26
30
|
);
|
|
27
31
|
-- platform-intents.md: durable intents a vertical enqueues (ctx.requestPlatform) for the platform
|
|
@@ -32,6 +36,8 @@ const KERNEL_DDL = `
|
|
|
32
36
|
kind TEXT NOT NULL,
|
|
33
37
|
payload TEXT NOT NULL,
|
|
34
38
|
requested_by TEXT NOT NULL,
|
|
39
|
+
-- K-42: the staff actor + session, when the intent was raised under one.
|
|
40
|
+
impersonation TEXT,
|
|
35
41
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
36
42
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
37
43
|
last_error TEXT,
|
|
@@ -57,6 +63,8 @@ const KERNEL_DDL = `
|
|
|
57
63
|
tenant_id TEXT NOT NULL,
|
|
58
64
|
scope_id TEXT,
|
|
59
65
|
operation TEXT,
|
|
66
|
+
-- K-42: WHICH of the two actors was refused is exactly what this log is for.
|
|
67
|
+
impersonation TEXT,
|
|
60
68
|
at TEXT NOT NULL,
|
|
61
69
|
drained_at TEXT
|
|
62
70
|
);
|
|
@@ -195,6 +203,7 @@ function rowToPlatformRequest(r) {
|
|
|
195
203
|
kind: r.kind,
|
|
196
204
|
payload: JSON.parse(r.payload),
|
|
197
205
|
requestedBy: JSON.parse(r.requested_by),
|
|
206
|
+
impersonation: r.impersonation == null ? null : JSON.parse(r.impersonation),
|
|
198
207
|
status: r.status,
|
|
199
208
|
attempts: r.attempts,
|
|
200
209
|
lastError: r.last_error,
|
|
@@ -746,6 +755,7 @@ export class SqliteScopeHost {
|
|
|
746
755
|
shredded_at TEXT,
|
|
747
756
|
PRIMARY KEY (scope_id, subject_id)
|
|
748
757
|
);
|
|
758
|
+
${IMPERSONATION_DDL}
|
|
749
759
|
CREATE TABLE IF NOT EXISTS _substrat_admin_log (
|
|
750
760
|
id TEXT PRIMARY KEY,
|
|
751
761
|
actor TEXT NOT NULL,
|
|
@@ -1821,6 +1831,40 @@ export class SqliteScopeHost {
|
|
|
1821
1831
|
const store = this.attachmentStore(conn.tenant_id, scope.vertical);
|
|
1822
1832
|
return this.buildAttachments(rt, { kind: 'connection', id: connectionId }, store);
|
|
1823
1833
|
}
|
|
1834
|
+
/** Read one session, or say so. The door and every invoke through it come here. */
|
|
1835
|
+
readImpersonation(session) {
|
|
1836
|
+
const q = impersonationByIdQuery(session);
|
|
1837
|
+
const row = this.directory.prepare(q.sql).get(...q.params);
|
|
1838
|
+
if (!row)
|
|
1839
|
+
throw new ImpersonationRefused(`unknown impersonation session: ${session}`);
|
|
1840
|
+
return mapImpersonationRow(row);
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* The impersonation door (K-42) — mirror of `getConnectorScope`/`getSystemScope`.
|
|
1844
|
+
*
|
|
1845
|
+
* The session is the authority and it is read from the directory, never from
|
|
1846
|
+
* the caller: a stub cannot be minted by describing a session, only by naming
|
|
1847
|
+
* one somebody opened and the admin log recorded. Everything after that is an
|
|
1848
|
+
* ordinary invoke against the impersonated principal.
|
|
1849
|
+
*/
|
|
1850
|
+
async getImpersonatedScope(session, tenantId, scopeId, options) {
|
|
1851
|
+
const record = this.readImpersonation(session);
|
|
1852
|
+
assertSessionUsable(record, this.clock(), { tenantId, scopeId });
|
|
1853
|
+
// Same lifecycle gates as the principal door, and reached the same way: a
|
|
1854
|
+
// suspended tenant refuses a support session exactly as it refuses a user.
|
|
1855
|
+
const scope = this.directory
|
|
1856
|
+
.prepare('SELECT tenant_id, status FROM scopes WHERE scope_id = ?')
|
|
1857
|
+
.get(scopeId);
|
|
1858
|
+
if (!scope || scope.tenant_id !== tenantId) {
|
|
1859
|
+
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
1860
|
+
}
|
|
1861
|
+
if (scope.status !== 'active') {
|
|
1862
|
+
throw new Error(`scope not active (status: ${scope.status}): ${scopeId}`);
|
|
1863
|
+
}
|
|
1864
|
+
const rt = this.runtime(tenantId, scopeId);
|
|
1865
|
+
await this.applyPendingMigrations(rt);
|
|
1866
|
+
return this.buildStub(tenantId, scopeId, rt, asPrincipal(record.principal), options, record.id);
|
|
1867
|
+
}
|
|
1824
1868
|
async getSystemScope(moduleId, tenantId, scopeId) {
|
|
1825
1869
|
// The scheduler's door (#383) — mirror of getConnectorScope. The module must be
|
|
1826
1870
|
// registered on this host (a schedule can only invoke its own vertical's ops),
|
|
@@ -1922,8 +1966,15 @@ export class SqliteScopeHost {
|
|
|
1922
1966
|
}
|
|
1923
1967
|
return report;
|
|
1924
1968
|
}
|
|
1925
|
-
/** The stub body, shared by the principal and
|
|
1926
|
-
buildStub(tenantId, scopeId, rt, subject, options
|
|
1969
|
+
/** The stub body, shared by the principal, connection, system and impersonation doors. */
|
|
1970
|
+
buildStub(tenantId, scopeId, rt, subject, options,
|
|
1971
|
+
/**
|
|
1972
|
+
* K-42: the session this stub acts under. `subject` is already the
|
|
1973
|
+
* IMPERSONATED principal — that is what makes the permission model answer
|
|
1974
|
+
* about them with no override branch anywhere — and this is what every
|
|
1975
|
+
* record additionally keeps, plus the bound a read-only session is held to.
|
|
1976
|
+
*/
|
|
1977
|
+
sessionId) {
|
|
1927
1978
|
const operations = this.operations;
|
|
1928
1979
|
return {
|
|
1929
1980
|
tenantId,
|
|
@@ -1934,6 +1985,20 @@ export class SqliteScopeHost {
|
|
|
1934
1985
|
// gives it, so a demo and a hosted vertical answer 404 for the same reason.
|
|
1935
1986
|
if (!handler)
|
|
1936
1987
|
return Promise.reject(substratError('not_found', `unknown operation: ${operation}`));
|
|
1988
|
+
// K-42: re-read the session on EVERY invoke, not once when the stub was
|
|
1989
|
+
// minted. A stub is a capability and nothing takes it away, so a session
|
|
1990
|
+
// checked only at the door would be a time box the one caller holding it
|
|
1991
|
+
// never runs out of — and an `endImpersonation` would stop nothing.
|
|
1992
|
+
let session;
|
|
1993
|
+
if (sessionId !== undefined) {
|
|
1994
|
+
try {
|
|
1995
|
+
session = this.readImpersonation(sessionId);
|
|
1996
|
+
assertSessionUsable(session, this.clock(), { tenantId, scopeId });
|
|
1997
|
+
}
|
|
1998
|
+
catch (err) {
|
|
1999
|
+
return Promise.reject(err);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1937
2002
|
// #129. A precondition the operation never declared is REFUSED, not
|
|
1938
2003
|
// ignored. A caller sending `If-Match` believes its write is serialised,
|
|
1939
2004
|
// and quietly dropping the header leaves that belief intact while the
|
|
@@ -1979,7 +2044,7 @@ export class SqliteScopeHost {
|
|
|
1979
2044
|
const invoked = await rt.actor.enqueue(async () => {
|
|
1980
2045
|
// Fresh per operation: the context carries the K-34 authorization accumulator,
|
|
1981
2046
|
// which must not leak across operations (invokes are serialized per scope).
|
|
1982
|
-
const ctx = this.operationContext(rt, subject, undefined, signals);
|
|
2047
|
+
const ctx = this.operationContext(rt, subject, undefined, signals, session);
|
|
1983
2048
|
const clonedInput = structuredClone(input);
|
|
1984
2049
|
// #893: parse, don't trust — at the scope door, from the operation's own
|
|
1985
2050
|
// declaration. BEFORE `BEGIN`, so a malformed call never opens a
|
|
@@ -2070,22 +2135,36 @@ export class SqliteScopeHost {
|
|
|
2070
2135
|
const prune = idempotencyPruneStatement(at);
|
|
2071
2136
|
rt.db.prepare(prune.sql).run(...prune.params);
|
|
2072
2137
|
}
|
|
2073
|
-
|
|
2138
|
+
// K-42: a read-only session's transaction is ROLLED BACK, never
|
|
2139
|
+
// committed — the half of read-only that is a mechanism rather than a
|
|
2140
|
+
// promise. `ctx.emit` and the other effecting verbs refuse outright,
|
|
2141
|
+
// which is what a support engineer sees; this is what holds when a
|
|
2142
|
+
// handler writes a row with plain `ctx.sql.exec` and calls none of
|
|
2143
|
+
// them. Same shape as the scope console's read-only query on the DO.
|
|
2144
|
+
if (session?.mode === 'read-only')
|
|
2145
|
+
rt.db.exec('ROLLBACK');
|
|
2146
|
+
else
|
|
2147
|
+
rt.db.exec('COMMIT');
|
|
2074
2148
|
}
|
|
2075
2149
|
catch (err) {
|
|
2076
2150
|
rt.db.exec('ROLLBACK');
|
|
2077
2151
|
// K-35: a refused check rolled the operation back. Record it now — a fresh
|
|
2078
2152
|
// statement in autocommit, AFTER the rollback, so the denial survives it.
|
|
2079
2153
|
if (err instanceof PermissionDenied)
|
|
2080
|
-
this.recordDenial(rt, subject, operation, err);
|
|
2154
|
+
this.recordDenial(rt, subject, operation, err, session);
|
|
2081
2155
|
throw err;
|
|
2082
2156
|
}
|
|
2083
2157
|
// Post-commit, still inside the actor task: drain outbox → consumers,
|
|
2084
2158
|
// then → executors. Prompt dispatch (K-22 §4.2): the common case
|
|
2085
2159
|
// completes inside this request, with the outbox as the retry backstop
|
|
2086
2160
|
// if it does not.
|
|
2087
|
-
|
|
2088
|
-
|
|
2161
|
+
// A read-only session committed nothing, so there is nothing of its to
|
|
2162
|
+
// drain — and draining anyway would run consumers as a side effect of a
|
|
2163
|
+
// session that may not have side effects.
|
|
2164
|
+
if (session?.mode !== 'read-only') {
|
|
2165
|
+
await this.dispatch(rt);
|
|
2166
|
+
await this.dispatchExecutors(rt);
|
|
2167
|
+
}
|
|
2089
2168
|
return structuredClone(result);
|
|
2090
2169
|
});
|
|
2091
2170
|
if (signals.platformRequests > 0)
|
|
@@ -2471,7 +2550,9 @@ export class SqliteScopeHost {
|
|
|
2471
2550
|
* catch AFTER `ROLLBACK`, so this INSERT runs in autocommit and survives — the whole
|
|
2472
2551
|
* point, since the denial is exactly the write the rolled-back operation could not make.
|
|
2473
2552
|
*/
|
|
2474
|
-
recordDenial(rt, subject, operation, err
|
|
2553
|
+
recordDenial(rt, subject, operation, err,
|
|
2554
|
+
/** K-42: the session the refused call ran under, when it ran under one. */
|
|
2555
|
+
impersonation) {
|
|
2475
2556
|
// Only an ENFORCED denial (assertAllowed, which attaches the checked permission +
|
|
2476
2557
|
// node) is recorded. A module's own hand-thrown `new PermissionDenied('…')` is its
|
|
2477
2558
|
// policy, carries no permission key, and is left to the module.
|
|
@@ -2484,9 +2565,9 @@ export class SqliteScopeHost {
|
|
|
2484
2565
|
: subject.id;
|
|
2485
2566
|
rt.db
|
|
2486
2567
|
.prepare(`INSERT INTO _substrat_denials
|
|
2487
|
-
(id, actor, permission, tenant_id, scope_id, operation, at)
|
|
2488
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
2489
|
-
.run(ulid(), JSON.stringify(actor), err.permission, err.node.tenantId, err.node.scopeId ?? null, operation, new Date().toISOString());
|
|
2568
|
+
(id, actor, permission, tenant_id, scope_id, operation, impersonation, at)
|
|
2569
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
2570
|
+
.run(ulid(), JSON.stringify(actor), err.permission, err.node.tenantId, err.node.scopeId ?? null, operation, impersonation ? JSON.stringify(impersonationStampOf(impersonation)) : null, new Date().toISOString());
|
|
2490
2571
|
}
|
|
2491
2572
|
parseOutboxRow(row) {
|
|
2492
2573
|
return domainEvent.parse({
|
|
@@ -2501,6 +2582,11 @@ export class SqliteScopeHost {
|
|
|
2501
2582
|
piiClass: row.pii_class,
|
|
2502
2583
|
...(row.subject_id ? { subjectId: row.subject_id } : {}),
|
|
2503
2584
|
...(row.authorization ? { authorization: JSON.parse(row.authorization) } : {}),
|
|
2585
|
+
// K-42: the stamp survives the read, so a consumer's event and an executor's
|
|
2586
|
+
// are the same fact the stored row is. Absent rather than null when nobody
|
|
2587
|
+
// was impersonating, because `DomainEvent.impersonation` is optional — the
|
|
2588
|
+
// shape module code never sees is also the shape it cannot branch on.
|
|
2589
|
+
...(row.impersonation ? { impersonation: JSON.parse(row.impersonation) } : {}),
|
|
2504
2590
|
payload: row.payload === null ? undefined : JSON.parse(row.payload),
|
|
2505
2591
|
});
|
|
2506
2592
|
}
|
|
@@ -4242,6 +4328,48 @@ export class SqliteScopeHost {
|
|
|
4242
4328
|
this.recordAccess(actor, 'shredSubject', { tenantId, scopeId }, { subjectId }, redacted.changes);
|
|
4243
4329
|
return receipt;
|
|
4244
4330
|
},
|
|
4331
|
+
// -- impersonation (K-42, #868) ----------------------------------------
|
|
4332
|
+
beginImpersonation: async (actor, input) => {
|
|
4333
|
+
// Fail closed on the scope before minting anything, exactly as the
|
|
4334
|
+
// principal door does: a session naming a scope that is not there is a
|
|
4335
|
+
// credential for nothing, and issuing one anyway means the admin log
|
|
4336
|
+
// records an access that could never have happened.
|
|
4337
|
+
this.assertScope(input.tenantId, input.scopeId);
|
|
4338
|
+
const session = newImpersonationSession(ulid(), actor, input, this.clock());
|
|
4339
|
+
this.directory
|
|
4340
|
+
.prepare(`INSERT INTO _substrat_impersonations
|
|
4341
|
+
(${IMPERSONATION_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
4342
|
+
.run(...impersonationRowValues(session));
|
|
4343
|
+
// BEFORE the session is returned, so the log entry precedes every row it
|
|
4344
|
+
// will ever touch. The reason rides in `after` — it is the field a review
|
|
4345
|
+
// reads first, and a log saying a session opened but not why is half a
|
|
4346
|
+
// record.
|
|
4347
|
+
this.recordAdmin(actor, 'beginImpersonation', { tenantId: session.tenantId, scopeId: session.scopeId }, null, session);
|
|
4348
|
+
return session;
|
|
4349
|
+
},
|
|
4350
|
+
endImpersonation: async (actor, id) => {
|
|
4351
|
+
const before = this.readImpersonation(id);
|
|
4352
|
+
// Idempotent: "stop that session" must not fail because somebody else
|
|
4353
|
+
// already stopped it, and it must not move the moment it stopped.
|
|
4354
|
+
if (before.endedAt !== null)
|
|
4355
|
+
return before;
|
|
4356
|
+
this.directory
|
|
4357
|
+
.prepare('UPDATE _substrat_impersonations SET ended_at = ? WHERE id = ?')
|
|
4358
|
+
.run(this.clock(), id);
|
|
4359
|
+
const after = this.readImpersonation(id);
|
|
4360
|
+
this.recordAdmin(actor, 'endImpersonation', { tenantId: after.tenantId, scopeId: after.scopeId }, before, after);
|
|
4361
|
+
return after;
|
|
4362
|
+
},
|
|
4363
|
+
listImpersonations: async (actor, filter) => {
|
|
4364
|
+
const q = impersonationListQuery(filter, this.clock());
|
|
4365
|
+
const rows = this.directory.prepare(q.sql).all(...q.params);
|
|
4366
|
+
const sessions = rows.map(mapImpersonationRow);
|
|
4367
|
+
// A staff READ, logged like every other one (K-24) — including the
|
|
4368
|
+
// `result_count`, which is what separates "checked one session" from
|
|
4369
|
+
// "enumerated every support session on the platform".
|
|
4370
|
+
this.recordAccess(actor, 'listImpersonations', { tenantId: (filter?.tenantId ?? null) }, filter ?? null, sessions.length);
|
|
4371
|
+
return sessions;
|
|
4372
|
+
},
|
|
4245
4373
|
reapTenant: async (actor, tenantId) => {
|
|
4246
4374
|
// The terminal tenant reap (§4.8), the tenant analogue of reapScope. The
|
|
4247
4375
|
// caller (the reap route / grace-window sweep) has already reaped every scope's
|
|
@@ -5113,7 +5241,14 @@ export class SqliteScopeHost {
|
|
|
5113
5241
|
*/
|
|
5114
5242
|
operationContext(rt, subject, overrideActor,
|
|
5115
5243
|
/** #458: per-invoke tally of `ctx.requestPlatform` calls; absent for consumer dispatch. */
|
|
5116
|
-
signals
|
|
5244
|
+
signals,
|
|
5245
|
+
/**
|
|
5246
|
+
* K-42: the session this operation is running under, if any. Read here and
|
|
5247
|
+
* nowhere module code can reach — a vertical that could see it could branch
|
|
5248
|
+
* on it, and "hide this row when support is looking" is the one behaviour
|
|
5249
|
+
* this whole feature exists to make impossible.
|
|
5250
|
+
*/
|
|
5251
|
+
impersonation) {
|
|
5117
5252
|
const principal = subject.id;
|
|
5118
5253
|
const checker = this.checker;
|
|
5119
5254
|
const relations = this.relations;
|
|
@@ -5124,6 +5259,10 @@ export class SqliteScopeHost {
|
|
|
5124
5259
|
// whatever has passed up to that point. A system/override actor is unconditionally
|
|
5125
5260
|
// allowed, so its checks are not authorizations and are not recorded.
|
|
5126
5261
|
const passed = [];
|
|
5262
|
+
// K-42: the two-actor stamp, computed once per operation. Every record this
|
|
5263
|
+
// operation writes about who did what carries it; nothing module code passes
|
|
5264
|
+
// can add it, change it or drop it.
|
|
5265
|
+
const stamp = impersonation ? impersonationStampOf(impersonation) : undefined;
|
|
5127
5266
|
/**
|
|
5128
5267
|
* The operation's instant (#812), read ONCE when the context is built.
|
|
5129
5268
|
*
|
|
@@ -5200,6 +5339,7 @@ export class SqliteScopeHost {
|
|
|
5200
5339
|
sql: scopedSql(rt.db),
|
|
5201
5340
|
now: () => at,
|
|
5202
5341
|
emit: (event) => {
|
|
5342
|
+
assertImpersonationWrites(impersonation, 'ctx.emit');
|
|
5203
5343
|
const input = domainEventInput.parse(event);
|
|
5204
5344
|
const full = domainEvent.parse({
|
|
5205
5345
|
...input,
|
|
@@ -5214,15 +5354,18 @@ export class SqliteScopeHost {
|
|
|
5214
5354
|
? { connection: subject.id }
|
|
5215
5355
|
: principal),
|
|
5216
5356
|
...(passed.length ? { authorization: passed.map((p) => ({ ...p })) } : {}),
|
|
5357
|
+
...(stamp ? { impersonation: stamp } : {}),
|
|
5217
5358
|
});
|
|
5218
5359
|
rt.db
|
|
5219
5360
|
.prepare(`INSERT INTO _substrat_outbox
|
|
5220
5361
|
(id, type, schema_version, occurred_at, tenant_id, scope_id, actor,
|
|
5221
|
-
entity_type, entity_id, pii_class, subject_id, authorization,
|
|
5222
|
-
|
|
5223
|
-
|
|
5362
|
+
entity_type, entity_id, pii_class, subject_id, authorization,
|
|
5363
|
+
impersonation, payload)
|
|
5364
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
5365
|
+
.run(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.payload === undefined ? null : JSON.stringify(full.payload));
|
|
5224
5366
|
},
|
|
5225
5367
|
requestPlatform: (request) => {
|
|
5368
|
+
assertImpersonationWrites(impersonation, 'ctx.requestPlatform');
|
|
5226
5369
|
const input = platformRequestInput.parse(request);
|
|
5227
5370
|
// Backpressure (platform-intents.md): refuse when the scope already holds too many pending
|
|
5228
5371
|
// intents, so a stuck or runaway vertical cannot flood the platform drain.
|
|
@@ -5241,9 +5384,9 @@ export class SqliteScopeHost {
|
|
|
5241
5384
|
: principal);
|
|
5242
5385
|
rt.db
|
|
5243
5386
|
.prepare(`INSERT INTO _substrat_platform_requests
|
|
5244
|
-
(id, kind, payload, requested_by, status, attempts, requested_at)
|
|
5245
|
-
VALUES (?, ?, ?, ?, 'pending', 0, ?)`)
|
|
5246
|
-
.run(id, input.kind, JSON.stringify(input.payload ?? null), JSON.stringify(requestedBy), at);
|
|
5387
|
+
(id, kind, payload, requested_by, impersonation, status, attempts, requested_at)
|
|
5388
|
+
VALUES (?, ?, ?, ?, ?, 'pending', 0, ?)`)
|
|
5389
|
+
.run(id, input.kind, JSON.stringify(input.payload ?? null), JSON.stringify(requestedBy), stamp ? JSON.stringify(stamp) : null, at);
|
|
5247
5390
|
if (signals)
|
|
5248
5391
|
signals.platformRequests += 1;
|
|
5249
5392
|
return id;
|
|
@@ -5327,6 +5470,7 @@ export class SqliteScopeHost {
|
|
|
5327
5470
|
* guardrail 2 the same way its `check` does.
|
|
5328
5471
|
*/
|
|
5329
5472
|
grant: async (principal, permission, entity) => {
|
|
5473
|
+
assertImpersonationWrites(impersonation, 'ctx.grant');
|
|
5330
5474
|
const held = await runCheck(permission, entity);
|
|
5331
5475
|
if (!held.allowed) {
|
|
5332
5476
|
throw new PermissionDenied(`cannot grant '${permission}' on ${entity.entityType}:${entity.entityId} — ` +
|
|
@@ -5339,6 +5483,7 @@ export class SqliteScopeHost {
|
|
|
5339
5483
|
},
|
|
5340
5484
|
/** Withdraw a grant this caller could have made. Same guardrails, same reason. */
|
|
5341
5485
|
revoke: async (principal, permission, entity) => {
|
|
5486
|
+
assertImpersonationWrites(impersonation, 'ctx.revoke');
|
|
5342
5487
|
const held = await runCheck(permission, entity);
|
|
5343
5488
|
if (!held.allowed) {
|
|
5344
5489
|
throw new PermissionDenied(`cannot revoke '${permission}' on ${entity.entityType}:${entity.entityId} — ` +
|
|
@@ -5350,6 +5495,7 @@ export class SqliteScopeHost {
|
|
|
5350
5495
|
},
|
|
5351
5496
|
atomic: createAtomic(runSub, { passed, signals }),
|
|
5352
5497
|
link: (child, parent) => {
|
|
5498
|
+
assertImpersonationWrites(impersonation, 'ctx.link');
|
|
5353
5499
|
const allowed = relations.get(child.entityType);
|
|
5354
5500
|
if (!allowed?.has(parent.entityType)) {
|
|
5355
5501
|
throw new Error(`undeclared entity relation: ${child.entityType} → ${parent.entityType} ` +
|
|
@@ -5526,6 +5672,13 @@ export class SqliteScopeHost {
|
|
|
5526
5672
|
// already settled reads as "nobody classified this" rather than claiming an origin
|
|
5527
5673
|
// the drain never actually decided.
|
|
5528
5674
|
this.ensureColumn(db, '_substrat_platform_requests', 'last_failure', 'last_failure TEXT');
|
|
5675
|
+
// K-42: the two-actor stamp on the three spine tables that record who did what,
|
|
5676
|
+
// for a scope DB created before impersonation existed. Nullable everywhere, and
|
|
5677
|
+
// the null means "nobody was impersonating" rather than "unrecorded" — every one
|
|
5678
|
+
// of those rows was written when there was no other possibility.
|
|
5679
|
+
this.ensureColumn(db, '_substrat_outbox', 'impersonation', 'impersonation TEXT');
|
|
5680
|
+
this.ensureColumn(db, '_substrat_platform_requests', 'impersonation', 'impersonation TEXT');
|
|
5681
|
+
this.ensureColumn(db, '_substrat_denials', 'impersonation', 'impersonation TEXT');
|
|
5529
5682
|
const appliedMigrations = new Set(db.prepare('SELECT module_id, version FROM _substrat_migrations').all().map((r) => `${r.module_id}@${r.version}`));
|
|
5530
5683
|
const created = { tenantId, scopeId, db, actor: new ScopeActor(), appliedMigrations };
|
|
5531
5684
|
this.scopes.set(key, created);
|