@substrat-run/kernel 0.138.0 → 0.140.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/admin-log-ddl.d.ts +21 -0
- package/dist/admin-log-ddl.d.ts.map +1 -0
- package/dist/admin-log-ddl.js +32 -0
- package/dist/admin-log-ddl.js.map +1 -0
- package/dist/attachment-extractor.d.ts +21 -2
- package/dist/attachment-extractor.d.ts.map +1 -1
- package/dist/attachment-extractor.js +31 -6
- package/dist/attachment-extractor.js.map +1 -1
- package/dist/attachment-text.d.ts +64 -0
- package/dist/attachment-text.d.ts.map +1 -1
- package/dist/attachment-text.js +87 -16
- package/dist/attachment-text.js.map +1 -1
- package/dist/attribution.d.ts +36 -3
- package/dist/attribution.d.ts.map +1 -1
- package/dist/attribution.js +21 -8
- package/dist/attribution.js.map +1 -1
- package/dist/audit-outcome.d.ts +149 -0
- package/dist/audit-outcome.d.ts.map +1 -0
- package/dist/audit-outcome.js +233 -0
- package/dist/audit-outcome.js.map +1 -0
- package/dist/connect-links.d.ts +91 -0
- package/dist/connect-links.d.ts.map +1 -0
- package/dist/connect-links.js +189 -0
- package/dist/connect-links.js.map +1 -0
- package/dist/connect-state.d.ts +12 -3
- package/dist/connect-state.d.ts.map +1 -1
- package/dist/connect-state.js +8 -3
- package/dist/connect-state.js.map +1 -1
- package/dist/derived-object.d.ts +14 -0
- package/dist/derived-object.d.ts.map +1 -0
- package/dist/derived-object.js +2 -0
- package/dist/derived-object.js.map +1 -0
- package/dist/derived-objects.d.ts +108 -0
- package/dist/derived-objects.d.ts.map +1 -0
- package/dist/derived-objects.js +199 -0
- package/dist/derived-objects.js.map +1 -0
- package/dist/email-intent.d.ts +56 -0
- package/dist/email-intent.d.ts.map +1 -0
- package/dist/email-intent.js +71 -0
- package/dist/email-intent.js.map +1 -0
- package/dist/entity-edges.d.ts +6 -1
- package/dist/entity-edges.d.ts.map +1 -1
- package/dist/entity-edges.js +13 -0
- package/dist/entity-edges.js.map +1 -1
- package/dist/entity-grant-shape.d.ts +94 -0
- package/dist/entity-grant-shape.d.ts.map +1 -0
- package/dist/entity-grant-shape.js +329 -0
- package/dist/entity-grant-shape.js.map +1 -0
- package/dist/entity-grant.d.ts +45 -0
- package/dist/entity-grant.d.ts.map +1 -0
- package/dist/entity-grant.js +71 -0
- package/dist/entity-grant.js.map +1 -0
- package/dist/entity-state-reads.d.ts +40 -0
- package/dist/entity-state-reads.d.ts.map +1 -0
- package/dist/entity-state-reads.js +91 -0
- package/dist/entity-state-reads.js.map +1 -0
- package/dist/entity-state.d.ts +204 -0
- package/dist/entity-state.d.ts.map +1 -0
- package/dist/entity-state.js +356 -0
- package/dist/entity-state.js.map +1 -0
- package/dist/entity-trash.d.ts +245 -0
- package/dist/entity-trash.d.ts.map +1 -0
- package/dist/entity-trash.js +380 -0
- package/dist/entity-trash.js.map +1 -0
- package/dist/findings.d.ts +143 -0
- package/dist/findings.d.ts.map +1 -0
- package/dist/findings.js +463 -0
- package/dist/findings.js.map +1 -0
- package/dist/grant-scoped-read.d.ts +41 -0
- package/dist/grant-scoped-read.d.ts.map +1 -0
- package/dist/grant-scoped-read.js +100 -0
- package/dist/grant-scoped-read.js.map +1 -0
- package/dist/index.d.ts +40 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -6
- package/dist/index.js.map +1 -1
- package/dist/kernel-outbox.d.ts +14 -0
- package/dist/kernel-outbox.d.ts.map +1 -0
- package/dist/kernel-outbox.js +37 -0
- package/dist/kernel-outbox.js.map +1 -0
- package/dist/list-index.d.ts +34 -3
- package/dist/list-index.d.ts.map +1 -1
- package/dist/list-index.js +83 -31
- package/dist/list-index.js.map +1 -1
- package/dist/mail-sender.d.ts +78 -0
- package/dist/mail-sender.d.ts.map +1 -0
- package/dist/mail-sender.js +2 -0
- package/dist/mail-sender.js.map +1 -0
- package/dist/membership-executor.d.ts +7 -47
- package/dist/membership-executor.d.ts.map +1 -1
- package/dist/membership-executor.js +28 -53
- package/dist/membership-executor.js.map +1 -1
- package/dist/migration-digest.d.ts +161 -0
- package/dist/migration-digest.d.ts.map +1 -0
- package/dist/migration-digest.js +237 -0
- package/dist/migration-digest.js.map +1 -0
- package/dist/module-erasure.d.ts +156 -0
- package/dist/module-erasure.d.ts.map +1 -0
- package/dist/module-erasure.js +440 -0
- package/dist/module-erasure.js.map +1 -0
- package/dist/module-migrations.d.ts +6 -3
- package/dist/module-migrations.d.ts.map +1 -1
- package/dist/module-migrations.js +12 -5
- package/dist/module-migrations.js.map +1 -1
- package/dist/peer.d.ts +9 -1
- package/dist/peer.d.ts.map +1 -1
- package/dist/peer.js +29 -0
- package/dist/peer.js.map +1 -1
- package/dist/permission-checker.d.ts +6 -0
- package/dist/permission-checker.d.ts.map +1 -1
- package/dist/permission-checker.js.map +1 -1
- package/dist/permission-eval.d.ts +51 -1
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +104 -2
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-sweep.d.ts +8 -2
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +17 -0
- package/dist/platform-sweep.js.map +1 -1
- package/dist/scope-host.d.ts +479 -24
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +82 -7
- package/dist/scope-host.js.map +1 -1
- package/dist/scope-role-admin.d.ts +68 -0
- package/dist/scope-role-admin.d.ts.map +1 -0
- package/dist/scope-role-admin.js +80 -0
- package/dist/scope-role-admin.js.map +1 -0
- package/dist/search-index.d.ts +21 -1
- package/dist/search-index.d.ts.map +1 -1
- package/dist/search-index.js +60 -37
- package/dist/search-index.js.map +1 -1
- package/dist/spine-guard.d.ts +71 -2
- package/dist/spine-guard.d.ts.map +1 -1
- package/dist/spine-guard.js +393 -10
- package/dist/spine-guard.js.map +1 -1
- package/dist/spine-restore.d.ts.map +1 -1
- package/dist/spine-restore.js +5 -0
- package/dist/spine-restore.js.map +1 -1
- package/dist/sql-identifier.d.ts +12 -0
- package/dist/sql-identifier.d.ts.map +1 -0
- package/dist/sql-identifier.js +17 -0
- package/dist/sql-identifier.js.map +1 -0
- package/dist/sql-statements.d.ts +71 -0
- package/dist/sql-statements.d.ts.map +1 -0
- package/dist/sql-statements.js +191 -0
- package/dist/sql-statements.js.map +1 -0
- package/dist/subject-redaction.d.ts +9 -1
- package/dist/subject-redaction.d.ts.map +1 -1
- package/dist/subject-redaction.js.map +1 -1
- package/dist/system-switch-record.d.ts +3 -0
- package/dist/system-switch-record.d.ts.map +1 -1
- package/dist/system-switch-record.js +2 -0
- package/dist/system-switch-record.js.map +1 -1
- package/dist/table-ownership.d.ts +103 -0
- package/dist/table-ownership.d.ts.map +1 -0
- package/dist/table-ownership.js +285 -0
- package/dist/table-ownership.js.map +1 -0
- package/dist/timeline.d.ts +4 -4
- package/dist/timeline.js +4 -4
- package/dist/visible-continuation.d.ts +38 -0
- package/dist/visible-continuation.d.ts.map +1 -0
- package/dist/visible-continuation.js +162 -0
- package/dist/visible-continuation.js.map +1 -0
- package/package.json +2 -2
|
@@ -2,53 +2,6 @@ import { instant, orgId, principalId, z, } from '@substrat-run/contracts';
|
|
|
2
2
|
import { isUnknownRoleError } from './permission-checker.js';
|
|
3
3
|
import { refuseDelivery } from './delivery-refusal.js';
|
|
4
4
|
import {} from './scope-host.js';
|
|
5
|
-
/**
|
|
6
|
-
* The membership executor (K-22 §4.2, membership.md §4.2; #1184) — the out-of-band half of
|
|
7
|
-
* joining and leaving, written once so a host mounts it instead of hand-rolling the effect.
|
|
8
|
-
*
|
|
9
|
-
* Module code asks in its own transaction and this effects after commit, through `HostAdmin`:
|
|
10
|
-
* an invite engine's accept emits `member.add-requested`; a vertical's removal emits
|
|
11
|
-
* `member.remove-requested`. A rolled-back operation leaves no event and nothing to effect.
|
|
12
|
-
* Both are driven inline by the emitting call, with the outbox and `_substrat_deliveries` as
|
|
13
|
-
* the retry backstop, so the common case completes inside the request.
|
|
14
|
-
*
|
|
15
|
-
* **What it effects: a tenant-level role, and nothing else.** An add assigns the invited role
|
|
16
|
-
* at the tenant node; a removal unassigns it. Neither joins nor leaves an org, although both
|
|
17
|
-
* payloads name one (the invites engine keys its invitations by org): what an org confers
|
|
18
|
-
* includes grants in each scope's own store, which no directory unit can bound, so making
|
|
19
|
-
* someone a member of an org is authority this seam does not carry.
|
|
20
|
-
*
|
|
21
|
-
* **Authority.** Every payload field is module-written, so none of them is authority.
|
|
22
|
-
*
|
|
23
|
-
* - An ADD is bounded by the inviter: the kernel-stamped `actor` of the invitation's own
|
|
24
|
-
* `invites.sent` event, asked through the K-21 set comparison at the tenant node. Asked
|
|
25
|
-
* twice: early, to refuse cheaply, and again inside the directory unit that writes the role
|
|
26
|
-
* (`applyMembership`), so a demotion, a grant lost or a role widened in between is not
|
|
27
|
-
* written past. The joiner is the request's own actor, who must have made the invitation's
|
|
28
|
-
* first acceptance, and the request must be the first one naming that invitation: one
|
|
29
|
-
* invitation, one join. The most a module can do is make the principal who actually invoked
|
|
30
|
-
* it look like the sender — the same delegation bound `ctx.grant` has.
|
|
31
|
-
* - A REMOVE is bounded by the remover, the request's own actor (§5.1: removal takes the
|
|
32
|
-
* same bound, or a junior admin could strip a role they could not have granted) — asked the
|
|
33
|
-
* same two ways, so a remover demoted in between removes nobody.
|
|
34
|
-
*
|
|
35
|
-
* **Removal wins.** An add is refused when the joiner was removed after it was requested: by a
|
|
36
|
-
* later `member.remove-requested` on the same membership (ordered by outbox id), or by any
|
|
37
|
-
* tenant-level removal at all — staff's included, and a no-op one — which raises the person's
|
|
38
|
-
* removal fence in the directory unit that revokes. The add's own unit reads the fence, and a
|
|
39
|
-
* fence at or after `occurredAt - MEMBERSHIP_REMOVAL_SKEW_MS` refuses it.
|
|
40
|
-
*
|
|
41
|
-
* A refusal is terminal (`refuseDelivery`): journaled with its reason, never retried, listed
|
|
42
|
-
* by `executorDeadLetters`, and reported to the emitting call through `onExecutorOutcomes`.
|
|
43
|
-
*
|
|
44
|
-
* **The trail.** Admin rows are written by `actor`, the platform identity that executed them,
|
|
45
|
-
* `onBehalfOf` (#977) the person whose authority bounded them, and with `causedBy` = the event
|
|
46
|
-
* id: the correlation id that joins the scope's half of the trail to the directory's. A
|
|
47
|
-
* removal that took nothing writes no row (K-21), but still raises the fence.
|
|
48
|
-
*
|
|
49
|
-
* **Idempotent.** A delivered event never reaches its handler again. A crash between the
|
|
50
|
-
* effect and its journal row re-runs the handler, and every write it makes is idempotent.
|
|
51
|
-
*/
|
|
52
5
|
/** What the add path consumes, and the version it reads. */
|
|
53
6
|
export const MEMBER_ADD_REQUESTED = 'member.add-requested';
|
|
54
7
|
/** What the remove path consumes, and the version it reads. */
|
|
@@ -96,39 +49,57 @@ export function registerMembershipExecutor(host, options) {
|
|
|
96
49
|
'spans the directory and a scope store. A scope role has assignScopeRoleBounded.');
|
|
97
50
|
}
|
|
98
51
|
const id = options.id ?? MEMBERSHIP_EXECUTOR_ID;
|
|
52
|
+
const removeId = membershipRemoveExecutorId(id);
|
|
53
|
+
// The org a request names rides along only on a mount that joins orgs (#2047).
|
|
54
|
+
const orgOf = (request) => (options.orgs === 'join' ? { orgId: request.orgId } : {});
|
|
99
55
|
// Attributed (#977): the person whose authority bounded the write, beside the platform
|
|
100
|
-
// actor that executed it
|
|
101
|
-
|
|
102
|
-
|
|
56
|
+
// actor that executed it — added to the `admin` the dispatch handed the handler, which
|
|
57
|
+
// already carries the event (#2069). A host built before `HostAdmin.attributed` (an adapter
|
|
58
|
+
// at 0.139) hands an admin without it but has the deprecated `host.attributed(…, { causedBy })`,
|
|
59
|
+
// so that form is the fallback, and is why it is kept. A host with neither predates #977 and
|
|
60
|
+
// records no person on any row; that is said aloud rather than lost quietly.
|
|
61
|
+
// `executorId` is the id the calling path is registered under, so a warning names that path.
|
|
62
|
+
const adminFor = (executorId, admin, who, event) => {
|
|
63
|
+
const onBehalfOf = { principal: who, tenantId: event.tenantId };
|
|
64
|
+
if (admin.attributed)
|
|
65
|
+
return admin.attributed(onBehalfOf);
|
|
66
|
+
if (host.attributed)
|
|
67
|
+
return host.attributed(onBehalfOf, { causedBy: event.id }).admin;
|
|
68
|
+
console.warn(`executor:${executorId}: this host cannot attribute an admin to a person; ${event.id}'s rows name no onBehalfOf`);
|
|
69
|
+
return admin;
|
|
70
|
+
};
|
|
71
|
+
host.registerExecutor(id, MEMBER_ADD_REQUESTED, async (admin, event, scope) => {
|
|
103
72
|
const decided = await authorizeAdd(event, scope);
|
|
104
73
|
if ('refused' in decided)
|
|
105
74
|
return decided.refused;
|
|
106
75
|
const { request, inviter } = decided;
|
|
107
76
|
// One directory unit: the fence, the bound asked again, the role and its audit row. A
|
|
108
77
|
// removal or a change of authority lands wholly before it or wholly after it.
|
|
109
|
-
const applied = await adminFor(inviter, event).applyMembership(options.actor, {
|
|
78
|
+
const applied = await adminFor(id, admin, inviter, event).applyMembership(options.actor, {
|
|
110
79
|
op: 'add',
|
|
111
80
|
tenantId: event.tenantId,
|
|
112
81
|
principal: request.principal,
|
|
113
82
|
roleKey: request.roleKey,
|
|
114
83
|
boundedBy: inviter,
|
|
84
|
+
...orgOf(request),
|
|
115
85
|
unlessRemovedSince: instant.parse(new Date(Date.parse(event.occurredAt) - MEMBERSHIP_REMOVAL_SKEW_MS).toISOString()),
|
|
116
86
|
});
|
|
117
87
|
return applied.applied ? undefined : refusalOf(applied, request, 'inviter', inviter);
|
|
118
88
|
}, options.retry);
|
|
119
|
-
host.registerExecutor(
|
|
89
|
+
host.registerExecutor(removeId, MEMBER_REMOVE_REQUESTED, async (admin, event, scope) => {
|
|
120
90
|
const decided = await authorizeRemove(event, scope);
|
|
121
91
|
if ('refused' in decided)
|
|
122
92
|
return decided.refused;
|
|
123
93
|
const { request, remover } = decided;
|
|
124
94
|
// One directory unit: the remover's bound asked again, the revoke, its audit row if it
|
|
125
95
|
// took anything, and the fence — raised even with nothing held, so a pending add sees it.
|
|
126
|
-
const applied = await adminFor(remover, event).applyMembership(options.actor, {
|
|
96
|
+
const applied = await adminFor(removeId, admin, remover, event).applyMembership(options.actor, {
|
|
127
97
|
op: 'remove',
|
|
128
98
|
tenantId: event.tenantId,
|
|
129
99
|
principal: request.principal,
|
|
130
100
|
roleKey: request.roleKey,
|
|
131
101
|
boundedBy: remover,
|
|
102
|
+
...orgOf(request),
|
|
132
103
|
});
|
|
133
104
|
return applied.applied ? undefined : refusalOf(applied, request, 'remover', remover);
|
|
134
105
|
}, options.retry);
|
|
@@ -139,6 +110,10 @@ function refusalOf(result, request, as, who) {
|
|
|
139
110
|
return refuseDelivery(`${request.principal} was removed after this request was made`);
|
|
140
111
|
if ('unknownRole' in result)
|
|
141
112
|
return refuseDelivery(`no such role in this tenant: ${result.unknownRole}`);
|
|
113
|
+
if ('unknownOrg' in result)
|
|
114
|
+
return refuseDelivery(`no such org in this tenant: ${result.unknownOrg}`);
|
|
115
|
+
if ('notMember' in result)
|
|
116
|
+
return refuseDelivery(`the ${as} ${who} is not a member of org ${result.notMember}`);
|
|
142
117
|
return refuseDelivery(`the ${as} ${who} no longer holds ${result.missing.join(', ')}, which '${request.roleKey}' confers`);
|
|
143
118
|
}
|
|
144
119
|
const refused = (reason) => ({ refused: refuseDelivery(reason) });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"membership-executor.js","sourceRoot":"","sources":["../src/membership-executor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,KAAK,EACL,WAAW,EACX,CAAC,
|
|
1
|
+
{"version":3,"file":"membership-executor.js","sourceRoot":"","sources":["../src/membership-executor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { moduleMigrations } from './module-migrations.js';
|
|
2
|
+
import { type SqlMigration } from './scope-host.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a scope's migration journal records beside each version, and the check that reads it
|
|
5
|
+
* (#2066). Shared by both adapters, so the rule and the sentence an operator reads are one.
|
|
6
|
+
*
|
|
7
|
+
* A scope used to record an applied migration by `(module_id, version)` alone, so a later
|
|
8
|
+
* `0025` whose SQL differed from the `0025` it ran was skipped as already applied: a dev
|
|
9
|
+
* database that ran one branch's `0025` and then `main`'s, or a preview that ran an unmerged
|
|
10
|
+
* branch, kept whichever schema it got first, and the first sign was a query failing against
|
|
11
|
+
* a column that was not there. `_substrat_migrations.sql_digest` closes that.
|
|
12
|
+
*
|
|
13
|
+
* - **The digest is SHA-256 over the exact SQL text**, lowercase hex, with no normalisation:
|
|
14
|
+
* not whitespace, not comments, not line endings. Any change is a different migration. A
|
|
15
|
+
* shipped migration's text never changes on `main` (`lint:migrations --check` refuses the
|
|
16
|
+
* edit upstream), so a difference at a scope means the scope ran something else.
|
|
17
|
+
* - **Only a module's AUTHORED migrations are held to it.** The kernel-derived DDL
|
|
18
|
+
* (`search/…`, `state/…`, `list/…`, see `moduleMigrations`) is versioned by its
|
|
19
|
+
* declaration on purpose: the version names what decides the DDL, so the kernel may change
|
|
20
|
+
* how it spells the same declaration between releases. Holding those rows to a digest would
|
|
21
|
+
* fail every scope with a searchable entity closed on the kernel upgrade that respelled a
|
|
22
|
+
* trigger. Their digest is still recorded, as the fact of what ran.
|
|
23
|
+
* - **A row applied before the column carries `'legacy'`, and is accepted.** Nobody measured
|
|
24
|
+
* what it ran, so it gets no digest: backfilling the registered one would write a value
|
|
25
|
+
* nobody measured, and bless the one row this exists to catch if that scope did run
|
|
26
|
+
* something else. The mark is explicit rather than a NULL, because a NULL cannot say where it
|
|
27
|
+
* came from: the rows present when the column arrives are marked (`MIGRATION_DIGEST_MARK_LEGACY`),
|
|
28
|
+
* a dump taken before the column restores as `'legacy'` (the restore derives it), and from
|
|
29
|
+
* then on the fence below refuses a NULL however it is written. So `'legacy'` is provenance,
|
|
30
|
+
* and a NULL — say a dump edited to clear a digest — is refused rather than trusted. Such a
|
|
31
|
+
* row is unprotected for good, which is the price of not lying in the journal.
|
|
32
|
+
*/
|
|
33
|
+
export declare function migrationDigest(sql: string): Promise<string>;
|
|
34
|
+
/** What a journal row applied before digests were recorded carries in `sql_digest` (#2066). */
|
|
35
|
+
export declare const MIGRATION_DIGEST_LEGACY = "legacy";
|
|
36
|
+
/**
|
|
37
|
+
* Marks the rows present when the column arrived. Run right after the column's ALTER on every
|
|
38
|
+
* wake: once the fence is in place it can only ever find those rows, since nothing else can
|
|
39
|
+
* leave a NULL. Literal text, like the fence, so `lint:spine-ddl` and a reviewer read one spelling.
|
|
40
|
+
*/
|
|
41
|
+
export declare const MIGRATION_DIGEST_MARK_LEGACY = "UPDATE _substrat_migrations SET sql_digest = 'legacy' WHERE sql_digest IS NULL";
|
|
42
|
+
/**
|
|
43
|
+
* The fence: no journal row is written, or rewritten, without a digest or the legacy mark — in
|
|
44
|
+
* both adapters' KERNEL_DDL, after the journal table, so `lint:spine-ddl` holds it like any
|
|
45
|
+
* spine trigger.
|
|
46
|
+
*
|
|
47
|
+
* Without it an older writer (an instance still on the previous release during a rollout or a
|
|
48
|
+
* rollback, or a second process over the same SQLite file) inserts a row that omits the
|
|
49
|
+
* column, and SQLite records NULL. With it that INSERT aborts, so the older writer's migration
|
|
50
|
+
* fails and its scope fails closed until code that records the digest serves it, which then
|
|
51
|
+
* applies the migration properly. KERNEL_DDL runs before the column is ALTERed onto a legacy
|
|
52
|
+
* journal, and on a table without the column the INSERT trigger makes any INSERT fail with
|
|
53
|
+
* `no such column`: there is no window in which a NULL can be written. A restore loads its
|
|
54
|
+
* rows under the fence too, so a dump that carries a NULL is refused (`spineRowsInsert` gives a
|
|
55
|
+
* dump from before the column the legacy mark instead).
|
|
56
|
+
*
|
|
57
|
+
* Migration SQL cannot drop it: `assertNoJournalSql` refuses any migration naming the journal.
|
|
58
|
+
* Literal text, no interpolation: `lint:spine-ddl` inlines a kernel fragment one level deep.
|
|
59
|
+
*/
|
|
60
|
+
export declare const MIGRATION_DIGEST_FENCE_DDL = "\n CREATE TRIGGER IF NOT EXISTS _substrat_migrations_digest_required\n BEFORE INSERT ON _substrat_migrations WHEN NEW.sql_digest IS NULL\n BEGIN\n SELECT RAISE(ABORT, 'a migration journal row must carry its sql_digest (#2066)');\n END;\n CREATE TRIGGER IF NOT EXISTS _substrat_migrations_digest_kept\n BEFORE UPDATE OF sql_digest ON _substrat_migrations WHEN NEW.sql_digest IS NULL\n BEGIN\n SELECT RAISE(ABORT, 'a migration journal row must carry its sql_digest (#2066)');\n END;\n";
|
|
61
|
+
/**
|
|
62
|
+
* Refuse migration SQL that names the migration journal (`_substrat_migration…`: the table, its
|
|
63
|
+
* fence, the DO's bookmarks), in any spelling the grammar allows — quoted, any case, schema-
|
|
64
|
+
* qualified — before any of it runs.
|
|
65
|
+
*
|
|
66
|
+
* A migration runs on the kernel's own handle, not `ctx.sql`, so the spine write guard never
|
|
67
|
+
* sees it, and an authored migration that repairs `_substrat_tuples` is a reviewed, linted path
|
|
68
|
+
* (`boundary-lint-allow R4 migration`). The journal is different: a migration that drops the
|
|
69
|
+
* fence, or writes its own journal rows, makes the digest check say whatever it wrote. Nothing
|
|
70
|
+
* legitimate in a module's migration names it, reads included.
|
|
71
|
+
*/
|
|
72
|
+
export declare function assertNoJournalSql(sql: string, what: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Refuse a migration's SQL before any of it runs, if it reaches where a migration must not.
|
|
75
|
+
*
|
|
76
|
+
* A migration runs on the kernel's own handle, not `ctx.sql`, so `ctx.sql`'s guard never sees it.
|
|
77
|
+
* Every migration is held to: no foreign key to the spine (#1898), and nothing that names the
|
|
78
|
+
* journal (`assertNoJournalSql`). An AUTHORED one is also held to the full spine write guard —
|
|
79
|
+
* no write, DDL or same-named TEMP object on any `_substrat_*` table — unless it is one of the
|
|
80
|
+
* exact reviewed texts above. Kernel-derived DDL is exempt from that last rule: creating
|
|
81
|
+
* `_substrat_*` indexes and triggers is what it is for.
|
|
82
|
+
*/
|
|
83
|
+
export declare function assertMigrationSql(sql: string, step: {
|
|
84
|
+
key: string;
|
|
85
|
+
digest: string;
|
|
86
|
+
authored: boolean;
|
|
87
|
+
}): void;
|
|
88
|
+
/**
|
|
89
|
+
* Refuse a dump whose journal is not consistent with its own shape (#2066 r3): a table whose
|
|
90
|
+
* recorded DDL declares `sql_digest` while its columns omit it, or the other way round.
|
|
91
|
+
*
|
|
92
|
+
* A dump exported before digests were recorded restores as `'legacy'` (`spineRowsInsert`), so
|
|
93
|
+
* the shape of the journal is what says which kind of dump this is — and a current dump with
|
|
94
|
+
* the column stripped would otherwise pass as legacy and unprotect every row. The dump's DDL is
|
|
95
|
+
* `sqlite_master`'s text, which an ALTER rewrites, so a real pre-#2066 journal carries neither.
|
|
96
|
+
* Nothing else a dump carries changed with #2066 to cross-check against (the fence is a trigger,
|
|
97
|
+
* and a dump holds tables). A dump is freely editable data: a consistent edit of both is not
|
|
98
|
+
* detectable, and restore is privileged, audited staff work that could forge any digest anyway.
|
|
99
|
+
* This catches corruption and partial edits, not an author set on lying.
|
|
100
|
+
*/
|
|
101
|
+
export declare function assertJournalDumpCoherent(tables: readonly {
|
|
102
|
+
name: string;
|
|
103
|
+
ddl: string;
|
|
104
|
+
columns: readonly string[];
|
|
105
|
+
}[]): void;
|
|
106
|
+
/** One migration a host applies, with its digest and whether it is held to it. */
|
|
107
|
+
export interface MigrationStep {
|
|
108
|
+
migration: SqlMigration;
|
|
109
|
+
digest: string;
|
|
110
|
+
/** Authored by the module, so held to its digest; false for kernel-derived DDL. */
|
|
111
|
+
authored: boolean;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A module's migrations in the order the host applies them (`moduleMigrations`), each with its
|
|
115
|
+
* digest and whether it is authored — read from the registration as it is now. The digest of a
|
|
116
|
+
* text is computed once per isolate, so a Durable Object that wakes with the same code hashes
|
|
117
|
+
* nothing again.
|
|
118
|
+
*/
|
|
119
|
+
export declare function migrationSteps(registration: Parameters<typeof moduleMigrations>[0]): Promise<readonly MigrationStep[]>;
|
|
120
|
+
/** A step a migration pass still has to run, with the module it belongs to. */
|
|
121
|
+
export interface PendingMigration extends MigrationStep {
|
|
122
|
+
moduleId: string;
|
|
123
|
+
}
|
|
124
|
+
/** What a migration pass has to do, or the divergence that fails the scope closed instead. */
|
|
125
|
+
export type MigrationPlan = {
|
|
126
|
+
pending: PendingMigration[];
|
|
127
|
+
diverged?: undefined;
|
|
128
|
+
} | {
|
|
129
|
+
pending: [];
|
|
130
|
+
diverged: {
|
|
131
|
+
version: string;
|
|
132
|
+
error: string;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Plan a scope's migration pass against its journal (`module@version` → recorded digest): the
|
|
137
|
+
* registered steps it has not applied, or the first applied one whose SQL differs, which fails
|
|
138
|
+
* the scope closed before anything else runs. Both adapters plan through this.
|
|
139
|
+
*/
|
|
140
|
+
export declare function planMigrations(modules: Iterable<{
|
|
141
|
+
readonly id: string;
|
|
142
|
+
readonly steps: Promise<readonly MigrationStep[]>;
|
|
143
|
+
}>, applied: ReadonlyMap<string, string | null>): Promise<MigrationPlan>;
|
|
144
|
+
/** The error a scope fails closed with when `module@version` cannot be applied or trusted. */
|
|
145
|
+
export declare function migrationFailedError(key: string, cause: string): Error;
|
|
146
|
+
/**
|
|
147
|
+
* Why a registered migration the scope has already applied cannot be trusted, or null when it
|
|
148
|
+
* can: on an authored migration, a recorded digest that differs from the registered one, or
|
|
149
|
+
* no digest and no legacy mark at all.
|
|
150
|
+
*
|
|
151
|
+
* The caller fails the scope closed with this as the cause, the way a migration that threw
|
|
152
|
+
* does — the same `migration failed for <module>@<version> — scope fails closed:` record, so
|
|
153
|
+
* the directory, the sweep and the dashboard read it with nothing new. The scope recovers
|
|
154
|
+
* when its journal and its schema agree with the deployment again: a scope that ran another
|
|
155
|
+
* branch's migration is rebuilt (a dev database, a preview) or restored to a dump taken before
|
|
156
|
+
* it ran; a deployment carrying the wrong SQL is redeployed with the SQL the scope ran. A
|
|
157
|
+
* migration is never edited or renumbered once a scope has it, so re-numbering is not a
|
|
158
|
+
* recovery: the scope would still hold the other SQL under this version.
|
|
159
|
+
*/
|
|
160
|
+
export declare function migrationDivergence(recorded: string | null | undefined, registered: string, authored: boolean): string | null;
|
|
161
|
+
//# sourceMappingURL=migration-digest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-digest.d.ts","sourceRoot":"","sources":["../src/migration-digest.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAoB,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAKtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE5D;AAED,+FAA+F;AAC/F,eAAO,MAAM,uBAAuB,WAAW,CAAC;AAEhD;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,mFAAmF,CAAC;AAE7H;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,qfAWtC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CASlE;AAiBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GAAG,IAAI,CAY9G;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAAE,GAAG,IAAI,CAY5H;AAED,kFAAkF;AAClF,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,QAAQ,EAAE,OAAO,CAAC;CACnB;AAcD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAStH;AAED,+EAA+E;AAC/E,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACrD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,8FAA8F;AAC9F,MAAM,MAAM,aAAa,GACrB;IAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;CAAE,GACrD;IAAE,OAAO,EAAE,EAAE,CAAC;IAAC,QAAQ,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAElE;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,QAAQ,CAAC;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,aAAa,EAAE,CAAC,CAAA;CAAE,CAAC,EAC7F,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,GAC1C,OAAO,CAAC,aAAa,CAAC,CAcxB;AAED,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAEtE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACnC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,OAAO,GAChB,MAAM,GAAG,IAAI,CAgBf"}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { substratError, tokenizeSql } from '@substrat-run/contracts';
|
|
2
|
+
import { moduleMigrations } from './module-migrations.js';
|
|
3
|
+
import { assertNoSpineReference, assertNoSpineWrite } from './spine-guard.js';
|
|
4
|
+
import { attachmentSha256 } from './scope-host.js';
|
|
5
|
+
/**
|
|
6
|
+
* What a scope's migration journal records beside each version, and the check that reads it
|
|
7
|
+
* (#2066). Shared by both adapters, so the rule and the sentence an operator reads are one.
|
|
8
|
+
*
|
|
9
|
+
* A scope used to record an applied migration by `(module_id, version)` alone, so a later
|
|
10
|
+
* `0025` whose SQL differed from the `0025` it ran was skipped as already applied: a dev
|
|
11
|
+
* database that ran one branch's `0025` and then `main`'s, or a preview that ran an unmerged
|
|
12
|
+
* branch, kept whichever schema it got first, and the first sign was a query failing against
|
|
13
|
+
* a column that was not there. `_substrat_migrations.sql_digest` closes that.
|
|
14
|
+
*
|
|
15
|
+
* - **The digest is SHA-256 over the exact SQL text**, lowercase hex, with no normalisation:
|
|
16
|
+
* not whitespace, not comments, not line endings. Any change is a different migration. A
|
|
17
|
+
* shipped migration's text never changes on `main` (`lint:migrations --check` refuses the
|
|
18
|
+
* edit upstream), so a difference at a scope means the scope ran something else.
|
|
19
|
+
* - **Only a module's AUTHORED migrations are held to it.** The kernel-derived DDL
|
|
20
|
+
* (`search/…`, `state/…`, `list/…`, see `moduleMigrations`) is versioned by its
|
|
21
|
+
* declaration on purpose: the version names what decides the DDL, so the kernel may change
|
|
22
|
+
* how it spells the same declaration between releases. Holding those rows to a digest would
|
|
23
|
+
* fail every scope with a searchable entity closed on the kernel upgrade that respelled a
|
|
24
|
+
* trigger. Their digest is still recorded, as the fact of what ran.
|
|
25
|
+
* - **A row applied before the column carries `'legacy'`, and is accepted.** Nobody measured
|
|
26
|
+
* what it ran, so it gets no digest: backfilling the registered one would write a value
|
|
27
|
+
* nobody measured, and bless the one row this exists to catch if that scope did run
|
|
28
|
+
* something else. The mark is explicit rather than a NULL, because a NULL cannot say where it
|
|
29
|
+
* came from: the rows present when the column arrives are marked (`MIGRATION_DIGEST_MARK_LEGACY`),
|
|
30
|
+
* a dump taken before the column restores as `'legacy'` (the restore derives it), and from
|
|
31
|
+
* then on the fence below refuses a NULL however it is written. So `'legacy'` is provenance,
|
|
32
|
+
* and a NULL — say a dump edited to clear a digest — is refused rather than trusted. Such a
|
|
33
|
+
* row is unprotected for good, which is the price of not lying in the journal.
|
|
34
|
+
*/
|
|
35
|
+
export function migrationDigest(sql) {
|
|
36
|
+
return attachmentSha256(new TextEncoder().encode(sql));
|
|
37
|
+
}
|
|
38
|
+
/** What a journal row applied before digests were recorded carries in `sql_digest` (#2066). */
|
|
39
|
+
export const MIGRATION_DIGEST_LEGACY = 'legacy';
|
|
40
|
+
/**
|
|
41
|
+
* Marks the rows present when the column arrived. Run right after the column's ALTER on every
|
|
42
|
+
* wake: once the fence is in place it can only ever find those rows, since nothing else can
|
|
43
|
+
* leave a NULL. Literal text, like the fence, so `lint:spine-ddl` and a reviewer read one spelling.
|
|
44
|
+
*/
|
|
45
|
+
export const MIGRATION_DIGEST_MARK_LEGACY = "UPDATE _substrat_migrations SET sql_digest = 'legacy' WHERE sql_digest IS NULL";
|
|
46
|
+
/**
|
|
47
|
+
* The fence: no journal row is written, or rewritten, without a digest or the legacy mark — in
|
|
48
|
+
* both adapters' KERNEL_DDL, after the journal table, so `lint:spine-ddl` holds it like any
|
|
49
|
+
* spine trigger.
|
|
50
|
+
*
|
|
51
|
+
* Without it an older writer (an instance still on the previous release during a rollout or a
|
|
52
|
+
* rollback, or a second process over the same SQLite file) inserts a row that omits the
|
|
53
|
+
* column, and SQLite records NULL. With it that INSERT aborts, so the older writer's migration
|
|
54
|
+
* fails and its scope fails closed until code that records the digest serves it, which then
|
|
55
|
+
* applies the migration properly. KERNEL_DDL runs before the column is ALTERed onto a legacy
|
|
56
|
+
* journal, and on a table without the column the INSERT trigger makes any INSERT fail with
|
|
57
|
+
* `no such column`: there is no window in which a NULL can be written. A restore loads its
|
|
58
|
+
* rows under the fence too, so a dump that carries a NULL is refused (`spineRowsInsert` gives a
|
|
59
|
+
* dump from before the column the legacy mark instead).
|
|
60
|
+
*
|
|
61
|
+
* Migration SQL cannot drop it: `assertNoJournalSql` refuses any migration naming the journal.
|
|
62
|
+
* Literal text, no interpolation: `lint:spine-ddl` inlines a kernel fragment one level deep.
|
|
63
|
+
*/
|
|
64
|
+
export const MIGRATION_DIGEST_FENCE_DDL = `
|
|
65
|
+
CREATE TRIGGER IF NOT EXISTS _substrat_migrations_digest_required
|
|
66
|
+
BEFORE INSERT ON _substrat_migrations WHEN NEW.sql_digest IS NULL
|
|
67
|
+
BEGIN
|
|
68
|
+
SELECT RAISE(ABORT, 'a migration journal row must carry its sql_digest (#2066)');
|
|
69
|
+
END;
|
|
70
|
+
CREATE TRIGGER IF NOT EXISTS _substrat_migrations_digest_kept
|
|
71
|
+
BEFORE UPDATE OF sql_digest ON _substrat_migrations WHEN NEW.sql_digest IS NULL
|
|
72
|
+
BEGIN
|
|
73
|
+
SELECT RAISE(ABORT, 'a migration journal row must carry its sql_digest (#2066)');
|
|
74
|
+
END;
|
|
75
|
+
`;
|
|
76
|
+
/**
|
|
77
|
+
* Refuse migration SQL that names the migration journal (`_substrat_migration…`: the table, its
|
|
78
|
+
* fence, the DO's bookmarks), in any spelling the grammar allows — quoted, any case, schema-
|
|
79
|
+
* qualified — before any of it runs.
|
|
80
|
+
*
|
|
81
|
+
* A migration runs on the kernel's own handle, not `ctx.sql`, so the spine write guard never
|
|
82
|
+
* sees it, and an authored migration that repairs `_substrat_tuples` is a reviewed, linted path
|
|
83
|
+
* (`boundary-lint-allow R4 migration`). The journal is different: a migration that drops the
|
|
84
|
+
* fence, or writes its own journal rows, makes the digest check say whatever it wrote. Nothing
|
|
85
|
+
* legitimate in a module's migration names it, reads included.
|
|
86
|
+
*/
|
|
87
|
+
export function assertNoJournalSql(sql, what) {
|
|
88
|
+
for (const token of tokenizeSql(sql)) {
|
|
89
|
+
if (!token.text.split('.').some((part) => part.toLowerCase().startsWith('_substrat_migration')))
|
|
90
|
+
continue;
|
|
91
|
+
throw substratError('forbidden', `${what} cannot name the migration journal ('${token.text}'): the journal and its digest fence are the kernel's (#2066)`, { reason: 'spine_write' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The authored migrations allowed to write the spine: four shipped ticket0 repairs of its own
|
|
96
|
+
* `_substrat_tuples` edges and its hand-recreated derived list indexes, each reviewed under
|
|
97
|
+
* `boundary-lint-allow R4 migration`. Keyed by module, version AND the digest of the exact text,
|
|
98
|
+
* so only those texts pass: a new migration, or any edit to one of these, gets the full guard.
|
|
99
|
+
* Shipped migrations are append-only, so this list only ever shrinks — a new spine repair goes
|
|
100
|
+
* through the platform, not a migration.
|
|
101
|
+
*/
|
|
102
|
+
const REVIEWED_SPINE_MIGRATIONS = new Set([
|
|
103
|
+
'@substrat-run/demo-ticket0@0020#d29122b68f3cc5d07a70fb87874085e0e6805e2e19612984160488c59789d75c',
|
|
104
|
+
'@substrat-run/demo-ticket0@0022#9de9088207fa914e9eb8c8344924708655cbecb11e5b64f76e1d2ec7d0bd0ea2',
|
|
105
|
+
'@substrat-run/demo-ticket0@0024#ba60103ae91108bfce21fd5b24818cec18aefe4ee3b03b49ca8013fd2634aae3',
|
|
106
|
+
'@substrat-run/demo-ticket0@0026#da0bb4e3c8b13f5febd8b2202e7267c30e9ee91a19d202181b99ccdeb71ec3a4',
|
|
107
|
+
]);
|
|
108
|
+
/**
|
|
109
|
+
* Refuse a migration's SQL before any of it runs, if it reaches where a migration must not.
|
|
110
|
+
*
|
|
111
|
+
* A migration runs on the kernel's own handle, not `ctx.sql`, so `ctx.sql`'s guard never sees it.
|
|
112
|
+
* Every migration is held to: no foreign key to the spine (#1898), and nothing that names the
|
|
113
|
+
* journal (`assertNoJournalSql`). An AUTHORED one is also held to the full spine write guard —
|
|
114
|
+
* no write, DDL or same-named TEMP object on any `_substrat_*` table — unless it is one of the
|
|
115
|
+
* exact reviewed texts above. Kernel-derived DDL is exempt from that last rule: creating
|
|
116
|
+
* `_substrat_*` indexes and triggers is what it is for.
|
|
117
|
+
*/
|
|
118
|
+
export function assertMigrationSql(sql, step) {
|
|
119
|
+
const what = `migration ${step.key}`;
|
|
120
|
+
assertNoSpineReference(sql, what);
|
|
121
|
+
assertNoJournalSql(sql, what);
|
|
122
|
+
if (!step.authored || REVIEWED_SPINE_MIGRATIONS.has(`${step.key}#${step.digest}`))
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
assertNoSpineWrite(sql);
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
throw substratError('forbidden', `${what} cannot write the platform spine: ${err.message.replace(/^ctx\.sql cannot write the platform spine: /, '')}`, {
|
|
129
|
+
reason: 'spine_write',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Refuse a dump whose journal is not consistent with its own shape (#2066 r3): a table whose
|
|
135
|
+
* recorded DDL declares `sql_digest` while its columns omit it, or the other way round.
|
|
136
|
+
*
|
|
137
|
+
* A dump exported before digests were recorded restores as `'legacy'` (`spineRowsInsert`), so
|
|
138
|
+
* the shape of the journal is what says which kind of dump this is — and a current dump with
|
|
139
|
+
* the column stripped would otherwise pass as legacy and unprotect every row. The dump's DDL is
|
|
140
|
+
* `sqlite_master`'s text, which an ALTER rewrites, so a real pre-#2066 journal carries neither.
|
|
141
|
+
* Nothing else a dump carries changed with #2066 to cross-check against (the fence is a trigger,
|
|
142
|
+
* and a dump holds tables). A dump is freely editable data: a consistent edit of both is not
|
|
143
|
+
* detectable, and restore is privileged, audited staff work that could forge any digest anyway.
|
|
144
|
+
* This catches corruption and partial edits, not an author set on lying.
|
|
145
|
+
*/
|
|
146
|
+
export function assertJournalDumpCoherent(tables) {
|
|
147
|
+
for (const t of tables) {
|
|
148
|
+
if (t.name.toLowerCase() !== '_substrat_migrations')
|
|
149
|
+
continue;
|
|
150
|
+
const declared = tokenizeSql(t.ddl).some((tok) => tok.text.toLowerCase() === 'sql_digest');
|
|
151
|
+
const carried = t.columns.some((c) => c.toLowerCase() === 'sql_digest');
|
|
152
|
+
if (declared === carried)
|
|
153
|
+
continue;
|
|
154
|
+
throw substratError('validation_failed', `restore refused: the dump's _substrat_migrations ${declared ? 'declares sql_digest in its DDL but carries no such column' : 'carries sql_digest but its DDL does not declare it'} — ` +
|
|
155
|
+
`it is neither a dump from before digests were recorded nor a whole one from after. Nothing was changed (#2066).`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Digests by SQL text. Keyed on the content, not on the registration object: a registration
|
|
160
|
+
* mutated or rebuilt with other SQL under the same version gets that SQL's digest, never a
|
|
161
|
+
* stale one. Bounded by the distinct migration texts the isolate has registered.
|
|
162
|
+
*/
|
|
163
|
+
const digestOf = new Map();
|
|
164
|
+
const cachedDigest = (sql) => {
|
|
165
|
+
let digest = digestOf.get(sql);
|
|
166
|
+
if (!digest)
|
|
167
|
+
digestOf.set(sql, (digest = migrationDigest(sql)));
|
|
168
|
+
return digest;
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* A module's migrations in the order the host applies them (`moduleMigrations`), each with its
|
|
172
|
+
* digest and whether it is authored — read from the registration as it is now. The digest of a
|
|
173
|
+
* text is computed once per isolate, so a Durable Object that wakes with the same code hashes
|
|
174
|
+
* nothing again.
|
|
175
|
+
*/
|
|
176
|
+
export function migrationSteps(registration) {
|
|
177
|
+
const authored = new Set((registration.migrations ?? []).map((m) => m.version));
|
|
178
|
+
return Promise.all(moduleMigrations(registration).map(async (migration) => ({
|
|
179
|
+
migration,
|
|
180
|
+
digest: await cachedDigest(migration.sql),
|
|
181
|
+
authored: authored.has(migration.version),
|
|
182
|
+
})));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Plan a scope's migration pass against its journal (`module@version` → recorded digest): the
|
|
186
|
+
* registered steps it has not applied, or the first applied one whose SQL differs, which fails
|
|
187
|
+
* the scope closed before anything else runs. Both adapters plan through this.
|
|
188
|
+
*/
|
|
189
|
+
export async function planMigrations(modules, applied) {
|
|
190
|
+
const pending = [];
|
|
191
|
+
for (const mod of modules) {
|
|
192
|
+
for (const step of await mod.steps) {
|
|
193
|
+
const key = `${mod.id}@${step.migration.version}`;
|
|
194
|
+
if (!applied.has(key)) {
|
|
195
|
+
pending.push({ moduleId: mod.id, ...step });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const error = migrationDivergence(applied.get(key), step.digest, step.authored);
|
|
199
|
+
if (error)
|
|
200
|
+
return { pending: [], diverged: { version: key, error } };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { pending };
|
|
204
|
+
}
|
|
205
|
+
/** The error a scope fails closed with when `module@version` cannot be applied or trusted. */
|
|
206
|
+
export function migrationFailedError(key, cause) {
|
|
207
|
+
return new Error(`migration failed for ${key} — scope fails closed: ${cause}`);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Why a registered migration the scope has already applied cannot be trusted, or null when it
|
|
211
|
+
* can: on an authored migration, a recorded digest that differs from the registered one, or
|
|
212
|
+
* no digest and no legacy mark at all.
|
|
213
|
+
*
|
|
214
|
+
* The caller fails the scope closed with this as the cause, the way a migration that threw
|
|
215
|
+
* does — the same `migration failed for <module>@<version> — scope fails closed:` record, so
|
|
216
|
+
* the directory, the sweep and the dashboard read it with nothing new. The scope recovers
|
|
217
|
+
* when its journal and its schema agree with the deployment again: a scope that ran another
|
|
218
|
+
* branch's migration is rebuilt (a dev database, a preview) or restored to a dump taken before
|
|
219
|
+
* it ran; a deployment carrying the wrong SQL is redeployed with the SQL the scope ran. A
|
|
220
|
+
* migration is never edited or renumbered once a scope has it, so re-numbering is not a
|
|
221
|
+
* recovery: the scope would still hold the other SQL under this version.
|
|
222
|
+
*/
|
|
223
|
+
export function migrationDivergence(recorded, registered, authored) {
|
|
224
|
+
if (!authored || recorded === undefined || recorded === registered || recorded === MIGRATION_DIGEST_LEGACY)
|
|
225
|
+
return null;
|
|
226
|
+
if (recorded === null) {
|
|
227
|
+
// The fence makes this unreachable through the kernel; a row that has it anyway was not
|
|
228
|
+
// written by the kernel, and says nothing about what ran.
|
|
229
|
+
return (`this scope's journal records neither a digest nor the legacy mark for this version, so ` +
|
|
230
|
+
`what it ran is unknown (registered sha256 ${registered}). Rebuild the scope, or restore it ` +
|
|
231
|
+
`to a dump the kernel wrote (#2066)`);
|
|
232
|
+
}
|
|
233
|
+
return (`this scope applied different SQL under this version (applied sha256 ${recorded}, ` +
|
|
234
|
+
`registered sha256 ${registered}). Rebuild the scope, or restore it to a dump taken before ` +
|
|
235
|
+
`it ran the other SQL; a migration is never edited or renumbered once a scope has applied it (#2066)`);
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=migration-digest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-digest.js","sourceRoot":"","sources":["../src/migration-digest.ts"],"names":[],"mappings":"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"}
|