@substrat-run/adapter-sqlite 0.82.0 → 0.84.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/index.d.ts +24 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +175 -11
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type BlobStoreHandle, type ConnectionId, type ModuleId, type DomainEvent, type PlatformRequestId, type PlatformRequest, type PlatformRequestFilter, type PlatformRequestStatus, type PlatformActorId, type PrincipalId, type ScopeDump, type ScopeId, type TenantId, type TenantStoreHandle } from '@substrat-run/contracts';
|
|
2
|
-
import { type BlobStoreProvisionInput, type ScopeAttachments, type ExecutorDeadLetter, type ExecutorDrainReport, type MigrateScopeOutcome, type MigrationFrontier, type ExecutorHandler, type ExecutorRetryPolicy, type ConnectorHandler, type ConnectorOptions, 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';
|
|
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';
|
|
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. */
|
|
5
5
|
dir: string;
|
|
@@ -20,6 +20,15 @@ export interface SqliteScopeHostOptions {
|
|
|
20
20
|
* provider will not produce on demand.
|
|
21
21
|
*/
|
|
22
22
|
fetch?: FetchLike;
|
|
23
|
+
/**
|
|
24
|
+
* What `ctx.now()` reads (#812). Defaults to the wall clock.
|
|
25
|
+
*
|
|
26
|
+
* The same seam as `fetch`, for the same reason: a scenario that needs an
|
|
27
|
+
* absence window to lapse, a metering period to roll, or a booking hold to
|
|
28
|
+
* expire cannot get there by waiting. Hand in a frozen or scripted clock and
|
|
29
|
+
* the interesting case becomes assertable.
|
|
30
|
+
*/
|
|
31
|
+
clock?: Clock;
|
|
23
32
|
}
|
|
24
33
|
export declare class SqliteScopeHost implements ScopeHost {
|
|
25
34
|
readonly admin: HostAdmin;
|
|
@@ -43,6 +52,8 @@ export declare class SqliteScopeHost implements ScopeHost {
|
|
|
43
52
|
private readonly relations;
|
|
44
53
|
/** entityType → its derived FTS5 index (#827). One entity type, one index. */
|
|
45
54
|
private readonly searchPlans;
|
|
55
|
+
/** #811: the paged lists modules declare, by entity type. Same one-owner rule as search. */
|
|
56
|
+
private readonly listPlans;
|
|
46
57
|
/** entityType → the declared attachment gate (#473): read key + write key (default: read). */
|
|
47
58
|
private readonly attachmentTargets;
|
|
48
59
|
/** operation name → its owning module's entitlementKey (§4.3 gate). */
|
|
@@ -60,6 +71,7 @@ export declare class SqliteScopeHost implements ScopeHost {
|
|
|
60
71
|
private readonly systemPrincipal;
|
|
61
72
|
private readonly secretBox;
|
|
62
73
|
private readonly fetchImpl;
|
|
74
|
+
private readonly clock;
|
|
63
75
|
constructor(options: SqliteScopeHostOptions);
|
|
64
76
|
/**
|
|
65
77
|
* The directory schema, applied on every open — and again after a restore (#40).
|
|
@@ -181,6 +193,15 @@ export declare class SqliteScopeHost implements ScopeHost {
|
|
|
181
193
|
dispatchConnector(tenantId: TenantId, scopeId: ScopeId, handler: ConnectorHandler, event: DomainEvent, options?: {
|
|
182
194
|
timeoutMs?: number;
|
|
183
195
|
}): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* The scope's own answer to "what may this connection do here" (#726 gap 1) — read
|
|
198
|
+
* from the delivered `connection:<id>` tuples, which is the same row the checker
|
|
199
|
+
* walks, so this cannot disagree with what would be enforced.
|
|
200
|
+
*
|
|
201
|
+
* Live only: a tombstoned tuple (K-21) and one past `expires_at` are both unenforced,
|
|
202
|
+
* and reporting either would make the read a worse answer than no read at all.
|
|
203
|
+
*/
|
|
204
|
+
connectionGrantsInScope(tenantId: TenantId, scopeId: ScopeId): Promise<ProjectedConnectionGrant[]>;
|
|
184
205
|
migrationFrontier(): MigrationFrontier;
|
|
185
206
|
/**
|
|
186
207
|
* The reconciliation sweep's wake + retry (kernel-design §5.3, #49). Reuses
|
|
@@ -199,6 +220,7 @@ export declare class SqliteScopeHost implements ScopeHost {
|
|
|
199
220
|
status: PlatformRequestStatus;
|
|
200
221
|
result?: unknown;
|
|
201
222
|
lastError?: string | null;
|
|
223
|
+
failure?: PlatformRequestFailure | null;
|
|
202
224
|
}): Promise<void>;
|
|
203
225
|
private dispatch;
|
|
204
226
|
/**
|
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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
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, createConnectionInput, 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, } from '@substrat-run/contracts';
|
|
5
|
-
import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, OPS_FAILURE_RETENTION_DAYS, backoffAt, platformRequestHistoryQuery, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, } from '@substrat-run/kernel';
|
|
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, 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, } from '@substrat-run/contracts';
|
|
5
|
+
import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, OPS_FAILURE_RETENTION_DAYS, backoffAt, platformRequestHistoryQuery, PLATFORM_REQUEST_COLUMNS, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, } from '@substrat-run/kernel';
|
|
6
6
|
import { ScopeActor } from './actor.js';
|
|
7
7
|
import { createTupleChecker } from './checker.js';
|
|
8
8
|
const KERNEL_DDL = `
|
|
@@ -35,6 +35,12 @@ const KERNEL_DDL = `
|
|
|
35
35
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
36
36
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
37
37
|
last_error TEXT,
|
|
38
|
+
-- #841: WHO refused, as JSON {origin, code, permission}. last_error says WHAT
|
|
39
|
+
-- happened and always did; this says whether it was the provider's answer or our
|
|
40
|
+
-- own refusal before egress -- the distinction the dashboard was guessing wrong.
|
|
41
|
+
-- NULL = never classified (a row settled before this column, or one that never
|
|
42
|
+
-- failed), which is not the same fact as an origin of 'unknown'.
|
|
43
|
+
last_failure TEXT,
|
|
38
44
|
result TEXT,
|
|
39
45
|
requested_at TEXT NOT NULL,
|
|
40
46
|
settled_at TEXT
|
|
@@ -187,11 +193,49 @@ function rowToPlatformRequest(r) {
|
|
|
187
193
|
status: r.status,
|
|
188
194
|
attempts: r.attempts,
|
|
189
195
|
lastError: r.last_error,
|
|
196
|
+
failure: r.last_failure == null ? null : JSON.parse(r.last_failure),
|
|
190
197
|
result: r.result === null ? null : JSON.parse(r.result),
|
|
191
198
|
requestedAt: r.requested_at,
|
|
192
199
|
settledAt: r.settled_at,
|
|
193
200
|
});
|
|
194
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* The dispatch capability (#726 remedy B): a connector delivery may read the attachments
|
|
204
|
+
* of the entity the DELIVERED EVENT names, and nothing else.
|
|
205
|
+
*
|
|
206
|
+
* The read a signing connector needs is inherently per-dispatch — the event names one
|
|
207
|
+
* `documentAttachmentId`, `bindDocument` already refused to bind an attachment owned by
|
|
208
|
+
* anything but the instance being signed, and `openAttachment` takes an id rather than a
|
|
209
|
+
* search. Modelling that as a standing scope-wide `protocol:read` grant was the mismatch
|
|
210
|
+
* this closes: the grant site has no entity leg, so the narrow check was being asked a
|
|
211
|
+
* question the grant model could only answer broadly — and `protocol:read` also gates
|
|
212
|
+
* `protocol/get`, `list-templates` and `list-for-entity`, none of which a connector
|
|
213
|
+
* sending one named document has any business reaching.
|
|
214
|
+
*
|
|
215
|
+
* **Derived here, never asserted by the caller.** The entity comes from this scope's own
|
|
216
|
+
* kernel-stamped spine row, looked up by event id. On the hosted path the platform runs
|
|
217
|
+
* the connector, so an entity passed across the seam would be the platform's word for
|
|
218
|
+
* what it is allowed to read; an event id it must resolve against the deployment's own
|
|
219
|
+
* outbox is not. The platform can name any delivery; it cannot name an entity.
|
|
220
|
+
*
|
|
221
|
+
* **No fallback to the permission check.** Inside a dispatch this IS the authority. A
|
|
222
|
+
* grant would only re-widen what the whole point is to narrow, and there is no read a
|
|
223
|
+
* connector legitimately makes during a delivery that the delivery does not name.
|
|
224
|
+
*/
|
|
225
|
+
const admitByDelivery = (rt, eventId, row) => {
|
|
226
|
+
const event = rt.db
|
|
227
|
+
.prepare('SELECT entity_type, entity_id FROM _substrat_outbox WHERE id = ?')
|
|
228
|
+
.get(eventId);
|
|
229
|
+
if (!event) {
|
|
230
|
+
throw new PermissionDenied(`attachment ${row.id}: delivery ${eventId} is not an event of this scope, so it ` +
|
|
231
|
+
`carries no authority to read anything here`);
|
|
232
|
+
}
|
|
233
|
+
if (event.entity_type !== row.entity_type || event.entity_id !== row.entity_id) {
|
|
234
|
+
throw new PermissionDenied(`attachment ${row.id} belongs to ${row.entity_type}/${row.entity_id}, and this ` +
|
|
235
|
+
`delivery is for ${event.entity_type}/${event.entity_id} — a connector may read ` +
|
|
236
|
+
`the attachments of the entity its event names, and no others`);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
195
239
|
export class SqliteScopeHost {
|
|
196
240
|
admin;
|
|
197
241
|
dir;
|
|
@@ -214,6 +258,8 @@ export class SqliteScopeHost {
|
|
|
214
258
|
relations = new Map();
|
|
215
259
|
/** entityType → its derived FTS5 index (#827). One entity type, one index. */
|
|
216
260
|
searchPlans = new Map();
|
|
261
|
+
/** #811: the paged lists modules declare, by entity type. Same one-owner rule as search. */
|
|
262
|
+
listPlans = new Map();
|
|
217
263
|
/** entityType → the declared attachment gate (#473): read key + write key (default: read). */
|
|
218
264
|
attachmentTargets = new Map();
|
|
219
265
|
/** operation name → its owning module's entitlementKey (§4.3 gate). */
|
|
@@ -231,9 +277,11 @@ export class SqliteScopeHost {
|
|
|
231
277
|
systemPrincipal = principalId.parse(ulid());
|
|
232
278
|
secretBox;
|
|
233
279
|
fetchImpl;
|
|
280
|
+
clock;
|
|
234
281
|
constructor(options) {
|
|
235
282
|
this.secretBox = options.secretBox ?? unconfiguredSecretBox;
|
|
236
283
|
this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
284
|
+
this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
|
|
237
285
|
this.dir = options.dir;
|
|
238
286
|
mkdirSync(this.dir, { recursive: true });
|
|
239
287
|
this.directory = new Database(join(this.dir, '_directory.sqlite'));
|
|
@@ -756,7 +804,9 @@ export class SqliteScopeHost {
|
|
|
756
804
|
* than assuming the worse case is what keeps the platform-dispatch path under the
|
|
757
805
|
* K-6 serialization the adapter promises.
|
|
758
806
|
*/
|
|
759
|
-
connectorContext(rt, timeoutMs, holdsActor
|
|
807
|
+
connectorContext(rt, timeoutMs, holdsActor,
|
|
808
|
+
/** The delivery this context is FOR (#726) — the dispatch capability's whole basis. */
|
|
809
|
+
eventId) {
|
|
760
810
|
const vertical = this.directory
|
|
761
811
|
.prepare('SELECT vertical FROM scopes WHERE scope_id = ?')
|
|
762
812
|
.get(rt.scopeId)?.vertical ?? null;
|
|
@@ -789,10 +839,19 @@ export class SqliteScopeHost {
|
|
|
789
839
|
// serialized turn and stays under K-6 like every other reader. Assuming
|
|
790
840
|
// the reentrant case everywhere would have dropped serialization on a path
|
|
791
841
|
// that never needed it.
|
|
842
|
+
// #726 gap 1: this connection's live grants IN THIS SCOPE, so a connector can
|
|
843
|
+
// assert its preconditions at the top of a dispatch rather than discovering a
|
|
844
|
+
// missing grant as a refusal several calls later.
|
|
845
|
+
grants: async () => (await this.connectionGrantsInScope(rt.tenantId, rt.scopeId))
|
|
846
|
+
.filter((g) => g.connectionId === open.id)
|
|
847
|
+
.map((g) => g.permission),
|
|
792
848
|
openAttachment: async (attachmentId) => {
|
|
793
849
|
const store = this.attachmentStore(rt.tenantId, vertical);
|
|
794
850
|
return this.buildAttachments(rt, { kind: 'connection', id: open.id }, store, {
|
|
795
851
|
reentrant: holdsActor,
|
|
852
|
+
// #726 remedy (B): inside a dispatch the authority to read is the DELIVERY,
|
|
853
|
+
// not a standing grant. See `admitByEvent` on `buildAttachments`.
|
|
854
|
+
admitByEvent: eventId,
|
|
796
855
|
}).open(attachmentId);
|
|
797
856
|
},
|
|
798
857
|
// #687: open a cell the scope sealed TO this connection. The keyId-indexed
|
|
@@ -896,9 +955,27 @@ export class SqliteScopeHost {
|
|
|
896
955
|
}
|
|
897
956
|
this.searchPlans.set(plan.entityType, plan);
|
|
898
957
|
}
|
|
958
|
+
// #811: the list indexes this module's `lists` declare — same placement and
|
|
959
|
+
// the same reason as the search indexes above: appended AFTER the module's
|
|
960
|
+
// own migrations, so `CREATE INDEX` names a table that exists.
|
|
961
|
+
const listMigrations = listIndexMigrations(manifest.id, manifest.lists);
|
|
962
|
+
for (const m of listMigrations) {
|
|
963
|
+
if (seen.has(m.version)) {
|
|
964
|
+
throw new Error(`duplicate migration version in ${manifest.id}: ${m.version}`);
|
|
965
|
+
}
|
|
966
|
+
seen.add(m.version);
|
|
967
|
+
}
|
|
968
|
+
for (const plan of listIndexPlans(manifest.id, manifest.lists)) {
|
|
969
|
+
const existing = this.listPlans.get(plan.entityType);
|
|
970
|
+
if (existing) {
|
|
971
|
+
throw new Error(`list: '${plan.entityType}' declares a paged list in both '${existing.moduleId}' and ` +
|
|
972
|
+
`'${plan.moduleId}' — one entity type, one walk; rename one`);
|
|
973
|
+
}
|
|
974
|
+
this.listPlans.set(plan.entityType, plan);
|
|
975
|
+
}
|
|
899
976
|
this.modules.set(manifest.id, {
|
|
900
977
|
id: manifest.id,
|
|
901
|
-
migrations: [...migrations, ...searchMigrations],
|
|
978
|
+
migrations: [...migrations, ...searchMigrations, ...listMigrations],
|
|
902
979
|
consumers,
|
|
903
980
|
schedules: manifest.schedules ?? [],
|
|
904
981
|
});
|
|
@@ -1362,6 +1439,10 @@ export class SqliteScopeHost {
|
|
|
1362
1439
|
.get(attachmentId);
|
|
1363
1440
|
if (!row)
|
|
1364
1441
|
return null;
|
|
1442
|
+
if (opts.admitByEvent !== undefined) {
|
|
1443
|
+
admitByDelivery(rt, opts.admitByEvent, row);
|
|
1444
|
+
return rowToRecord(row);
|
|
1445
|
+
}
|
|
1365
1446
|
const gate = targetGate(row.entity_type);
|
|
1366
1447
|
assertAllowed(await ctx.check(gate.read, { entityType: row.entity_type, entityId: row.entity_id }));
|
|
1367
1448
|
return rowToRecord(row);
|
|
@@ -1919,7 +2000,7 @@ export class SqliteScopeHost {
|
|
|
1919
2000
|
if (executor.kind === 'connector') {
|
|
1920
2001
|
// `true`: dispatchExecutors is only ever reached from inside
|
|
1921
2002
|
// `rt.actor.enqueue` (invoke's post-commit tail, or drainDue).
|
|
1922
|
-
await executor.handler(this.connectorContext(rt, executor.timeoutMs, true), event);
|
|
2003
|
+
await executor.handler(this.connectorContext(rt, executor.timeoutMs, true, event.id), event);
|
|
1923
2004
|
}
|
|
1924
2005
|
else {
|
|
1925
2006
|
await executor.handler(this.admin, event);
|
|
@@ -1986,12 +2067,47 @@ export class SqliteScopeHost {
|
|
|
1986
2067
|
try {
|
|
1987
2068
|
// `false`: this path deliberately does NOT enqueue, so nothing is held and
|
|
1988
2069
|
// the connection's reads take an ordinary serialized turn.
|
|
1989
|
-
await handler(this.connectorContext(rt, options?.timeoutMs ?? 30_000, false), event);
|
|
2070
|
+
await handler(this.connectorContext(rt, options?.timeoutMs ?? 30_000, false, event.id), event);
|
|
1990
2071
|
}
|
|
1991
2072
|
finally {
|
|
1992
2073
|
this.causedBy = null;
|
|
1993
2074
|
}
|
|
1994
2075
|
}
|
|
2076
|
+
/**
|
|
2077
|
+
* The scope's own answer to "what may this connection do here" (#726 gap 1) — read
|
|
2078
|
+
* from the delivered `connection:<id>` tuples, which is the same row the checker
|
|
2079
|
+
* walks, so this cannot disagree with what would be enforced.
|
|
2080
|
+
*
|
|
2081
|
+
* Live only: a tombstoned tuple (K-21) and one past `expires_at` are both unenforced,
|
|
2082
|
+
* and reporting either would make the read a worse answer than no read at all.
|
|
2083
|
+
*/
|
|
2084
|
+
async connectionGrantsInScope(tenantId, scopeId) {
|
|
2085
|
+
const rt = this.runtime(tenantId, scopeId);
|
|
2086
|
+
await this.applyPendingMigrations(rt);
|
|
2087
|
+
const now = new Date().toISOString();
|
|
2088
|
+
// BOTH tuple stores, because a scope check consults both: rule 2 inheritance means
|
|
2089
|
+
// a tenant-level grant (`grantToConnection` with `scopeId: null`) is enforced here
|
|
2090
|
+
// exactly as a scope-level one is. Reading only the scope's own table would answer
|
|
2091
|
+
// "not granted" where the checker answers allow — a read-back that disagrees with
|
|
2092
|
+
// enforcement is worse than none, since it is the read an operator would trust.
|
|
2093
|
+
const rows = [
|
|
2094
|
+
...rt.db
|
|
2095
|
+
.prepare(`SELECT subject, relation, expires_at FROM _substrat_tuples
|
|
2096
|
+
WHERE subject LIKE 'connection:%' AND relation LIKE 'granted:%'
|
|
2097
|
+
AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`)
|
|
2098
|
+
.all(now),
|
|
2099
|
+
...this.directory
|
|
2100
|
+
.prepare(`SELECT subject, relation, expires_at FROM _substrat_tenant_tuples
|
|
2101
|
+
WHERE tenant_id = ? AND subject LIKE 'connection:%' AND relation LIKE 'granted:%'
|
|
2102
|
+
AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`)
|
|
2103
|
+
.all(tenantId, now),
|
|
2104
|
+
].sort((a, b) => a.subject.localeCompare(b.subject) || a.relation.localeCompare(b.relation));
|
|
2105
|
+
return rows.map((r) => projectedConnectionGrant.parse({
|
|
2106
|
+
connectionId: r.subject.slice('connection:'.length),
|
|
2107
|
+
permission: r.relation.slice('granted:'.length),
|
|
2108
|
+
...(r.expires_at ? { expiresAt: r.expires_at } : {}),
|
|
2109
|
+
}));
|
|
2110
|
+
}
|
|
1995
2111
|
migrationFrontier() {
|
|
1996
2112
|
let total = 0;
|
|
1997
2113
|
for (const mod of this.modules.values())
|
|
@@ -2077,7 +2193,7 @@ export class SqliteScopeHost {
|
|
|
2077
2193
|
const rt = this.runtime(tenantId, scopeId);
|
|
2078
2194
|
await this.applyPendingMigrations(rt);
|
|
2079
2195
|
const rows = rt.db
|
|
2080
|
-
.prepare(`SELECT
|
|
2196
|
+
.prepare(`SELECT ${PLATFORM_REQUEST_COLUMNS}
|
|
2081
2197
|
FROM _substrat_platform_requests WHERE status = 'pending' ORDER BY id`)
|
|
2082
2198
|
.all();
|
|
2083
2199
|
return rows.map(rowToPlatformRequest);
|
|
@@ -2094,10 +2210,10 @@ export class SqliteScopeHost {
|
|
|
2094
2210
|
await rt.actor.enqueue(() => {
|
|
2095
2211
|
rt.db
|
|
2096
2212
|
.prepare(`UPDATE _substrat_platform_requests
|
|
2097
|
-
SET status = ?, result = COALESCE(?, result), last_error = ?,
|
|
2213
|
+
SET status = ?, result = COALESCE(?, result), last_error = ?, last_failure = ?,
|
|
2098
2214
|
attempts = attempts + 1, settled_at = ?
|
|
2099
2215
|
WHERE id = ?`)
|
|
2100
|
-
.run(outcome.status, outcome.result === undefined ? null : JSON.stringify(outcome.result), outcome.lastError ?? null, outcome.status === 'pending' ? null : new Date().toISOString(), id);
|
|
2216
|
+
.run(outcome.status, outcome.result === undefined ? null : JSON.stringify(outcome.result), outcome.lastError ?? null, outcome.failure == null ? null : JSON.stringify(outcome.failure), outcome.status === 'pending' ? null : new Date().toISOString(), id);
|
|
2101
2217
|
});
|
|
2102
2218
|
}
|
|
2103
2219
|
async dispatch(rt) {
|
|
@@ -4767,11 +4883,23 @@ export class SqliteScopeHost {
|
|
|
4767
4883
|
const checker = this.checker;
|
|
4768
4884
|
const relations = this.relations;
|
|
4769
4885
|
const searchPlans = this.searchPlans;
|
|
4886
|
+
const listPlans = this.listPlans;
|
|
4770
4887
|
// K-34: the checks that passed in THIS operation. The context is created per invoke
|
|
4771
4888
|
// (see buildStub), so this accumulates one operation's authorizations; `emit` snapshots
|
|
4772
4889
|
// whatever has passed up to that point. A system/override actor is unconditionally
|
|
4773
4890
|
// allowed, so its checks are not authorizations and are not recorded.
|
|
4774
4891
|
const passed = [];
|
|
4892
|
+
/**
|
|
4893
|
+
* The operation's instant (#812), read ONCE when the context is built.
|
|
4894
|
+
*
|
|
4895
|
+
* Everything the operation stamps — its own rows via `ctx.now()`, the
|
|
4896
|
+
* `occurredAt` on every event it emits, the `requested_at` on every platform
|
|
4897
|
+
* intent it enqueues — comes from here, so a row and the event announcing it
|
|
4898
|
+
* can never disagree about when. Reading the clock per-call would have made
|
|
4899
|
+
* `ctx.now()` deterministic only in the sense that each reading is separately
|
|
4900
|
+
* unpredictable.
|
|
4901
|
+
*/
|
|
4902
|
+
const at = this.clock();
|
|
4775
4903
|
/**
|
|
4776
4904
|
* The scope host's half of `ctx.atomic` (#770) — everything else is the
|
|
4777
4905
|
* kernel's (`createAtomic`). SQLite gives us savepoints, which nest inside
|
|
@@ -4835,12 +4963,13 @@ export class SqliteScopeHost {
|
|
|
4835
4963
|
scopeId: rt.scopeId,
|
|
4836
4964
|
principal,
|
|
4837
4965
|
sql: scopedSql(rt.db),
|
|
4966
|
+
now: () => at,
|
|
4838
4967
|
emit: (event) => {
|
|
4839
4968
|
const input = domainEventInput.parse(event);
|
|
4840
4969
|
const full = domainEvent.parse({
|
|
4841
4970
|
...input,
|
|
4842
4971
|
id: eventId.parse(ulid()),
|
|
4843
|
-
occurredAt:
|
|
4972
|
+
occurredAt: at,
|
|
4844
4973
|
tenantId: rt.tenantId,
|
|
4845
4974
|
scopeId: rt.scopeId,
|
|
4846
4975
|
actor: overrideActor ??
|
|
@@ -4879,7 +5008,7 @@ export class SqliteScopeHost {
|
|
|
4879
5008
|
.prepare(`INSERT INTO _substrat_platform_requests
|
|
4880
5009
|
(id, kind, payload, requested_by, status, attempts, requested_at)
|
|
4881
5010
|
VALUES (?, ?, ?, ?, 'pending', 0, ?)`)
|
|
4882
|
-
.run(id, input.kind, JSON.stringify(input.payload ?? null), JSON.stringify(requestedBy),
|
|
5011
|
+
.run(id, input.kind, JSON.stringify(input.payload ?? null), JSON.stringify(requestedBy), at);
|
|
4883
5012
|
if (signals)
|
|
4884
5013
|
signals.platformRequests += 1;
|
|
4885
5014
|
return id;
|
|
@@ -4904,6 +5033,37 @@ export class SqliteScopeHost {
|
|
|
4904
5033
|
const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit));
|
|
4905
5034
|
return rt.db.prepare(q.sql).all(...q.params).map((row) => ({ entityType, id: row.id, rank: row.rank }));
|
|
4906
5035
|
},
|
|
5036
|
+
/**
|
|
5037
|
+
* #811. The plan is registration state, the indexes are scope state — same
|
|
5038
|
+
* split as search, and the same consequence: an entity nobody declared is
|
|
5039
|
+
* `NotListable` (a wiring mistake), while a declared one whose indexes are
|
|
5040
|
+
* missing in THIS scope cannot happen, because the DDL rides the same
|
|
5041
|
+
* journal as the module's own migrations.
|
|
5042
|
+
*/
|
|
5043
|
+
page: (entityType, params) => {
|
|
5044
|
+
const plan = listPlans.get(entityType);
|
|
5045
|
+
if (!plan)
|
|
5046
|
+
throw new NotListable(entityType);
|
|
5047
|
+
const limit = listLimitOf(params.limit);
|
|
5048
|
+
const q = listQuery(plan, {
|
|
5049
|
+
limit,
|
|
5050
|
+
sort: params.sort,
|
|
5051
|
+
order: params.order,
|
|
5052
|
+
cursor: params.cursor,
|
|
5053
|
+
filters: params.filters,
|
|
5054
|
+
});
|
|
5055
|
+
const rows = rt.db.prepare(q.sql).all(...q.params);
|
|
5056
|
+
// A FULL page may have more; a short one is the end of the walk. Same rule
|
|
5057
|
+
// as `pageOf`, applied here because the cursor is a COLUMN's value (plus
|
|
5058
|
+
// the tie-break) rather than a field the caller could name.
|
|
5059
|
+
const last = rows.length >= limit ? rows[rows.length - 1] : undefined;
|
|
5060
|
+
const nextCursor = last === undefined ? null : cursorOf(last, q.sortColumn, plan.idColumn);
|
|
5061
|
+
const page = { entries: rows, nextCursor };
|
|
5062
|
+
if (!params.total)
|
|
5063
|
+
return page;
|
|
5064
|
+
const counted = rt.db.prepare(q.countSql).all(...q.countParams);
|
|
5065
|
+
return { ...page, total: counted[0]?.n ?? 0 };
|
|
5066
|
+
},
|
|
4907
5067
|
/**
|
|
4908
5068
|
* Narrow a permission this caller already holds onto one entity (#K-sharing).
|
|
4909
5069
|
*
|
|
@@ -5119,6 +5279,10 @@ export class SqliteScopeHost {
|
|
|
5119
5279
|
// so legacy outbox rows read as "unrecorded" — the honest value. (_substrat_denials
|
|
5120
5280
|
// is a whole new table, so KERNEL_DDL's IF NOT EXISTS covers it with no ALTER.)
|
|
5121
5281
|
this.ensureColumn(db, '_substrat_outbox', 'authorization', 'authorization TEXT');
|
|
5282
|
+
// #841: refusal attribution on a scope DB that predates it. Nullable, so every row
|
|
5283
|
+
// already settled reads as "nobody classified this" rather than claiming an origin
|
|
5284
|
+
// the drain never actually decided.
|
|
5285
|
+
this.ensureColumn(db, '_substrat_platform_requests', 'last_failure', 'last_failure TEXT');
|
|
5122
5286
|
const appliedMigrations = new Set(db.prepare('SELECT module_id, version FROM _substrat_migrations').all().map((r) => `${r.module_id}@${r.version}`));
|
|
5123
5287
|
const created = { tenantId, scopeId, db, actor: new ScopeActor(), appliedMigrations };
|
|
5124
5288
|
this.scopes.set(key, created);
|