@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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@serfab/cadre-core/edge-node` — build and run an edge device's cadre node the prescribed way:
|
|
3
|
+
* `createEdgeNode(platform)` over the app's key store and storage. A subpath rather than the root
|
|
4
|
+
* export, so cadre-host and the CLI never load it; the React Native kit's `createPhoneNode`
|
|
5
|
+
* (`@serfab/cadre-rn/phone-node`) is this over the secure store and rn-leveldb, and the browser
|
|
6
|
+
* and NativeScript reference apps run it over IndexedDB and SQLite.
|
|
7
|
+
*/
|
|
8
|
+
export {
|
|
9
|
+
createEdgeNode,
|
|
10
|
+
type EdgeJoinProgress,
|
|
11
|
+
type EdgeNode,
|
|
12
|
+
type EdgeNodeStatus,
|
|
13
|
+
type JoinCadreOptions,
|
|
14
|
+
type JoinCadreResult,
|
|
15
|
+
type UnfinishedRemoval,
|
|
16
|
+
} from './edge-node.js';
|
|
17
|
+
export {
|
|
18
|
+
DEFAULT_EDGE_NODE_NAMES,
|
|
19
|
+
type EdgeNodeNames,
|
|
20
|
+
type EdgeNodePlatform,
|
|
21
|
+
type EdgeNodeStorage,
|
|
22
|
+
type RemovableSlot,
|
|
23
|
+
} from './platform.js';
|
|
24
|
+
export {
|
|
25
|
+
isNoiseCryptoMode,
|
|
26
|
+
NOISE_CRYPTO_MODES,
|
|
27
|
+
parseSavedStart,
|
|
28
|
+
serializeSavedStart,
|
|
29
|
+
type EdgeNodeOptions,
|
|
30
|
+
type EdgeNodeOrigin,
|
|
31
|
+
type NoiseCryptoMode,
|
|
32
|
+
type SavedStart,
|
|
33
|
+
} from './options.js';
|
|
34
|
+
export {
|
|
35
|
+
buildEdgeNodeConfig,
|
|
36
|
+
DEFAULT_OWNER_GENESIS_TIMEOUT_MS,
|
|
37
|
+
DEFAULT_OWNER_KEY_LABEL,
|
|
38
|
+
runOwnerGenesis,
|
|
39
|
+
wireJoinedOwnerKey,
|
|
40
|
+
type EdgeNodeConfigInputs,
|
|
41
|
+
type JoinedOwnerKeyOptions,
|
|
42
|
+
type OwnerGenesisOptions,
|
|
43
|
+
type OwnerGenesisResult,
|
|
44
|
+
} from './config.js';
|
|
45
|
+
export { attachStrandWhenWritable, DEFAULT_STRAND_PATIENCE_MS, retryAfterRestart } from './strands.js';
|
|
46
|
+
export { StrandsNeedOwnerJoinError, type PendingStrandMove, type StrandMoveOutcome } from './strand-move.js';
|
|
47
|
+
export { isOwnerGrant, type JoinCredential } from './join-credential.js';
|
|
48
|
+
export { type RedeemedOwnerGrant } from './owner-grant.js';
|
|
49
|
+
export { FormerPartyNotEmptyError, StrandMovePendingError, type DissolveFormerPartyOptions } from './dissolve.js';
|
|
50
|
+
export {
|
|
51
|
+
bootstrapPeersKvKey,
|
|
52
|
+
enrolledMachinesKvKey,
|
|
53
|
+
kvStoreSlot,
|
|
54
|
+
strandNetworkKvKey,
|
|
55
|
+
trustedOwnersKvKey,
|
|
56
|
+
type KvStoreApi,
|
|
57
|
+
type NodeLocalKv,
|
|
58
|
+
} from './node-local.js';
|
|
59
|
+
export { lazyRawStorage } from './lazy-raw-storage.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `EdgeNode.joinCadre` takes: a machine invitation, which admits the app as a machine, or an
|
|
3
|
+
* owner grant, which makes it a machine and an owner at once (docs/architecture.md → "An app
|
|
4
|
+
* joins an existing cadre"). Both name the party; the codecs refuse each other's text by name,
|
|
5
|
+
* so an app's one join field decodes whichever was pasted.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CadreInvitation } from '../cadre-invite-protocol.js';
|
|
9
|
+
import type { OwnerGrantBundle } from '../owner-grant-protocol.js';
|
|
10
|
+
|
|
11
|
+
export type JoinCredential = CadreInvitation | OwnerGrantBundle;
|
|
12
|
+
|
|
13
|
+
/** The bundle's private half tells the two apart: a grant carries `grantPrivateKey`, an invitation `invitePrivateKey`. */
|
|
14
|
+
export function isOwnerGrant(credential: JoinCredential): credential is OwnerGrantBundle {
|
|
15
|
+
return 'grantPrivateKey' in credential;
|
|
16
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lazyRawStorage` — an `IRawStorage` over an opener that is asynchronous.
|
|
3
|
+
*
|
|
4
|
+
* `CadreNodeConfig.storage.provider`, and so `EdgeNodeStorage.rawStorage`, is a synchronous
|
|
5
|
+
* factory. rn-leveldb opens synchronously, but IndexedDB and NativeScript's SQLite open with a
|
|
6
|
+
* promise, so those platforms return this proxy: every (already asynchronous) method awaits one
|
|
7
|
+
* cached `open()` before delegating, and an async-iterator method opens only once iteration
|
|
8
|
+
* starts. Per-scope handle caching, and closing, stay the platform's: the proxy owns nothing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ActionId, ActionRev, BlockId, IBlock, Transform } from '@optimystic/db-core';
|
|
12
|
+
import type { BlockCommitProof, BlockMetadata, FlushOptions, IRawStorage, StoreDurability } from '@optimystic/db-p2p';
|
|
13
|
+
|
|
14
|
+
/** The optional `IRawStorage` members this proxy forwards, delegating when the opened storage has them. */
|
|
15
|
+
type OptionalMethodForwarded = 'getApproximateBytesUsed' | 'listBlockIds';
|
|
16
|
+
/**
|
|
17
|
+
* `getStoreIdentity`: see the accepted tradeoff on {@link LazyRawStorage}. `readCached`: a marker
|
|
18
|
+
* set only by a storage with a read cache beneath it, which the proxy cannot know before opening
|
|
19
|
+
* and cadre-core's cache wrap reads before any open. `close`: the storage behind the proxy is the
|
|
20
|
+
* platform's, shared with every later proxy over the same scope, and nothing in cadre-core closes
|
|
21
|
+
* a store its provider returned.
|
|
22
|
+
*/
|
|
23
|
+
type OptionalMemberOmitted = 'getStoreIdentity' | 'readCached' | 'close';
|
|
24
|
+
|
|
25
|
+
type OptionalKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? K : never }[keyof T];
|
|
26
|
+
type MustBeNever<T extends never> = T;
|
|
27
|
+
/**
|
|
28
|
+
* Fails to compile when `IRawStorage` gains an optional member that is in neither list above, so
|
|
29
|
+
* each one is forwarded or omitted by decision rather than by default.
|
|
30
|
+
*/
|
|
31
|
+
export type UnclassifiedOptionalMember = MustBeNever<
|
|
32
|
+
Exclude<OptionalKeys<IRawStorage>, OptionalMethodForwarded | OptionalMemberOmitted>
|
|
33
|
+
>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `IRawStorage`'s optional methods are detected by callers checking that the method exists, so
|
|
37
|
+
* one this proxy leaves out reads as "not supported". The `implements` clause names the optional
|
|
38
|
+
* methods the proxy forwards, so dropping one is a compile error, and `UnclassifiedOptionalMember`
|
|
39
|
+
* rejects one added upstream later. The two forwarded ones delegate when the opened storage has
|
|
40
|
+
* them and otherwise answer as an omission would (0 bytes; no block ids).
|
|
41
|
+
*
|
|
42
|
+
* NOTE: those two are always present on the proxy, a stub when the opened storage lacks them,
|
|
43
|
+
* which `IRawStorage` asks backends not to do (feature detection should see the true capability:
|
|
44
|
+
* the peer-join backfill is inert without `listBlockIds` and runs an empty scan with the stub).
|
|
45
|
+
* The proxy cannot know before opening, and every storage a platform opens behind it (IndexedDB,
|
|
46
|
+
* SQLite) has both. If a platform ever opens one without them, let it declare the capability
|
|
47
|
+
* beside `durability` and omit the method here.
|
|
48
|
+
*
|
|
49
|
+
* NOTE: accepted tradeoff — `getStoreIdentity` is omitted although the opened storage may have it.
|
|
50
|
+
* It is synchronous and must return a string fixed at construction, but the real identity exists
|
|
51
|
+
* only after the asynchronous open; the interface says a backend that cannot honour that must
|
|
52
|
+
* omit the method. `withReadCache` then identifies the store by object. Revisit if two proxies over
|
|
53
|
+
* one scope are seen getting separate read caches, or if the interface allows a lazily-resolved
|
|
54
|
+
* identity.
|
|
55
|
+
*/
|
|
56
|
+
class LazyRawStorage implements IRawStorage, Required<Pick<IRawStorage, OptionalMethodForwarded>> {
|
|
57
|
+
private opening: Promise<IRawStorage> | null = null;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly open: () => Promise<IRawStorage>,
|
|
61
|
+
readonly durability: StoreDurability,
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One open shared by every call; a rejected open is forgotten, so the next call retries while
|
|
66
|
+
* the calls that awaited the failed one still get its rejection.
|
|
67
|
+
*
|
|
68
|
+
* NOTE: retries have no backoff: one fresh open attempt per operation on a permanently broken
|
|
69
|
+
* scope. Revisit only if a failing scope is seen issuing opens in a tight loop.
|
|
70
|
+
*/
|
|
71
|
+
private storage(): Promise<IRawStorage> {
|
|
72
|
+
if (this.opening) return this.opening;
|
|
73
|
+
const opening = this.open();
|
|
74
|
+
this.opening = opening;
|
|
75
|
+
// The rejection itself reaches every caller awaiting `opening`; this only forgets it.
|
|
76
|
+
void opening.catch(() => {
|
|
77
|
+
if (this.opening === opening) this.opening = null;
|
|
78
|
+
});
|
|
79
|
+
return opening;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
|
|
83
|
+
return (await this.storage()).getMetadata(blockId);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
|
|
87
|
+
return (await this.storage()).saveMetadata(blockId, metadata);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
|
|
91
|
+
return (await this.storage()).getRevision(blockId, rev);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
|
|
95
|
+
return (await this.storage()).saveRevision(blockId, rev, actionId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async *listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev> {
|
|
99
|
+
const storage = await this.storage();
|
|
100
|
+
yield* storage.listRevisions(blockId, startRev, endRev);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
104
|
+
return (await this.storage()).getPendingTransaction(blockId, actionId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
108
|
+
return (await this.storage()).savePendingTransaction(blockId, actionId, transform);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
112
|
+
return (await this.storage()).deletePendingTransaction(blockId, actionId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
|
|
116
|
+
const storage = await this.storage();
|
|
117
|
+
yield* storage.listPendingTransactions(blockId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
121
|
+
return (await this.storage()).getTransaction(blockId, actionId);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
125
|
+
return (await this.storage()).saveTransaction(blockId, actionId, transform);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async getBlockProof(blockId: BlockId, rev: number): Promise<BlockCommitProof | undefined> {
|
|
129
|
+
return (await this.storage()).getBlockProof(blockId, rev);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async saveBlockProof(blockId: BlockId, rev: number, proof: BlockCommitProof): Promise<void> {
|
|
133
|
+
return (await this.storage()).saveBlockProof(blockId, rev, proof);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
|
|
137
|
+
return (await this.storage()).getMaterializedBlock(blockId, actionId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
|
|
141
|
+
return (await this.storage()).saveMaterializedBlock(blockId, actionId, block);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
145
|
+
return (await this.storage()).promotePendingTransaction(blockId, actionId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async flush(options?: FlushOptions): Promise<void> {
|
|
149
|
+
return (await this.storage()).flush(options);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async whenDurable(): Promise<void> {
|
|
153
|
+
return (await this.storage()).whenDurable();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async getApproximateBytesUsed(): Promise<number> {
|
|
157
|
+
const storage = await this.storage();
|
|
158
|
+
return storage.getApproximateBytesUsed ? storage.getApproximateBytesUsed() : 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async *listBlockIds(): AsyncIterable<BlockId> {
|
|
162
|
+
const storage = await this.storage();
|
|
163
|
+
if (storage.listBlockIds) yield* storage.listBlockIds();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* An `IRawStorage` whose every method awaits one cached `open()` before delegating. A rejected
|
|
169
|
+
* open is forgotten, so the next call retries; the async-iterator methods open only once
|
|
170
|
+
* iteration starts.
|
|
171
|
+
*
|
|
172
|
+
* `durability` is what the opened storage reports (`IRawStorage.durability`), stated here because
|
|
173
|
+
* it is read synchronously, before any open: a platform knows its backend (`'power-loss'` for
|
|
174
|
+
* SQLite and IndexedDB).
|
|
175
|
+
*/
|
|
176
|
+
export function lazyRawStorage(open: () => Promise<IRawStorage>, durability: StoreDurability): IRawStorage {
|
|
177
|
+
return new LazyRawStorage(open, durability);
|
|
178
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The edge node's node-local records: the key each of cadre-core's node-local records is filed
|
|
3
|
+
* under in the platform's node-local key-value store (`EdgeNodeStorage.nodeLocal`), and the
|
|
4
|
+
* `DurableSlot` over one such key, which the persistent stores (`PersistentBootstrapPeerStore`,
|
|
5
|
+
* `PersistentEnrolledMachineStore`, `PersistentStrandNetworkStateStore`, and
|
|
6
|
+
* `PersistentTrustedOwnerStore` on a platform with no anchor slot of its own) read through.
|
|
7
|
+
*
|
|
8
|
+
* Every record key is `<record>.<partyId>`, so a party's records never collide with another's,
|
|
9
|
+
* and an app's own records in the same store stay clear of them by using a dot-free key. The
|
|
10
|
+
* party id needs no encoding: the store deals in text keys.
|
|
11
|
+
*
|
|
12
|
+
* Which records are trust-bearing, and so belong in a more tamper-resistant store where the
|
|
13
|
+
* platform has one, is `EdgeNodeStorage.trustedOwnerSlot`'s concern; the React Native kit's
|
|
14
|
+
* `@serfab/cadre-rn/node-local` carries that reasoning.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { DurableSlot } from '../node-local-snapshot.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The subset of a key-value store a slot needs. Declared here, structurally, so a platform
|
|
21
|
+
* passes its own store (`LevelDBKVStore`, `SqliteKVStore`, an IndexedDB object store) and a
|
|
22
|
+
* test an in-memory fake.
|
|
23
|
+
*/
|
|
24
|
+
export interface KvStoreApi {
|
|
25
|
+
/** The key's text, or `undefined` when absent. Throws on a read fault. */
|
|
26
|
+
get(key: string): Promise<string | undefined>;
|
|
27
|
+
/** Resolves once `get` on this store reads `value`; durable only after a later {@link flush}. */
|
|
28
|
+
set(key: string, value: string): Promise<void>;
|
|
29
|
+
/** Make every write that resolved before this call durable. */
|
|
30
|
+
flush(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The node-local store the edge node runs over: {@link KvStoreApi} plus deletion, for the records a removal drops. */
|
|
34
|
+
export interface NodeLocalKv extends KvStoreApi {
|
|
35
|
+
/** Resolves once `get` reads the key as absent; durable only after a later {@link KvStoreApi.flush}. Idempotent. */
|
|
36
|
+
delete(key: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Key of a party's retained cold-start dial targets (`PersistentBootstrapPeerStore`). */
|
|
40
|
+
export function bootstrapPeersKvKey(partyId: string): string {
|
|
41
|
+
return `bootstrap-peers.${partyId}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Key of a party's last-known enrolled-machine count (`PersistentEnrolledMachineStore`). */
|
|
45
|
+
export function enrolledMachinesKvKey(partyId: string): string {
|
|
46
|
+
return `enrolled-machines.${partyId}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Key of a party's strand network state (`PersistentStrandNetworkStateStore`): per strand, the
|
|
51
|
+
* FRET routing table its strand node saved, re-imported after a relaunch.
|
|
52
|
+
*/
|
|
53
|
+
export function strandNetworkKvKey(partyId: string): string {
|
|
54
|
+
return `strand-network.${partyId}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Key of a party's trusted-owner anchor (`PersistentTrustedOwnerStore`) on a platform that
|
|
59
|
+
* supplies no `EdgeNodeStorage.trustedOwnerSlot`: the browser and NativeScript apps. The anchor
|
|
60
|
+
* is trust-bearing, so a platform with a more tamper-resistant store than its node-local one
|
|
61
|
+
* (the phone's secure store) keeps it there instead.
|
|
62
|
+
*/
|
|
63
|
+
export function trustedOwnersKvKey(partyId: string): string {
|
|
64
|
+
return `trusted-owners.${partyId}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A {@link DurableSlot} over one key of a key-value store.
|
|
69
|
+
*
|
|
70
|
+
* A pass-through but for the flush: the store already deals in text and already reports an
|
|
71
|
+
* absent key as `undefined`, and a read fault throws out of `get`, which is exactly the
|
|
72
|
+
* "present but unreadable ⇒ throw" contract {@link DurableSlot} asks for. A `set` may resolve
|
|
73
|
+
* into the store's write group, so `save` flushes after it to be durable when it resolves, as
|
|
74
|
+
* {@link DurableSlot.save} requires.
|
|
75
|
+
*/
|
|
76
|
+
export function kvStoreSlot(kv: KvStoreApi, key: string): DurableSlot {
|
|
77
|
+
return {
|
|
78
|
+
load: () => kv.get(key),
|
|
79
|
+
save: async (text) => {
|
|
80
|
+
await kv.set(key, text);
|
|
81
|
+
await kv.flush();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What one edge-node start is made with, and the record that remembers it between launches.
|
|
3
|
+
*
|
|
4
|
+
* The record is one per app, not party-scoped: it is what *selects* the party, so every other
|
|
5
|
+
* node-local record is read through the `partyId` it carries. `EdgeNode.start` writes it after
|
|
6
|
+
* every successful start and `joinCadre` once a member admitted the app (`autoStart: true`),
|
|
7
|
+
* `stop` rewrites it with `autoStart: false`, and a removal from the party replaces it
|
|
8
|
+
* ({@link startAfterRemoval}); an app's launch and a push wake into a killed process read it to
|
|
9
|
+
* start the node unattended.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* How much of Noise's crypto the platform replaces (`EdgeNodePlatform.noiseCrypto`): none, the
|
|
14
|
+
* per-frame hash and cipher, or those and the handshake's key exchange too. Named here, apart from
|
|
15
|
+
* any implementation, so a stored mode can be validated without loading a native module; the
|
|
16
|
+
* React Native kit's `/noise-crypto` builds the implementation for each.
|
|
17
|
+
*/
|
|
18
|
+
export type NoiseCryptoMode = 'off' | 'symmetric' | 'full';
|
|
19
|
+
|
|
20
|
+
export interface EdgeNodeOptions {
|
|
21
|
+
/** Party ID — identifies this cadre. The app mints one on first run. */
|
|
22
|
+
partyId: string;
|
|
23
|
+
/** Bootstrap multiaddrs (the party's always-on machines). */
|
|
24
|
+
bootstrapAddrs: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Circuit-relay multiaddrs this node reserves a slot on. An edge device accepts no inbound
|
|
27
|
+
* connection, so a relay reservation is the only address anyone can dial it at. Empty is
|
|
28
|
+
* allowed: the node starts and dials out, but cannot be invited to or joined.
|
|
29
|
+
*/
|
|
30
|
+
relayAddrs: string[];
|
|
31
|
+
/** How much of Noise's crypto the platform replaces; read only when the node is built. */
|
|
32
|
+
noiseCryptoMode?: NoiseCryptoMode;
|
|
33
|
+
/**
|
|
34
|
+
* How this app came to `partyId`. A `'founded'` party runs owner genesis on every start; a
|
|
35
|
+
* `'joined'` one never does, because genesis on a party whose control store has not arrived
|
|
36
|
+
* would seat this app as a second founder. Absent means `'founded'`.
|
|
37
|
+
*/
|
|
38
|
+
origin?: EdgeNodeOrigin;
|
|
39
|
+
/**
|
|
40
|
+
* The name the founding owner key gets on its `OwnerKey` row when this start founds the party
|
|
41
|
+
* ("phone", "Ann's iPhone"). Read only by genesis; default `'phone'`. Trimmed, at most 64
|
|
42
|
+
* characters (`OWNER_KEY_MAX_LABEL`); a longer one fails the start.
|
|
43
|
+
*/
|
|
44
|
+
ownerKeyLabel?: string;
|
|
45
|
+
/**
|
|
46
|
+
* The party this app founded and left by joining another (`EdgeNode.joinCadre`). Its data
|
|
47
|
+
* stays on disk under its own names, for moving its strands and dissolving the old cadre.
|
|
48
|
+
*/
|
|
49
|
+
formerPartyId?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether this app founded its party, or joined it by invitation. */
|
|
53
|
+
export type EdgeNodeOrigin = 'founded' | 'joined';
|
|
54
|
+
|
|
55
|
+
/** The last start, as `EdgeNode.loadSavedStart` reports it. */
|
|
56
|
+
export interface SavedStart {
|
|
57
|
+
/** Exactly what the node was last successfully started with. */
|
|
58
|
+
options: EdgeNodeOptions;
|
|
59
|
+
/** True after a successful start, false after `stop`. Gates every unattended start. */
|
|
60
|
+
autoStart: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The `EdgeNodePlatform.dataVersion` of the build that last started over this device's data,
|
|
63
|
+
* or `undefined` when that build named none. The app compares it with its own to decide whether
|
|
64
|
+
* the stored data is usable.
|
|
65
|
+
*/
|
|
66
|
+
writtenBy?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The saved start once an owner of its party removed this app: the party this app founded and
|
|
71
|
+
* left (`formerPartyId`), which it may go back to, not started unattended; `undefined` when it
|
|
72
|
+
* left none. No bootstrap addresses: the former party's machines are in its own bootstrap-peer
|
|
73
|
+
* store, which every start reads.
|
|
74
|
+
*/
|
|
75
|
+
export function startAfterRemoval(saved: SavedStart): SavedStart | undefined {
|
|
76
|
+
const { partyId, formerPartyId, relayAddrs, noiseCryptoMode } = saved.options;
|
|
77
|
+
if (formerPartyId === undefined || formerPartyId === partyId) return undefined;
|
|
78
|
+
const options: EdgeNodeOptions = { partyId: formerPartyId, bootstrapAddrs: [], relayAddrs, origin: 'founded' };
|
|
79
|
+
if (noiseCryptoMode !== undefined) options.noiseCryptoMode = noiseCryptoMode;
|
|
80
|
+
return { options, autoStart: false, ...(saved.writtenBy !== undefined && { writtenBy: saved.writtenBy }) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every Noise crypto mode, so a stored mode can be validated without loading the native module. */
|
|
84
|
+
export const NOISE_CRYPTO_MODES: readonly NoiseCryptoMode[] = ['symmetric', 'full', 'off'];
|
|
85
|
+
|
|
86
|
+
export function isNoiseCryptoMode(value: unknown): value is NoiseCryptoMode {
|
|
87
|
+
return (NOISE_CRYPTO_MODES as readonly unknown[]).includes(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const RECORD_VERSION = 1;
|
|
91
|
+
|
|
92
|
+
/** The stored shape. Flat, so a record read by hand is legible. */
|
|
93
|
+
interface StartRecord {
|
|
94
|
+
version: typeof RECORD_VERSION;
|
|
95
|
+
partyId: string;
|
|
96
|
+
bootstrapAddrs: string[];
|
|
97
|
+
relayAddrs: string[];
|
|
98
|
+
noiseCryptoMode?: NoiseCryptoMode;
|
|
99
|
+
origin?: EdgeNodeOrigin;
|
|
100
|
+
formerPartyId?: string;
|
|
101
|
+
ownerKeyLabel?: string;
|
|
102
|
+
autoStart: boolean;
|
|
103
|
+
writtenBy?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function serializeSavedStart(saved: SavedStart): string {
|
|
107
|
+
const { partyId, bootstrapAddrs, relayAddrs, noiseCryptoMode, origin, formerPartyId, ownerKeyLabel } = saved.options;
|
|
108
|
+
const record: StartRecord = {
|
|
109
|
+
version: RECORD_VERSION,
|
|
110
|
+
partyId,
|
|
111
|
+
bootstrapAddrs,
|
|
112
|
+
relayAddrs,
|
|
113
|
+
noiseCryptoMode,
|
|
114
|
+
origin,
|
|
115
|
+
formerPartyId,
|
|
116
|
+
ownerKeyLabel,
|
|
117
|
+
autoStart: saved.autoStart,
|
|
118
|
+
writtenBy: saved.writtenBy,
|
|
119
|
+
};
|
|
120
|
+
return JSON.stringify(record);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The saved record, or `undefined` when there is none or it is unusable (logged).
|
|
125
|
+
*
|
|
126
|
+
* Only the party id is worth discarding a record over. A malformed optional field falls back to
|
|
127
|
+
* its default — `[]` for an address list, the build default for the Noise mode, absent for
|
|
128
|
+
* `origin`, `formerPartyId` and `ownerKeyLabel`, `false` for `autoStart` — so a damaged record still keeps the
|
|
129
|
+
* app in its party, at worst needing the user to start the node by hand.
|
|
130
|
+
*/
|
|
131
|
+
export function parseSavedStart(text: string | undefined): SavedStart | undefined {
|
|
132
|
+
if (text === undefined) return undefined;
|
|
133
|
+
const record = parseRecord(text);
|
|
134
|
+
if (!record) return undefined;
|
|
135
|
+
const { partyId } = record;
|
|
136
|
+
if (typeof partyId !== 'string' || partyId.trim().length === 0) {
|
|
137
|
+
console.warn('[cadre-core/edge-node] ignoring saved start options with no party id');
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
const options: EdgeNodeOptions = {
|
|
141
|
+
partyId,
|
|
142
|
+
bootstrapAddrs: stringArrayOrEmpty(record.bootstrapAddrs),
|
|
143
|
+
relayAddrs: stringArrayOrEmpty(record.relayAddrs),
|
|
144
|
+
};
|
|
145
|
+
if (isNoiseCryptoMode(record.noiseCryptoMode)) options.noiseCryptoMode = record.noiseCryptoMode;
|
|
146
|
+
if (record.origin === 'founded' || record.origin === 'joined') options.origin = record.origin;
|
|
147
|
+
if (typeof record.formerPartyId === 'string' && record.formerPartyId.trim().length > 0) {
|
|
148
|
+
options.formerPartyId = record.formerPartyId;
|
|
149
|
+
}
|
|
150
|
+
if (typeof record.ownerKeyLabel === 'string') options.ownerKeyLabel = record.ownerKeyLabel;
|
|
151
|
+
const saved: SavedStart = { options, autoStart: record.autoStart === true };
|
|
152
|
+
if (typeof record.writtenBy === 'string') saved.writtenBy = record.writtenBy;
|
|
153
|
+
return saved;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The record's fields, or `undefined` (logged) when the text is not a record of this version. */
|
|
157
|
+
function parseRecord(text: string): Record<string, unknown> | undefined {
|
|
158
|
+
let raw: unknown;
|
|
159
|
+
try {
|
|
160
|
+
raw = JSON.parse(text);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.warn('[cadre-core/edge-node] ignoring saved start options that are not JSON:', err);
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
166
|
+
console.warn('[cadre-core/edge-node] ignoring saved start options that are not an object');
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const record = raw as Record<string, unknown>;
|
|
170
|
+
if (record.version !== RECORD_VERSION) {
|
|
171
|
+
console.warn(`[cadre-core/edge-node] ignoring saved start options of unknown version ${String(record.version)}`);
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
return record;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function stringArrayOrEmpty(value: unknown): string[] {
|
|
178
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string') ? value : [];
|
|
179
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EdgeNode.redeemOwnerGrant`'s sequence: make the app an owner of the party it runs in with
|
|
3
|
+
* owner-grant text, seating the owner slot's key (docs/architecture.md → "Owner grants"). Runs
|
|
4
|
+
* over a narrow view of the runtime ({@link OwnerGrantRuntime}), inside one of its transitions,
|
|
5
|
+
* as the join does.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CadreNode } from '../cadre-node.js';
|
|
9
|
+
import { decodeOwnerGrant, OwnerGrantRejectedError } from '../owner-grant-protocol.js';
|
|
10
|
+
import type { OwnerSigner } from '../owner-signer.js';
|
|
11
|
+
import type { OwnerGenesisResult } from './config.js';
|
|
12
|
+
|
|
13
|
+
/** The part of the edge-node runtime a redemption drives; the join's view has the same two calls. */
|
|
14
|
+
export interface OwnerGrantRuntime {
|
|
15
|
+
/** The owner slot's key, created when the slot is empty: the key an owner grant seats. */
|
|
16
|
+
ownerSlotSigner(): Promise<OwnerSigner>;
|
|
17
|
+
/** `granted`: an owner grant has just seated the slot's key, so wait for the row. */
|
|
18
|
+
wireJoinedOwner(node: CadreNode, granted: boolean): Promise<OwnerGenesisResult>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** What {@link redeemOwnerGrantText} settled. */
|
|
22
|
+
export interface RedeemedOwnerGrant {
|
|
23
|
+
/** The owner slot's key: the one the grant seated, or found already an owner. */
|
|
24
|
+
ownerKey: string;
|
|
25
|
+
/** The key was already a live owner of the party: nothing was signed or written, and the grant spent no use. */
|
|
26
|
+
alreadyOwner: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The key is wired as the node's owner signer. False when its row has not reached this node
|
|
29
|
+
* within the owner-step bound (a redemption at another member); a later start wires it.
|
|
30
|
+
*/
|
|
31
|
+
wired: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decode `text`, refuse a grant for another party, answer a slot key that is already an owner
|
|
36
|
+
* here, else redeem the grant for the slot's key (`CadreNode.redeemOwnerGrant`) and wire it.
|
|
37
|
+
*
|
|
38
|
+
* The already-owner answer comes first because the redemption's local path refuses `alone`
|
|
39
|
+
* before it asks whether the key is an owner, and a device that is one should hear so whether
|
|
40
|
+
* or not it is connected.
|
|
41
|
+
*
|
|
42
|
+
* @throws the decoder's error for text that is not an owner grant; `OwnerGrantRejectedError`
|
|
43
|
+
* `party-mismatch` for a grant of another party, the code a member serving another cadre answers;
|
|
44
|
+
* and what `CadreNode.redeemOwnerGrant` throws.
|
|
45
|
+
*/
|
|
46
|
+
export async function redeemOwnerGrantText(runtime: OwnerGrantRuntime, node: CadreNode, text: string): Promise<RedeemedOwnerGrant> {
|
|
47
|
+
const bundle = decodeOwnerGrant(text);
|
|
48
|
+
if (bundle.partyId !== node.partyId) {
|
|
49
|
+
throw new OwnerGrantRejectedError('party-mismatch', `This grant is for cadre ${bundle.partyId}, not the one this app runs in`);
|
|
50
|
+
}
|
|
51
|
+
// NOTE: a key created here stays in the slot when the redemption then fails, as a failed grant
|
|
52
|
+
// join's does (`edge-join.ts`), so a later import of a different exported key is refused as
|
|
53
|
+
// slot-occupied until a grant seats this one. If users meet that, delete a key this call created
|
|
54
|
+
// when the refusal is one that wrote nothing (every refusal but an unreachable member or an
|
|
55
|
+
// unverifiable reply, after either of which the row may exist).
|
|
56
|
+
const signer = await runtime.ownerSlotSigner();
|
|
57
|
+
if (await node.wireOwnKeyIfOwner(signer)) {
|
|
58
|
+
return { ownerKey: signer.publicKey, alreadyOwner: true, wired: true };
|
|
59
|
+
}
|
|
60
|
+
const { alreadyOwner } = await node.redeemOwnerGrant(bundle, signer);
|
|
61
|
+
const wired = (await runtime.wireJoinedOwner(node, true)) === 'enrolled';
|
|
62
|
+
return { ownerKey: signer.publicKey, alreadyOwner, wired };
|
|
63
|
+
}
|