@serfab/cadre-core 1.16.0 → 1.18.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/README.md +4 -3
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +81 -142
- package/dist/cadre-invite-protocol.js +168 -458
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +1089 -475
- package/dist/cadre-node.js +2170 -1037
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +88 -32
- package/dist/control-cohort.js +91 -33
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +856 -318
- package/dist/control-database.js +1422 -701
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +11 -4
- package/dist/control-protocol-guard.js +20 -5
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +97 -0
- package/dist/control-schema-version.js +225 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +262 -78
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +45 -10
- package/dist/control-write-retry.js +92 -22
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +27 -17
- package/dist/index.js +39 -17
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +29 -12
- package/dist/joined-strand-store.js +66 -27
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +256 -0
- package/dist/member-redemption.js +490 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +251 -0
- package/dist/owner-grant-protocol.js +344 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +89 -20
- package/dist/peer-join-backfill.js +265 -108
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +162 -133
- package/dist/seed-bootstrap.js +300 -283
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +24 -2
- package/dist/strand-database.js +29 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -88
- package/dist/strand-first-sync-gate.js +288 -121
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +27 -18
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +30 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +45 -22
- package/dist/strand-membership-writer.js +226 -131
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +36 -32
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +374 -150
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +17 -8
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +740 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +2402 -1079
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +204 -128
- package/src/control-database.ts +1749 -820
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +23 -6
- package/src/control-schema-version.ts +301 -0
- package/src/control-schema.ts +273 -78
- package/src/control-write-retry.ts +641 -567
- package/src/delegate-admission.ts +351 -314
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +899 -789
- package/src/joined-strand-store.ts +73 -28
- package/src/key-store.ts +10 -4
- package/src/link-budget.ts +9 -7
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +649 -0
- package/src/membership-view.ts +144 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +547 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-authorization.ts +558 -488
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +343 -349
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +352 -319
- package/src/strand-first-sync-gate.ts +334 -133
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +33 -25
- package/src/strand-membership-writer.ts +2093 -1974
- package/src/strand-move.ts +50 -38
- package/src/strand-peer-redial.ts +2 -3
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +391 -152
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
*
|
|
15
15
|
* ## The classes
|
|
16
16
|
*
|
|
17
|
-
* - `stranger-open` — seed delivery, strand formation, cadre-invitation redemption
|
|
18
|
-
* each handler makes its own trust decision in-protocol (a seed
|
|
19
|
-
* anchored trust policy, a formation token, an invitation's
|
|
17
|
+
* - `stranger-open` — seed delivery, strand formation, cadre-invitation redemption, owner-grant
|
|
18
|
+
* redemption. Any peer; each handler makes its own trust decision in-protocol (a seed
|
|
19
|
+
* signature against the anchored trust policy, a formation token, an invitation's or a
|
|
20
|
+
* grant's possession proof).
|
|
20
21
|
* - `transport` — libp2p plumbing (identify, ping, hole punching, AutoNAT, the circuit-relay
|
|
21
22
|
* stop and hop protocols, WebRTC signaling). Every connection runs these, the redeeming
|
|
22
23
|
* device's own included: refusing identify would make that device treat the member as
|
|
@@ -81,6 +82,7 @@ import type { Connection, Libp2p, Stream, StreamHandler, StreamHandlerRecord } f
|
|
|
81
82
|
import { SEED_PROTOCOL } from './seed-bootstrap.js';
|
|
82
83
|
import { FORMATION_PROTOCOL } from './strand-formation-protocol.js';
|
|
83
84
|
import { CADRE_INVITE_PROTOCOL } from './cadre-invite-protocol.js';
|
|
85
|
+
import { OWNER_GRANT_PROTOCOL } from './owner-grant-protocol.js';
|
|
84
86
|
import { WAKE_PROTOCOL } from './strand-wake-protocol.js';
|
|
85
87
|
import { STRAND_ADDR_PROTOCOL } from './strand-addr-protocol.js';
|
|
86
88
|
import { withTimeout } from './control-stream.js';
|
|
@@ -89,7 +91,7 @@ const log = debug('sereus:cadre:control-protocol-guard');
|
|
|
89
91
|
|
|
90
92
|
/** What a peer must be to open a stream on a control-node protocol (see the module doc). */
|
|
91
93
|
export type ControlProtocolClass =
|
|
92
|
-
/** Any peer. The handler makes its own trust decision in-protocol (seed, formation, cadre-invite). */
|
|
94
|
+
/** Any peer. The handler makes its own trust decision in-protocol (seed, formation, cadre-invite, owner-grant). */
|
|
93
95
|
| 'stranger-open'
|
|
94
96
|
/** libp2p plumbing that a stranger's connection needs or that carries no party data. */
|
|
95
97
|
| 'transport'
|
|
@@ -133,6 +135,21 @@ export function controlNetworkName(partyId: string): string {
|
|
|
133
135
|
return `control-${partyId}`;
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/** The control database's two storage protocols, `repo` and `cluster`. */
|
|
139
|
+
function controlDatabaseProtocols(partyId: string): readonly string[] {
|
|
140
|
+
const prefix = `/optimystic/${controlNetworkName(partyId)}`;
|
|
141
|
+
return [`${prefix}/repo/1.0.0`, `${prefix}/cluster/1.0.0`];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether a peer whose protocol list is `protocols` serves the control database. Optimystic
|
|
146
|
+
* counts a peer into a control block's cohort by the same test (`servesThisNetwork` in db-p2p's
|
|
147
|
+
* `libp2p-key-network.ts`), before its membership check.
|
|
148
|
+
*/
|
|
149
|
+
export function servesControlDatabase(partyId: string, protocols: readonly string[]): boolean {
|
|
150
|
+
return controlDatabaseProtocols(partyId).some((protocol) => protocols.includes(protocol));
|
|
151
|
+
}
|
|
152
|
+
|
|
136
153
|
/**
|
|
137
154
|
* The class of every protocol a control node serves, by exact protocol id: the single
|
|
138
155
|
* declaration the guard enforces. See the module doc for what each class admits and why each
|
|
@@ -145,12 +162,12 @@ export function controlProtocolClasses(partyId: string): ReadonlyMap<string, Con
|
|
|
145
162
|
[SEED_PROTOCOL, 'stranger-open'],
|
|
146
163
|
[FORMATION_PROTOCOL, 'stranger-open'],
|
|
147
164
|
[CADRE_INVITE_PROTOCOL, 'stranger-open'],
|
|
165
|
+
[OWNER_GRANT_PROTOCOL, 'stranger-open'],
|
|
148
166
|
|
|
149
167
|
[WAKE_PROTOCOL, 'members'],
|
|
150
168
|
[STRAND_ADDR_PROTOCOL, 'members'],
|
|
151
169
|
|
|
152
|
-
|
|
153
|
-
[`${prefix}/cluster/1.0.0`, 'members-snapshot'],
|
|
170
|
+
...controlDatabaseProtocols(partyId).map((protocol): [string, ControlProtocolClass] => [protocol, 'members-snapshot']),
|
|
154
171
|
[`${prefix}/db-p2p/sync/1.0.0`, 'members-snapshot'],
|
|
155
172
|
[`${prefix}/db-p2p/block-transfer/1.0.0`, 'members-snapshot'],
|
|
156
173
|
[`${fret}/neighbors`, 'members-snapshot'],
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* control-schema-version.ts — the check `ControlDatabase.loadSchema` runs on a warm control
|
|
3
|
+
* store after declaring the control schema and before applying it.
|
|
4
|
+
*
|
|
5
|
+
* `apply schema CadreControl` runs whatever DDL makes the hydrated catalog equal the declared
|
|
6
|
+
* schema, and that apply is how a store written under an older control schema version is
|
|
7
|
+
* migrated in place: between consecutive versions the difference is limited to statements the
|
|
8
|
+
* optimystic module applies to a table's catalog record without rewriting a row, so running the
|
|
9
|
+
* diff from a store at version K to this build's declaration runs each version's changes from
|
|
10
|
+
* K+1 up to the build's, in order. This module decides, from the stored version and the pending
|
|
11
|
+
* DDL and before any of it runs, whether the apply may go ahead, and throws one typed error
|
|
12
|
+
* naming the reason when it may not.
|
|
13
|
+
*
|
|
14
|
+
* The same change classifier ({@link classifyControlSchemaChange}) is what
|
|
15
|
+
* `control-schema-versions.spec.ts` runs over each pair of consecutive control schema versions,
|
|
16
|
+
* so a schema edit that no store could be migrated across fails when it is made. Where the
|
|
17
|
+
* version lives, when it changes and what a new version may change: docs/architecture.md →
|
|
18
|
+
* "Control schema versions and migrations".
|
|
19
|
+
*
|
|
20
|
+
* The stored tag is the version of the last start that applied the schema to the store: the
|
|
21
|
+
* apply's `alter table OwnerKey set tags (…)` persists since Optimystic 1.16.0, so after a
|
|
22
|
+
* migrating start the store records this build's version. A store additively upgraded by a build
|
|
23
|
+
* from before that still reads the version it was created under, which is harmless: the diff is
|
|
24
|
+
* computed from the catalog, not from the number, and the number only has to tell a store that
|
|
25
|
+
* a newer release migrated from one this build can migrate.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Parser, type Database, type SchemaManager, type SqlValue, type TableSchema } from '@quereus/quereus';
|
|
29
|
+
import type { AlterTableStmt, ColumnDef, Statement } from '@quereus/quereus/parser';
|
|
30
|
+
import { CONTROL_SCHEMA_BASELINE_VERSION, CONTROL_SCHEMA_NAME, CONTROL_SCHEMA_VERSION_TAG } from './control-schema.js';
|
|
31
|
+
import { controlStorageScope } from './storage-scope.js';
|
|
32
|
+
|
|
33
|
+
/** The control table whose tags carry {@link CONTROL_SCHEMA_VERSION_TAG}. */
|
|
34
|
+
const VERSION_TABLE = 'OwnerKey';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Why a warm control store is refused, in the order the check decides them:
|
|
38
|
+
* - `newer-release`: the store records a version above this build's, so a newer release has
|
|
39
|
+
* migrated it (this machine before a downgrade, or another machine sharing its catalog
|
|
40
|
+
* lineage); this machine is the one to update.
|
|
41
|
+
* - `before-migrations`: the store records a version below {@link CONTROL_SCHEMA_BASELINE_VERSION}
|
|
42
|
+
* (written by 1.16.0 or earlier); it cannot be migrated and must be recreated.
|
|
43
|
+
* - `not-migratable`: a pending statement would rewrite or discard rows. Reachable only from a
|
|
44
|
+
* development or hand-made store, since the edit-time check refuses such a version.
|
|
45
|
+
*/
|
|
46
|
+
export type ControlStoreIncompatibility = 'newer-release' | 'before-migrations' | 'not-migratable';
|
|
47
|
+
|
|
48
|
+
/** A warm control store this build cannot apply its control schema to. */
|
|
49
|
+
export class ControlStoreIncompatibleError extends Error {
|
|
50
|
+
readonly reason: ControlStoreIncompatibility;
|
|
51
|
+
readonly partyId: string;
|
|
52
|
+
/** The version the store records: the one its last migrating start declared; 0 = before versions were recorded. */
|
|
53
|
+
readonly storeVersion: number;
|
|
54
|
+
/** The version this build declares. */
|
|
55
|
+
readonly buildVersion: number;
|
|
56
|
+
/** The DDL the apply would have run, from `diff schema`; for diagnostics, not for parsing. */
|
|
57
|
+
readonly pendingDdl: readonly string[];
|
|
58
|
+
/** `controlStorageScope(partyId)`: the storage the embedder deletes to recreate (docs/architecture.md). */
|
|
59
|
+
readonly storageScope: string;
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
reason: ControlStoreIncompatibility,
|
|
63
|
+
partyId: string,
|
|
64
|
+
storeVersion: number,
|
|
65
|
+
buildVersion: number,
|
|
66
|
+
pendingDdl: readonly string[]
|
|
67
|
+
) {
|
|
68
|
+
const storageScope = controlStorageScope(partyId);
|
|
69
|
+
super(refusalMessage(reason, partyId, storeVersion, buildVersion, storageScope));
|
|
70
|
+
this.name = 'ControlStoreIncompatibleError';
|
|
71
|
+
this.reason = reason;
|
|
72
|
+
this.partyId = partyId;
|
|
73
|
+
this.storeVersion = storeVersion;
|
|
74
|
+
this.buildVersion = buildVersion;
|
|
75
|
+
this.pendingDdl = pendingDdl;
|
|
76
|
+
this.storageScope = storageScope;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The message for each {@link ControlStoreIncompatibility}. Only the two recreate cases advise
|
|
82
|
+
* recreating: a store a newer release migrated holds rows written since, and recreating it
|
|
83
|
+
* would throw them away.
|
|
84
|
+
*/
|
|
85
|
+
function refusalMessage(
|
|
86
|
+
reason: ControlStoreIncompatibility,
|
|
87
|
+
partyId: string,
|
|
88
|
+
storeVersion: number,
|
|
89
|
+
buildVersion: number,
|
|
90
|
+
storageScope: string
|
|
91
|
+
): string {
|
|
92
|
+
const store = `The control store for party ${JSON.stringify(partyId)}`;
|
|
93
|
+
const versions = `it records control schema version ${storeVersion}; this build declares ${buildVersion}`;
|
|
94
|
+
const recreate = 'Run a release whose control schema matches the store, or recreate the control store ' +
|
|
95
|
+
`(storage scope ${storageScope}) as docs/architecture.md describes.`;
|
|
96
|
+
switch (reason) {
|
|
97
|
+
case 'newer-release':
|
|
98
|
+
return `${store} was migrated by a newer release (${versions}). Update this app or node to that ` +
|
|
99
|
+
'release or a later one; the store and its rows are intact.';
|
|
100
|
+
case 'before-migrations':
|
|
101
|
+
return `${store} was written before control schema migrations (${versions}; migrations start at ` +
|
|
102
|
+
`version ${CONTROL_SCHEMA_BASELINE_VERSION}, and docs/architecture.md says why earlier stores are not ` +
|
|
103
|
+
`migrated), so this build cannot upgrade it in place. ${recreate}`;
|
|
104
|
+
case 'not-migratable':
|
|
105
|
+
return `${store} cannot be migrated to this build's control schema (${versions}): applying it would ` +
|
|
106
|
+
`rewrite or discard stored rows. ${recreate}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Throw {@link ControlStoreIncompatibleError} when the declared control schema must not be applied
|
|
112
|
+
* to the hydrated store; return having run nothing otherwise, and the apply then migrates the
|
|
113
|
+
* store. Needs `declare schema CadreControl` to have run, and runs only `diff schema`, which
|
|
114
|
+
* executes no DDL.
|
|
115
|
+
*
|
|
116
|
+
* A cold store (the hydrated catalog lists no control table) passes without a diff: the apply
|
|
117
|
+
* creates everything. A warm store is refused for the first of these that holds
|
|
118
|
+
* ({@link ControlStoreIncompatibility}):
|
|
119
|
+
* - its version is above the build's: even a diff that only adds (the newer release no longer
|
|
120
|
+
* declares something this build does) would put back what that release removed, and the newer
|
|
121
|
+
* release would then refuse its own store;
|
|
122
|
+
* - its version is below the baseline, whatever the diff: a store written by 1.16.0 or earlier,
|
|
123
|
+
* which the baseline paragraph of docs/architecture.md explains cannot be migrated;
|
|
124
|
+
* - any pending statement is `unmigratable` ({@link classifyControlSchemaChange}).
|
|
125
|
+
*
|
|
126
|
+
* Every `additive` and `catalog-only` change passes, so a store from an older migratable
|
|
127
|
+
* release warm-starts and the apply brings it to this build's version. A store that is both
|
|
128
|
+
* older than the baseline and not migratable reports `before-migrations`, whose message gives the
|
|
129
|
+
* recreate advice and points at the explanation.
|
|
130
|
+
*/
|
|
131
|
+
export async function refuseIncompatibleControlStore(db: Database, partyId: string): Promise<void> {
|
|
132
|
+
if (!isWarmControlStore(db)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const storeVersion = schemaVersionOf(db.schemaManager.getTable(CONTROL_SCHEMA_NAME, VERSION_TABLE)?.tags);
|
|
136
|
+
const buildVersion = declaredSchemaVersion(db);
|
|
137
|
+
const pending = await pendingControlSchemaChanges(db);
|
|
138
|
+
const reason = incompatibilityOf(storeVersion, buildVersion, pending);
|
|
139
|
+
if (reason !== undefined) {
|
|
140
|
+
throw new ControlStoreIncompatibleError(reason, partyId, storeVersion, buildVersion, pending.map((change) => change.ddl));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The first {@link ControlStoreIncompatibility} that holds for a warm store, or undefined when the apply may migrate it. */
|
|
145
|
+
function incompatibilityOf(
|
|
146
|
+
storeVersion: number,
|
|
147
|
+
buildVersion: number,
|
|
148
|
+
pending: readonly PendingControlSchemaChange[]
|
|
149
|
+
): ControlStoreIncompatibility | undefined {
|
|
150
|
+
if (storeVersion > buildVersion) {
|
|
151
|
+
return 'newer-release';
|
|
152
|
+
}
|
|
153
|
+
if (storeVersion < CONTROL_SCHEMA_BASELINE_VERSION) {
|
|
154
|
+
return 'before-migrations';
|
|
155
|
+
}
|
|
156
|
+
return pending.some((change) => change.kind === 'unmigratable') ? 'not-migratable' : undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Whether the hydrated catalog lists at least one control table, i.e. the store was written before. */
|
|
160
|
+
function isWarmControlStore(db: Database): boolean {
|
|
161
|
+
const schema = db.schemaManager.getSchema(CONTROL_SCHEMA_NAME);
|
|
162
|
+
return schema !== undefined && !schema.getAllTables().next().done;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The version the declared control schema carries on its version table; 0 when it carries none. */
|
|
166
|
+
export function declaredSchemaVersion(db: Database): number {
|
|
167
|
+
const declared = db.declaredSchemaManager.getDeclaredSchema(CONTROL_SCHEMA_NAME);
|
|
168
|
+
const table = declared?.items.find(
|
|
169
|
+
(item) => item.type === 'declaredTable' && item.tableStmt.table.name.toLowerCase() === VERSION_TABLE.toLowerCase()
|
|
170
|
+
);
|
|
171
|
+
return schemaVersionOf(table?.type === 'declaredTable' ? table.tableStmt.tags : undefined);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The control schema version a table's tags record: 0 when the tag is absent, which is what
|
|
176
|
+
* every store written before versions were recorded holds.
|
|
177
|
+
*/
|
|
178
|
+
function schemaVersionOf(tags: Readonly<Record<string, SqlValue>> | undefined): number {
|
|
179
|
+
const value = tags?.[CONTROL_SCHEMA_VERSION_TAG];
|
|
180
|
+
if (value === undefined || value === null) {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
const version = typeof value === 'bigint' ? Number(value) : value;
|
|
184
|
+
if (typeof version !== 'number' || !Number.isSafeInteger(version) || version < 0) {
|
|
185
|
+
throw new Error(`The ${VERSION_TABLE} tag ${CONTROL_SCHEMA_VERSION_TAG} holds ${String(value)}, not a control schema version`);
|
|
186
|
+
}
|
|
187
|
+
return version;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* How applying one `diff schema` statement to a warm store treats its rows, from most to least
|
|
192
|
+
* migratable (docs/architecture.md → "Control schema versions and migrations"):
|
|
193
|
+
* - `additive`: a new table, a new index, or a table tag change, which the optimystic module
|
|
194
|
+
* applies without touching a row;
|
|
195
|
+
* - `catalog-only`: a change the optimystic module applies to the table's catalog record without
|
|
196
|
+
* rewriting a row (since Optimystic 1.16.0);
|
|
197
|
+
* - `unmigratable`: everything else, which rewrites or discards rows.
|
|
198
|
+
*/
|
|
199
|
+
export type ControlSchemaChangeKind = 'additive' | 'catalog-only' | 'unmigratable';
|
|
200
|
+
|
|
201
|
+
const CHANGE_KINDS: readonly ControlSchemaChangeKind[] = ['additive', 'catalog-only', 'unmigratable'];
|
|
202
|
+
|
|
203
|
+
/** One row of `diff schema CadreControl`, classified. */
|
|
204
|
+
export interface PendingControlSchemaChange {
|
|
205
|
+
readonly ddl: string;
|
|
206
|
+
/** The least migratable kind among the row's statements. */
|
|
207
|
+
readonly kind: ControlSchemaChangeKind;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The DDL `apply schema CadreControl` would run now, one entry per `diff schema` row, each
|
|
212
|
+
* classified against the catalog `db` holds. Needs `declare schema CadreControl` to have run.
|
|
213
|
+
*/
|
|
214
|
+
export async function pendingControlSchemaChanges(db: Database): Promise<PendingControlSchemaChange[]> {
|
|
215
|
+
const changes: PendingControlSchemaChange[] = [];
|
|
216
|
+
for await (const row of db.eval(`diff schema ${CONTROL_SCHEMA_NAME}`)) {
|
|
217
|
+
const ddl = row.ddl as string;
|
|
218
|
+
const kinds = new Parser().parseAll(ddl).map((statement) => classifyControlSchemaChange(statement, db.schemaManager));
|
|
219
|
+
changes.push({ ddl, kind: leastMigratable(kinds) });
|
|
220
|
+
}
|
|
221
|
+
return changes;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The least migratable of `kinds`; `additive` when there are none. */
|
|
225
|
+
function leastMigratable(kinds: readonly ControlSchemaChangeKind[]): ControlSchemaChangeKind {
|
|
226
|
+
return CHANGE_KINDS[Math.max(0, ...kinds.map((kind) => CHANGE_KINDS.indexOf(kind)))];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The {@link ControlSchemaChangeKind} of one statement `diff schema` emits. A name the statement
|
|
231
|
+
* drops is resolved against `catalog`, the one the statement would run against: the hydrated
|
|
232
|
+
* store's at runtime.
|
|
233
|
+
*/
|
|
234
|
+
function classifyControlSchemaChange(statement: Statement, catalog: SchemaManager): ControlSchemaChangeKind {
|
|
235
|
+
switch (statement.type) {
|
|
236
|
+
case 'createTable':
|
|
237
|
+
return 'additive';
|
|
238
|
+
// NOTE: a `unique index` over an existing table fails the apply on a store whose rows
|
|
239
|
+
// already repeat the key, which the spec's empty tables cannot show. If a version adds
|
|
240
|
+
// one to a table that holds rows, prove the stored rows are unique on it.
|
|
241
|
+
case 'createIndex':
|
|
242
|
+
return 'additive';
|
|
243
|
+
case 'drop':
|
|
244
|
+
return statement.objectType === 'index' ? 'catalog-only' : 'unmigratable';
|
|
245
|
+
case 'alterTable':
|
|
246
|
+
return classifyTableAlteration(statement, catalog);
|
|
247
|
+
default:
|
|
248
|
+
return 'unmigratable';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function classifyTableAlteration({ table, action }: AlterTableStmt, catalog: SchemaManager): ControlSchemaChangeKind {
|
|
253
|
+
const stored = (): TableSchema | undefined => catalog.getTable(table.schema ?? CONTROL_SCHEMA_NAME, table.name);
|
|
254
|
+
switch (action.type) {
|
|
255
|
+
case 'setTags':
|
|
256
|
+
return 'additive';
|
|
257
|
+
// NOTE: Quereus checks stored rows against an added CHECK, so one that a stored row fails
|
|
258
|
+
// fails the apply on that store; control-schema-versions.spec.ts diffs empty tables and
|
|
259
|
+
// cannot see it. If a version adds a CHECK over existing columns, prove the rows hold it.
|
|
260
|
+
case 'addConstraint':
|
|
261
|
+
return action.constraint.type === 'check' ? 'catalog-only' : 'unmigratable';
|
|
262
|
+
case 'dropConstraint':
|
|
263
|
+
return hasCheckConstraint(stored(), action.name) ? 'catalog-only' : 'unmigratable';
|
|
264
|
+
case 'addColumn':
|
|
265
|
+
return isPlainNullableColumn(action.column) ? 'catalog-only' : 'unmigratable';
|
|
266
|
+
case 'dropColumn':
|
|
267
|
+
return isUnkeyedColumn(stored(), action.name) ? 'catalog-only' : 'unmigratable';
|
|
268
|
+
default:
|
|
269
|
+
return 'unmigratable';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function hasCheckConstraint(table: TableSchema | undefined, name: string): boolean {
|
|
274
|
+
return table?.checkConstraints.some((check) => check.name?.toLowerCase() === name.toLowerCase()) ?? false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Whether existing rows can read null for the column: declared `null` and nothing else (no
|
|
279
|
+
* default, generated expression or other column constraint). The `null` must be explicit,
|
|
280
|
+
* because Quereus makes a column not null unless it says otherwise.
|
|
281
|
+
*/
|
|
282
|
+
function isPlainNullableColumn(column: ColumnDef): boolean {
|
|
283
|
+
return column.constraints.length > 0 && column.constraints.every((constraint) => constraint.type === 'null');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Whether the table has the column and no index, unique constraint or primary key covers it.
|
|
288
|
+
*
|
|
289
|
+
* NOTE: judged against the catalog before the apply, so a column whose covering index the same
|
|
290
|
+
* diff drops (`diff schema` emits the `drop index` first) still counts as covered. Conservative;
|
|
291
|
+
* if a version needs that change, judge the diff as a sequence.
|
|
292
|
+
*/
|
|
293
|
+
function isUnkeyedColumn(table: TableSchema | undefined, name: string): boolean {
|
|
294
|
+
const column = table?.columnIndexMap.get(name.toLowerCase());
|
|
295
|
+
if (table === undefined || column === undefined) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
return !table.primaryKeyDefinition.some((key) => key.index === column)
|
|
299
|
+
&& !(table.indexes ?? []).some((index) => index.columns.some((indexed) => indexed.index === column))
|
|
300
|
+
&& !(table.uniqueConstraints ?? []).some((unique) => unique.columns.includes(column));
|
|
301
|
+
}
|