@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.
Files changed (268) hide show
  1. package/README.md +4 -3
  2. package/dist/bootstrap-peer-store-file.js +1 -1
  3. package/dist/bootstrap-peer-store.d.ts +1 -1
  4. package/dist/bootstrap-peer-store.js +4 -4
  5. package/dist/cadre-invite-protocol.d.ts +81 -142
  6. package/dist/cadre-invite-protocol.js +168 -458
  7. package/dist/cadre-invite-protocol.js.map +1 -1
  8. package/dist/cadre-join-request.d.ts +15 -2
  9. package/dist/cadre-join-request.js +22 -3
  10. package/dist/cadre-join-request.js.map +1 -1
  11. package/dist/cadre-node.d.ts +1089 -475
  12. package/dist/cadre-node.js +2170 -1037
  13. package/dist/cadre-node.js.map +1 -1
  14. package/dist/control-authorization.d.ts +36 -14
  15. package/dist/control-authorization.js +20 -3
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.d.ts +88 -32
  18. package/dist/control-cohort.js +91 -33
  19. package/dist/control-cohort.js.map +1 -1
  20. package/dist/control-database.d.ts +856 -318
  21. package/dist/control-database.js +1422 -701
  22. package/dist/control-database.js.map +1 -1
  23. package/dist/control-formation-recorder.d.ts +46 -43
  24. package/dist/control-formation-recorder.js +77 -78
  25. package/dist/control-formation-recorder.js.map +1 -1
  26. package/dist/control-protocol-guard.d.ts +11 -4
  27. package/dist/control-protocol-guard.js +20 -5
  28. package/dist/control-protocol-guard.js.map +1 -1
  29. package/dist/control-schema-version.d.ts +97 -0
  30. package/dist/control-schema-version.js +225 -0
  31. package/dist/control-schema-version.js.map +1 -0
  32. package/dist/control-schema.d.ts +45 -5
  33. package/dist/control-schema.js +262 -78
  34. package/dist/control-schema.js.map +1 -1
  35. package/dist/control-write-retry.d.ts +45 -10
  36. package/dist/control-write-retry.js +92 -22
  37. package/dist/control-write-retry.js.map +1 -1
  38. package/dist/delegate-admission.d.ts +13 -0
  39. package/dist/delegate-admission.js +27 -0
  40. package/dist/delegate-admission.js.map +1 -1
  41. package/dist/ed25519-key.d.ts +10 -7
  42. package/dist/ed25519-key.js +23 -20
  43. package/dist/ed25519-key.js.map +1 -1
  44. package/dist/edge-node/config.d.ts +122 -0
  45. package/dist/edge-node/config.js +199 -0
  46. package/dist/edge-node/config.js.map +1 -0
  47. package/dist/edge-node/dissolve.d.ts +95 -0
  48. package/dist/edge-node/dissolve.js +184 -0
  49. package/dist/edge-node/dissolve.js.map +1 -0
  50. package/dist/edge-node/edge-join.d.ts +44 -0
  51. package/dist/edge-node/edge-join.js +227 -0
  52. package/dist/edge-node/edge-join.js.map +1 -0
  53. package/dist/edge-node/edge-node.d.ts +295 -0
  54. package/dist/edge-node/edge-node.js +704 -0
  55. package/dist/edge-node/edge-node.js.map +1 -0
  56. package/dist/edge-node/index.d.ts +18 -0
  57. package/dist/edge-node/index.js +18 -0
  58. package/dist/edge-node/index.js.map +1 -0
  59. package/dist/edge-node/join-credential.d.ts +11 -0
  60. package/dist/edge-node/join-credential.js +11 -0
  61. package/dist/edge-node/join-credential.js.map +1 -0
  62. package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
  63. package/dist/edge-node/lazy-raw-storage.js +134 -0
  64. package/dist/edge-node/lazy-raw-storage.js.map +1 -0
  65. package/dist/edge-node/node-local.d.ts +60 -0
  66. package/dist/edge-node/node-local.js +58 -0
  67. package/dist/edge-node/node-local.js.map +1 -0
  68. package/dist/edge-node/options.d.ts +83 -0
  69. package/dist/edge-node/options.js +110 -0
  70. package/dist/edge-node/options.js.map +1 -0
  71. package/dist/edge-node/owner-grant.d.ts +41 -0
  72. package/dist/edge-node/owner-grant.js +38 -0
  73. package/dist/edge-node/owner-grant.js.map +1 -0
  74. package/dist/edge-node/platform.d.ts +140 -0
  75. package/dist/edge-node/platform.js +12 -0
  76. package/dist/edge-node/platform.js.map +1 -0
  77. package/dist/edge-node/strand-move.d.ts +115 -0
  78. package/dist/edge-node/strand-move.js +223 -0
  79. package/dist/edge-node/strand-move.js.map +1 -0
  80. package/dist/edge-node/strands.d.ts +28 -0
  81. package/dist/edge-node/strands.js +60 -0
  82. package/dist/edge-node/strands.js.map +1 -0
  83. package/dist/enrolled-machine-store.d.ts +3 -3
  84. package/dist/enrolled-machine-store.js +3 -3
  85. package/dist/formation-token.d.ts +48 -0
  86. package/dist/formation-token.js +26 -0
  87. package/dist/formation-token.js.map +1 -0
  88. package/dist/index.d.ts +27 -17
  89. package/dist/index.js +39 -17
  90. package/dist/index.js.map +1 -1
  91. package/dist/joined-strand-store.d.ts +29 -12
  92. package/dist/joined-strand-store.js +66 -27
  93. package/dist/joined-strand-store.js.map +1 -1
  94. package/dist/key-store.d.ts +9 -4
  95. package/dist/key-store.js +9 -4
  96. package/dist/key-store.js.map +1 -1
  97. package/dist/link-budget.d.ts +9 -7
  98. package/dist/link-budget.js +9 -7
  99. package/dist/link-budget.js.map +1 -1
  100. package/dist/machine-removal.d.ts +19 -39
  101. package/dist/machine-removal.js +24 -98
  102. package/dist/machine-removal.js.map +1 -1
  103. package/dist/member-redemption.d.ts +256 -0
  104. package/dist/member-redemption.js +490 -0
  105. package/dist/member-redemption.js.map +1 -0
  106. package/dist/membership-view.d.ts +91 -0
  107. package/dist/membership-view.js +110 -0
  108. package/dist/membership-view.js.map +1 -0
  109. package/dist/owner-anchor-sync.d.ts +4 -4
  110. package/dist/owner-anchor-sync.js +16 -15
  111. package/dist/owner-anchor-sync.js.map +1 -1
  112. package/dist/owner-grant-protocol.d.ts +251 -0
  113. package/dist/owner-grant-protocol.js +344 -0
  114. package/dist/owner-grant-protocol.js.map +1 -0
  115. package/dist/owner-key-export.d.ts +70 -0
  116. package/dist/owner-key-export.js +279 -0
  117. package/dist/owner-key-export.js.map +1 -0
  118. package/dist/owner-key-flows.d.ts +55 -0
  119. package/dist/owner-key-flows.js +60 -0
  120. package/dist/owner-key-flows.js.map +1 -0
  121. package/dist/owner-key-store.d.ts +31 -0
  122. package/dist/owner-key-store.js +81 -0
  123. package/dist/owner-key-store.js.map +1 -0
  124. package/dist/owner-keys.d.ts +78 -0
  125. package/dist/owner-keys.js +133 -0
  126. package/dist/owner-keys.js.map +1 -0
  127. package/dist/owner-signer.d.ts +54 -0
  128. package/dist/owner-signer.js +51 -0
  129. package/dist/owner-signer.js.map +1 -0
  130. package/dist/peer-addr-book.d.ts +10 -1
  131. package/dist/peer-addr-book.js +4 -3
  132. package/dist/peer-addr-book.js.map +1 -1
  133. package/dist/peer-authorization.d.ts +83 -18
  134. package/dist/peer-authorization.js +123 -53
  135. package/dist/peer-authorization.js.map +1 -1
  136. package/dist/peer-join-backfill.d.ts +89 -20
  137. package/dist/peer-join-backfill.js +265 -108
  138. package/dist/peer-join-backfill.js.map +1 -1
  139. package/dist/pending-join-runner.d.ts +48 -18
  140. package/dist/pending-join-runner.js +74 -56
  141. package/dist/pending-join-runner.js.map +1 -1
  142. package/dist/provider/rent-node.js +1 -1
  143. package/dist/provider/rent-node.js.map +1 -1
  144. package/dist/seed-bootstrap.d.ts +162 -133
  145. package/dist/seed-bootstrap.js +300 -283
  146. package/dist/seed-bootstrap.js.map +1 -1
  147. package/dist/seed-trust-policy.js +4 -4
  148. package/dist/strand-addr-protocol.d.ts +4 -1
  149. package/dist/strand-addr-protocol.js +8 -2
  150. package/dist/strand-addr-protocol.js.map +1 -1
  151. package/dist/strand-cohort.d.ts +3 -0
  152. package/dist/strand-cohort.js.map +1 -1
  153. package/dist/strand-database.d.ts +24 -2
  154. package/dist/strand-database.js +29 -6
  155. package/dist/strand-database.js.map +1 -1
  156. package/dist/strand-first-sync-gate.d.ts +176 -88
  157. package/dist/strand-first-sync-gate.js +288 -121
  158. package/dist/strand-first-sync-gate.js.map +1 -1
  159. package/dist/strand-formation-deadlines.d.ts +9 -9
  160. package/dist/strand-formation-deadlines.js +9 -9
  161. package/dist/strand-formation-manager.d.ts +23 -17
  162. package/dist/strand-formation-manager.js +46 -44
  163. package/dist/strand-formation-manager.js.map +1 -1
  164. package/dist/strand-formation-protocol.d.ts +5 -8
  165. package/dist/strand-formation-protocol.js +101 -16
  166. package/dist/strand-formation-protocol.js.map +1 -1
  167. package/dist/strand-instance-manager.d.ts +27 -18
  168. package/dist/strand-instance-manager.js +119 -50
  169. package/dist/strand-instance-manager.js.map +1 -1
  170. package/dist/strand-membership-reconciler.d.ts +16 -13
  171. package/dist/strand-membership-reconciler.js +30 -22
  172. package/dist/strand-membership-reconciler.js.map +1 -1
  173. package/dist/strand-membership-writer.d.ts +45 -22
  174. package/dist/strand-membership-writer.js +226 -131
  175. package/dist/strand-membership-writer.js.map +1 -1
  176. package/dist/strand-move.d.ts +6 -5
  177. package/dist/strand-move.js +36 -32
  178. package/dist/strand-move.js.map +1 -1
  179. package/dist/strand-peer-redial.js +2 -3
  180. package/dist/strand-peer-redial.js.map +1 -1
  181. package/dist/strand-revocation-enforcer.d.ts +134 -60
  182. package/dist/strand-revocation-enforcer.js +144 -71
  183. package/dist/strand-revocation-enforcer.js.map +1 -1
  184. package/dist/strand-solicitation.d.ts +50 -39
  185. package/dist/strand-solicitation.js +10 -3
  186. package/dist/strand-solicitation.js.map +1 -1
  187. package/dist/strand-watcher.d.ts +80 -13
  188. package/dist/strand-watcher.js +152 -48
  189. package/dist/strand-watcher.js.map +1 -1
  190. package/dist/table-change-watch.d.ts +63 -0
  191. package/dist/table-change-watch.js +209 -0
  192. package/dist/table-change-watch.js.map +1 -0
  193. package/dist/timeout-scheduler.d.ts +4 -3
  194. package/dist/timeout-scheduler.js +4 -3
  195. package/dist/timeout-scheduler.js.map +1 -1
  196. package/dist/types.d.ts +374 -150
  197. package/dist/types.js +4 -4
  198. package/dist/types.js.map +1 -1
  199. package/package.json +17 -8
  200. package/src/bootstrap-peer-store-file.ts +44 -44
  201. package/src/bootstrap-peer-store.ts +214 -214
  202. package/src/cadre-invite-protocol.ts +740 -1052
  203. package/src/cadre-join-request.ts +24 -3
  204. package/src/cadre-node.ts +2402 -1079
  205. package/src/control-authorization.ts +47 -14
  206. package/src/control-cohort.ts +204 -128
  207. package/src/control-database.ts +1749 -820
  208. package/src/control-formation-recorder.ts +359 -353
  209. package/src/control-protocol-guard.ts +23 -6
  210. package/src/control-schema-version.ts +301 -0
  211. package/src/control-schema.ts +273 -78
  212. package/src/control-write-retry.ts +641 -567
  213. package/src/delegate-admission.ts +351 -314
  214. package/src/ed25519-key.ts +174 -171
  215. package/src/edge-node/config.ts +277 -0
  216. package/src/edge-node/dissolve.ts +251 -0
  217. package/src/edge-node/edge-join.ts +319 -0
  218. package/src/edge-node/edge-node.ts +1045 -0
  219. package/src/edge-node/index.ts +59 -0
  220. package/src/edge-node/join-credential.ts +16 -0
  221. package/src/edge-node/lazy-raw-storage.ts +178 -0
  222. package/src/edge-node/node-local.ts +84 -0
  223. package/src/edge-node/options.ts +179 -0
  224. package/src/edge-node/owner-grant.ts +63 -0
  225. package/src/edge-node/platform.ts +151 -0
  226. package/src/edge-node/strand-move.ts +270 -0
  227. package/src/edge-node/strands.ts +72 -0
  228. package/src/enrolled-machine-store.ts +3 -3
  229. package/src/formation-token.ts +63 -0
  230. package/src/index.ts +899 -789
  231. package/src/joined-strand-store.ts +73 -28
  232. package/src/key-store.ts +10 -4
  233. package/src/link-budget.ts +9 -7
  234. package/src/machine-removal.ts +26 -111
  235. package/src/member-redemption.ts +649 -0
  236. package/src/membership-view.ts +144 -0
  237. package/src/owner-anchor-sync.ts +19 -18
  238. package/src/owner-grant-protocol.ts +547 -0
  239. package/src/owner-key-export.ts +348 -0
  240. package/src/owner-key-flows.ts +109 -0
  241. package/src/owner-key-store.ts +85 -0
  242. package/src/owner-keys.ts +178 -0
  243. package/src/owner-signer.ts +94 -0
  244. package/src/peer-addr-book.ts +4 -3
  245. package/src/peer-authorization.ts +558 -488
  246. package/src/peer-join-backfill.ts +971 -761
  247. package/src/pending-join-runner.ts +76 -53
  248. package/src/provider/rent-node.ts +1 -1
  249. package/src/seed-bootstrap.ts +343 -349
  250. package/src/seed-trust-policy.ts +4 -4
  251. package/src/strand-addr-protocol.ts +547 -538
  252. package/src/strand-cohort.ts +4 -0
  253. package/src/strand-database.ts +352 -319
  254. package/src/strand-first-sync-gate.ts +334 -133
  255. package/src/strand-formation-deadlines.ts +227 -227
  256. package/src/strand-formation-manager.ts +731 -725
  257. package/src/strand-formation-protocol.ts +122 -24
  258. package/src/strand-instance-manager.ts +2092 -2026
  259. package/src/strand-membership-reconciler.ts +33 -25
  260. package/src/strand-membership-writer.ts +2093 -1974
  261. package/src/strand-move.ts +50 -38
  262. package/src/strand-peer-redial.ts +2 -3
  263. package/src/strand-revocation-enforcer.ts +740 -654
  264. package/src/strand-solicitation.ts +61 -34
  265. package/src/strand-watcher.ts +175 -56
  266. package/src/table-change-watch.ts +285 -0
  267. package/src/timeout-scheduler.ts +29 -28
  268. 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
+ }