@serfab/cadre-core 1.16.0 → 1.17.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 (199) hide show
  1. package/README.md +1 -1
  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 +55 -138
  6. package/dist/cadre-invite-protocol.js +94 -449
  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 +530 -247
  12. package/dist/cadre-node.js +1046 -525
  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 +22 -23
  18. package/dist/control-cohort.js +24 -24
  19. package/dist/control-cohort.js.map +1 -1
  20. package/dist/control-database.d.ts +416 -136
  21. package/dist/control-database.js +757 -378
  22. package/dist/control-database.js.map +1 -1
  23. package/dist/control-protocol-guard.d.ts +5 -4
  24. package/dist/control-protocol-guard.js +6 -3
  25. package/dist/control-protocol-guard.js.map +1 -1
  26. package/dist/control-schema-version.d.ts +80 -0
  27. package/dist/control-schema-version.js +203 -0
  28. package/dist/control-schema-version.js.map +1 -0
  29. package/dist/control-schema.d.ts +45 -5
  30. package/dist/control-schema.js +254 -70
  31. package/dist/control-schema.js.map +1 -1
  32. package/dist/control-write-retry.d.ts +5 -5
  33. package/dist/control-write-retry.js +10 -8
  34. package/dist/control-write-retry.js.map +1 -1
  35. package/dist/delegate-admission.d.ts +13 -0
  36. package/dist/delegate-admission.js +27 -0
  37. package/dist/delegate-admission.js.map +1 -1
  38. package/dist/ed25519-key.d.ts +10 -7
  39. package/dist/ed25519-key.js +23 -20
  40. package/dist/ed25519-key.js.map +1 -1
  41. package/dist/edge-node/config.d.ts +122 -0
  42. package/dist/edge-node/config.js +199 -0
  43. package/dist/edge-node/config.js.map +1 -0
  44. package/dist/edge-node/dissolve.d.ts +95 -0
  45. package/dist/edge-node/dissolve.js +184 -0
  46. package/dist/edge-node/dissolve.js.map +1 -0
  47. package/dist/edge-node/edge-join.d.ts +44 -0
  48. package/dist/edge-node/edge-join.js +227 -0
  49. package/dist/edge-node/edge-join.js.map +1 -0
  50. package/dist/edge-node/edge-node.d.ts +295 -0
  51. package/dist/edge-node/edge-node.js +704 -0
  52. package/dist/edge-node/edge-node.js.map +1 -0
  53. package/dist/edge-node/index.d.ts +18 -0
  54. package/dist/edge-node/index.js +18 -0
  55. package/dist/edge-node/index.js.map +1 -0
  56. package/dist/edge-node/join-credential.d.ts +11 -0
  57. package/dist/edge-node/join-credential.js +11 -0
  58. package/dist/edge-node/join-credential.js.map +1 -0
  59. package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
  60. package/dist/edge-node/lazy-raw-storage.js +134 -0
  61. package/dist/edge-node/lazy-raw-storage.js.map +1 -0
  62. package/dist/edge-node/node-local.d.ts +60 -0
  63. package/dist/edge-node/node-local.js +58 -0
  64. package/dist/edge-node/node-local.js.map +1 -0
  65. package/dist/edge-node/options.d.ts +83 -0
  66. package/dist/edge-node/options.js +110 -0
  67. package/dist/edge-node/options.js.map +1 -0
  68. package/dist/edge-node/owner-grant.d.ts +41 -0
  69. package/dist/edge-node/owner-grant.js +38 -0
  70. package/dist/edge-node/owner-grant.js.map +1 -0
  71. package/dist/edge-node/platform.d.ts +140 -0
  72. package/dist/edge-node/platform.js +12 -0
  73. package/dist/edge-node/platform.js.map +1 -0
  74. package/dist/edge-node/strand-move.d.ts +115 -0
  75. package/dist/edge-node/strand-move.js +223 -0
  76. package/dist/edge-node/strand-move.js.map +1 -0
  77. package/dist/edge-node/strands.d.ts +28 -0
  78. package/dist/edge-node/strands.js +60 -0
  79. package/dist/edge-node/strands.js.map +1 -0
  80. package/dist/index.d.ts +16 -7
  81. package/dist/index.js +26 -7
  82. package/dist/index.js.map +1 -1
  83. package/dist/joined-strand-store.d.ts +3 -3
  84. package/dist/joined-strand-store.js +3 -3
  85. package/dist/joined-strand-store.js.map +1 -1
  86. package/dist/key-store.d.ts +9 -4
  87. package/dist/key-store.js +9 -4
  88. package/dist/key-store.js.map +1 -1
  89. package/dist/machine-removal.d.ts +19 -39
  90. package/dist/machine-removal.js +24 -98
  91. package/dist/machine-removal.js.map +1 -1
  92. package/dist/member-redemption.d.ts +250 -0
  93. package/dist/member-redemption.js +485 -0
  94. package/dist/member-redemption.js.map +1 -0
  95. package/dist/owner-anchor-sync.d.ts +4 -4
  96. package/dist/owner-anchor-sync.js +16 -15
  97. package/dist/owner-anchor-sync.js.map +1 -1
  98. package/dist/owner-grant-protocol.d.ts +247 -0
  99. package/dist/owner-grant-protocol.js +330 -0
  100. package/dist/owner-grant-protocol.js.map +1 -0
  101. package/dist/owner-key-export.d.ts +70 -0
  102. package/dist/owner-key-export.js +279 -0
  103. package/dist/owner-key-export.js.map +1 -0
  104. package/dist/owner-key-flows.d.ts +55 -0
  105. package/dist/owner-key-flows.js +60 -0
  106. package/dist/owner-key-flows.js.map +1 -0
  107. package/dist/owner-key-store.d.ts +31 -0
  108. package/dist/owner-key-store.js +81 -0
  109. package/dist/owner-key-store.js.map +1 -0
  110. package/dist/owner-keys.d.ts +78 -0
  111. package/dist/owner-keys.js +133 -0
  112. package/dist/owner-keys.js.map +1 -0
  113. package/dist/owner-signer.d.ts +54 -0
  114. package/dist/owner-signer.js +51 -0
  115. package/dist/owner-signer.js.map +1 -0
  116. package/dist/peer-authorization.d.ts +83 -18
  117. package/dist/peer-authorization.js +123 -53
  118. package/dist/peer-authorization.js.map +1 -1
  119. package/dist/provider/rent-node.js +1 -1
  120. package/dist/provider/rent-node.js.map +1 -1
  121. package/dist/seed-bootstrap.d.ts +131 -109
  122. package/dist/seed-bootstrap.js +260 -248
  123. package/dist/seed-bootstrap.js.map +1 -1
  124. package/dist/seed-trust-policy.js +4 -4
  125. package/dist/strand-addr-protocol.d.ts +4 -1
  126. package/dist/strand-addr-protocol.js +8 -2
  127. package/dist/strand-addr-protocol.js.map +1 -1
  128. package/dist/strand-cohort.d.ts +3 -0
  129. package/dist/strand-cohort.js.map +1 -1
  130. package/dist/strand-database.d.ts +1 -0
  131. package/dist/strand-database.js +3 -0
  132. package/dist/strand-database.js.map +1 -1
  133. package/dist/strand-first-sync-gate.d.ts +4 -3
  134. package/dist/strand-first-sync-gate.js +4 -3
  135. package/dist/strand-first-sync-gate.js.map +1 -1
  136. package/dist/strand-instance-manager.d.ts +1 -1
  137. package/dist/strand-membership-writer.d.ts +13 -2
  138. package/dist/strand-membership-writer.js +52 -16
  139. package/dist/strand-membership-writer.js.map +1 -1
  140. package/dist/strand-move.d.ts +6 -5
  141. package/dist/strand-move.js +31 -30
  142. package/dist/strand-move.js.map +1 -1
  143. package/dist/strand-peer-redial.js +2 -3
  144. package/dist/strand-peer-redial.js.map +1 -1
  145. package/dist/types.d.ts +286 -103
  146. package/dist/types.js.map +1 -1
  147. package/package.json +11 -2
  148. package/src/bootstrap-peer-store-file.ts +44 -44
  149. package/src/bootstrap-peer-store.ts +214 -214
  150. package/src/cadre-invite-protocol.ts +652 -1052
  151. package/src/cadre-join-request.ts +24 -3
  152. package/src/cadre-node.ts +1166 -541
  153. package/src/control-authorization.ts +47 -14
  154. package/src/control-cohort.ts +127 -128
  155. package/src/control-database.ts +940 -456
  156. package/src/control-protocol-guard.ts +7 -4
  157. package/src/control-schema-version.ts +251 -0
  158. package/src/control-schema.ts +265 -70
  159. package/src/control-write-retry.ts +569 -567
  160. package/src/delegate-admission.ts +37 -0
  161. package/src/ed25519-key.ts +174 -171
  162. package/src/edge-node/config.ts +277 -0
  163. package/src/edge-node/dissolve.ts +251 -0
  164. package/src/edge-node/edge-join.ts +319 -0
  165. package/src/edge-node/edge-node.ts +1045 -0
  166. package/src/edge-node/index.ts +59 -0
  167. package/src/edge-node/join-credential.ts +16 -0
  168. package/src/edge-node/lazy-raw-storage.ts +178 -0
  169. package/src/edge-node/node-local.ts +84 -0
  170. package/src/edge-node/options.ts +179 -0
  171. package/src/edge-node/owner-grant.ts +63 -0
  172. package/src/edge-node/platform.ts +151 -0
  173. package/src/edge-node/strand-move.ts +270 -0
  174. package/src/edge-node/strands.ts +72 -0
  175. package/src/index.ts +886 -789
  176. package/src/joined-strand-store.ts +4 -4
  177. package/src/key-store.ts +10 -4
  178. package/src/machine-removal.ts +26 -111
  179. package/src/member-redemption.ts +642 -0
  180. package/src/owner-anchor-sync.ts +19 -18
  181. package/src/owner-grant-protocol.ts +532 -0
  182. package/src/owner-key-export.ts +348 -0
  183. package/src/owner-key-flows.ts +109 -0
  184. package/src/owner-key-store.ts +85 -0
  185. package/src/owner-keys.ts +178 -0
  186. package/src/owner-signer.ts +94 -0
  187. package/src/peer-authorization.ts +558 -488
  188. package/src/provider/rent-node.ts +1 -1
  189. package/src/seed-bootstrap.ts +301 -312
  190. package/src/seed-trust-policy.ts +4 -4
  191. package/src/strand-addr-protocol.ts +547 -538
  192. package/src/strand-cohort.ts +4 -0
  193. package/src/strand-database.ts +3 -0
  194. package/src/strand-first-sync-gate.ts +4 -3
  195. package/src/strand-instance-manager.ts +1 -1
  196. package/src/strand-membership-writer.ts +61 -16
  197. package/src/strand-move.ts +45 -36
  198. package/src/strand-peer-redial.ts +2 -3
  199. package/src/types.ts +304 -103
@@ -42,6 +42,7 @@
42
42
  import debug from 'debug';
43
43
  import { multiaddr } from '@multiformats/multiaddr';
44
44
  import { peerIdFromString } from '@libp2p/peer-id';
45
+ import type { StrandAddrOutcome } from './strand-addr-protocol.js';
45
46
 
46
47
  const log = debug('sereus:cadre:delegate-admission');
47
48
 
@@ -238,6 +239,42 @@ export function dueRelayAnnounces(
238
239
  );
239
240
  }
240
241
 
242
+ /**
243
+ * Fold one announce pass's outcomes into `refusing`, the relay peerIds remembered
244
+ * as refusing the strand-addr protocol. A relay whose negotiation refused it
245
+ * (`unsupported`) is added; one that replied with any status is removed, because
246
+ * it serves the protocol now. A relay that did not reply (`unreachable`) is left
247
+ * as it was: being down says nothing about what it serves. Only `relayPeerIds`
248
+ * are read, so a sibling asked in the same pass is never recorded. Mutates in
249
+ * place.
250
+ */
251
+ export function recordRelayRefusals(
252
+ refusing: Set<string>,
253
+ relayPeerIds: readonly string[],
254
+ outcomes: ReadonlyMap<string, StrandAddrOutcome>
255
+ ): void {
256
+ for (const relayPeerId of relayPeerIds) {
257
+ switch (outcomes.get(relayPeerId)) {
258
+ case 'unsupported':
259
+ refusing.add(relayPeerId);
260
+ break;
261
+ case 'answered':
262
+ case 'empty':
263
+ case 'unavailable':
264
+ refusing.delete(relayPeerId);
265
+ break;
266
+ }
267
+ }
268
+ }
269
+
270
+ /** Of `relays`, the ones not remembered in `refusing` (see {@link recordRelayRefusals}). */
271
+ export function relaysNotRefusing(
272
+ relays: readonly CircuitRelayTarget[],
273
+ refusing: ReadonlySet<string>
274
+ ): CircuitRelayTarget[] {
275
+ return relays.filter((relay) => !refusing.has(relay.relayPeerId));
276
+ }
277
+
241
278
  /**
242
279
  * Drop the entries of a {@link peerStrandKey}-keyed map whose strand is no longer
243
280
  * running, and — when `livePeerIds` is given — whose peer is not among them. A
@@ -1,171 +1,174 @@
1
- import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
2
- import { getPublicKey } from '@optimystic/quereus-plugin-crypto';
3
- import { peerIdFromPublicKey, peerIdFromString } from '@libp2p/peer-id';
4
- import { publicKeyFromRaw } from '@libp2p/crypto/keys';
5
- import type { PrivateKey } from '@libp2p/interface';
6
-
7
- /**
8
- * An Ed25519 keypair expressed in the base64url form that
9
- * `@optimystic/quereus-plugin-crypto` (`sign`/`verify`/`getPublicKey`) consumes.
10
- * Generic bridge type — reused for owner keys, member keys, and any other
11
- * seed→public keypair the control/strand databases sign with.
12
- */
13
- export interface Ed25519KeyPair {
14
- /** 32-byte Ed25519 seed, base64url-encoded — the crypto-plugin private key. */
15
- privateKeyB64: string;
16
- /** 32-byte Ed25519 public key, base64url-encoded. */
17
- publicKeyB64: string;
18
- }
19
-
20
- /**
21
- * Bridge a libp2p Ed25519 private key into the base64url keypair used by the
22
- * control- and strand-database signing constraints.
23
- *
24
- * libp2p stores an Ed25519 private key as 64 raw bytes: the first 32 are the
25
- * seed (the actual scalar source), the last 32 are the public key — see
26
- * `@libp2p/crypto`'s `Ed25519PrivateKey`. `@optimystic/quereus-plugin-crypto`
27
- * (via `@noble/curves`) treats the 32-byte seed *as* the private key and
28
- * derives the public key from it with standard Ed25519. The two derivations
29
- * agree, so the node's peer identity and its signing key are one keypair:
30
- * `getPublicKey(privateKeyB64)` === `publicKeyB64`.
31
- *
32
- * @param privateKey - The node's libp2p Ed25519 private key.
33
- * @returns The base64url seed/public-key pair for signing operations.
34
- * @throws If the key is not Ed25519 or the raw bytes aren't the expected length.
35
- */
36
- export function ed25519KeyPairFromLibp2p(privateKey: PrivateKey): Ed25519KeyPair {
37
- if (privateKey.type !== 'Ed25519') {
38
- throw new Error(`ed25519KeyPairFromLibp2p requires an Ed25519 key, got ${privateKey.type}`);
39
- }
40
-
41
- const raw = privateKey.raw;
42
- if (raw.length !== 64) {
43
- throw new Error(`Expected a 64-byte Ed25519 raw private key (seed||public), got ${raw.length} bytes`);
44
- }
45
-
46
- const seed = raw.subarray(0, 32);
47
- const publicKeyRaw = privateKey.publicKey.raw;
48
- if (publicKeyRaw.length !== 32) {
49
- throw new Error(`Expected a 32-byte Ed25519 public key, got ${publicKeyRaw.length} bytes`);
50
- }
51
-
52
- return {
53
- privateKeyB64: uint8ArrayToString(seed, 'base64url'),
54
- publicKeyB64: uint8ArrayToString(publicKeyRaw, 'base64url'),
55
- };
56
- }
57
-
58
- /**
59
- * Derive the base64url Ed25519 public key from a base64url 32-byte private seed
60
- * — the same derivation the seed-bootstrap signer uses internally
61
- * (`SeedBootstrapService` constructor). Use this to enroll a standalone
62
- * (non-libp2p) key into the control DB before minting an invite, when the
63
- * key is *not* the node's peer identity (so `ed25519KeyPairFromLibp2p`,
64
- * which needs a libp2p key object, does not apply).
65
- *
66
- * @param privateKeyB64 - The base64url-encoded 32-byte Ed25519 seed.
67
- * @returns The base64url-encoded Ed25519 public key.
68
- */
69
- export function ed25519PublicKeyFromPrivate(privateKeyB64: string): string {
70
- return getPublicKey(privateKeyB64, 'ed25519', 'base64url', 'base64url') as string;
71
- }
72
-
73
- /**
74
- * Derive the base64url ed25519 public key embedded in an Ed25519 libp2p PeerId.
75
- *
76
- * An Ed25519 PeerId is an identity multihash of the public key, so
77
- * `peerIdFromString(id).publicKey.raw` is the 32-byte ed25519 key whose
78
- * base64url form matches the `OwnerKey.Key` representation (and
79
- * `ed25519KeyPairFromLibp2p().publicKeyB64`). Returns null for a non-Ed25519
80
- * id, a missing embedded key, or any parse failure — callers treat null as
81
- * "not an owner" rather than throwing.
82
- *
83
- * Lives here, beside the other key bridges, so the lightweight verifiers in
84
- * `peer-authorization.ts` can check a stored `PeerKey` against a row's `PeerId`
85
- * without importing the seed-bootstrap runtime.
86
- */
87
- export function ed25519PublicKeyB64FromPeerId(peerId: string): string | null {
88
- try {
89
- const parsed = peerIdFromString(peerId);
90
- if (parsed.type !== 'Ed25519' || !parsed.publicKey) {
91
- return null;
92
- }
93
- return uint8ArrayToString(parsed.publicKey.raw, 'base64url');
94
- } catch {
95
- return null;
96
- }
97
- }
98
-
99
- /**
100
- * The inverse of {@link ed25519PublicKeyB64FromPeerId}: the libp2p peer id whose identity
101
- * is this ed25519 public key (base64url), or null when the value does not decode to a
102
- * 32-byte key. How an `OwnerKey` row is matched to the machine it names
103
- * (`machine-removal.ts`), since a machine's owner key is the key behind its peer id.
104
- */
105
- export function peerIdFromEd25519PublicKeyB64(publicKeyB64: string): string | null {
106
- try {
107
- const raw = uint8ArrayFromString(publicKeyB64, 'base64url');
108
- if (raw.length !== 32) {
109
- return null;
110
- }
111
- return peerIdFromPublicKey(publicKeyFromRaw(raw)).toString();
112
- } catch {
113
- return null;
114
- }
115
- }
116
-
117
- /** Longest rejected key value echoed back in an error — a real key is 43 base64url chars. */
118
- const REJECTED_VALUE_ECHO_LIMIT = 64;
119
-
120
- /**
121
- * Render a rejected value for an error message, capped: owner keys now reach this check
122
- * from remote-supplied fields (a cadre invitation's `ownerKeys`, a donation request's
123
- * `ownerKeys`), so an unbounded echo would let a peer turn one junk string into a
124
- * megabyte of log line.
125
- */
126
- function describeRejected(value: string): string {
127
- return value.length <= REJECTED_VALUE_ECHO_LIMIT
128
- ? value
129
- : `${value.slice(0, REJECTED_VALUE_ECHO_LIMIT)}… (${value.length} chars)`;
130
- }
131
-
132
- /**
133
- * Reject a value that isn't shaped like a base64url-encoded 32-byte Ed25519 public key
134
- * before it reaches the control database, where a malformed key would otherwise sit
135
- * indistinguishable from a real one until some later, unrelated signature check fails.
136
- *
137
- * Trims first, reusing the blank-value message for an empty result so that case keeps its
138
- * existing wording instead of a confusing base64url error. Every rejection names the offending
139
- * value (capped, see {@link describeRejected}) — with several keys in one batch, the message is
140
- * the only thing that says WHICH one. Does not check the decoded bytes
141
- * are a valid point on the Ed25519 curve — a well-formed-but-off-curve key still fails
142
- * signature verification later exactly like any other wrong key.
143
- *
144
- * @param value - Candidate base64url-encoded Ed25519 public key.
145
- * @param label - Human-readable name of the field, used in error messages.
146
- * @returns The trimmed value, so callers write the same bytes they validated.
147
- */
148
- export function requireEd25519PublicKeyB64(value: string, label: string): string {
149
- const trimmed = value.trim();
150
- if (trimmed.length === 0) {
151
- throw new Error(`A ${label} is required (received an empty or whitespace-only value)`);
152
- }
153
-
154
- let decoded: Uint8Array;
155
- try {
156
- decoded = uint8ArrayFromString(trimmed, 'base64url');
157
- } catch (error) {
158
- throw new Error(
159
- `A ${label} must be a base64url-encoded Ed25519 public key (could not decode "${describeRejected(trimmed)}" as base64url)`,
160
- { cause: error },
161
- );
162
- }
163
-
164
- if (decoded.length !== 32) {
165
- throw new Error(
166
- `A ${label} must be a base64url-encoded 32-byte Ed25519 public key ("${describeRejected(trimmed)}" decoded to ${decoded.length} bytes)`,
167
- );
168
- }
169
-
170
- return trimmed;
171
- }
1
+ import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
2
+ import { getPublicKey } from '@optimystic/quereus-plugin-crypto';
3
+ import { peerIdFromString } from '@libp2p/peer-id';
4
+ import type { PrivateKey } from '@libp2p/interface';
5
+
6
+ /**
7
+ * An Ed25519 keypair expressed in the base64url form that
8
+ * `@optimystic/quereus-plugin-crypto` (`sign`/`verify`/`getPublicKey`) consumes.
9
+ * Generic bridge type — reused for owner keys, member keys, and any other
10
+ * seed→public keypair the control/strand databases sign with.
11
+ */
12
+ export interface Ed25519KeyPair {
13
+ /** 32-byte Ed25519 seed, base64url-encoded — the crypto-plugin private key. */
14
+ privateKeyB64: string;
15
+ /** 32-byte Ed25519 public key, base64url-encoded. */
16
+ publicKeyB64: string;
17
+ }
18
+
19
+ /**
20
+ * Bridge a libp2p Ed25519 private key into the base64url keypair used by the
21
+ * control- and strand-database signing constraints.
22
+ *
23
+ * libp2p stores an Ed25519 private key as 64 raw bytes: the first 32 are the
24
+ * seed (the actual scalar source), the last 32 are the public key — see
25
+ * `@libp2p/crypto`'s `Ed25519PrivateKey`. `@optimystic/quereus-plugin-crypto`
26
+ * (via `@noble/curves`) treats the 32-byte seed *as* the private key and
27
+ * derives the public key from it with standard Ed25519. The two derivations
28
+ * agree, so the node's peer identity and its signing key are one keypair:
29
+ * `getPublicKey(privateKeyB64)` === `publicKeyB64`.
30
+ *
31
+ * @param privateKey - The node's libp2p Ed25519 private key.
32
+ * @returns The base64url seed/public-key pair for signing operations.
33
+ * @throws If the key is not Ed25519 or the raw bytes aren't the expected length.
34
+ */
35
+ export function ed25519KeyPairFromLibp2p(privateKey: PrivateKey): Ed25519KeyPair {
36
+ if (privateKey.type !== 'Ed25519') {
37
+ throw new Error(`ed25519KeyPairFromLibp2p requires an Ed25519 key, got ${privateKey.type}`);
38
+ }
39
+
40
+ const raw = privateKey.raw;
41
+ if (raw.length !== 64) {
42
+ throw new Error(`Expected a 64-byte Ed25519 raw private key (seed||public), got ${raw.length} bytes`);
43
+ }
44
+
45
+ const seed = raw.subarray(0, 32);
46
+ const publicKeyRaw = privateKey.publicKey.raw;
47
+ if (publicKeyRaw.length !== 32) {
48
+ throw new Error(`Expected a 32-byte Ed25519 public key, got ${publicKeyRaw.length} bytes`);
49
+ }
50
+
51
+ return {
52
+ privateKeyB64: uint8ArrayToString(seed, 'base64url'),
53
+ publicKeyB64: uint8ArrayToString(publicKeyRaw, 'base64url'),
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Derive the base64url Ed25519 public key from a base64url 32-byte private seed
59
+ * — the same derivation the seed-bootstrap signer uses internally
60
+ * (`SeedBootstrapService` constructor). Use this to enroll a standalone
61
+ * (non-libp2p) key into the control DB before minting an invite, when the
62
+ * key is *not* the node's peer identity (so `ed25519KeyPairFromLibp2p`,
63
+ * which needs a libp2p key object, does not apply).
64
+ *
65
+ * @param privateKeyB64 - The base64url-encoded 32-byte Ed25519 seed.
66
+ * @returns The base64url-encoded Ed25519 public key.
67
+ */
68
+ export function ed25519PublicKeyFromPrivate(privateKeyB64: string): string {
69
+ return getPublicKey(privateKeyB64, 'ed25519', 'base64url', 'base64url') as string;
70
+ }
71
+
72
+ /**
73
+ * Reject a value that isn't a base64url-encoded 32-byte Ed25519 seed, the private-key form
74
+ * {@link ed25519PublicKeyFromPrivate} and the crypto plugin's `sign` take. Unlike
75
+ * {@link requireEd25519PublicKeyB64}, no message names the value: it is a private key.
76
+ *
77
+ * @param value - Candidate base64url-encoded Ed25519 seed; surrounding whitespace is ignored.
78
+ * @param label - Human-readable name of the field, used in error messages.
79
+ * @returns The seed re-encoded as canonical base64url, so two spellings of one key compare equal.
80
+ */
81
+ export function requireEd25519PrivateKeyB64(value: string, label: string): string {
82
+ let decoded: Uint8Array;
83
+ try {
84
+ decoded = uint8ArrayFromString(value.trim(), 'base64url');
85
+ } catch (error) {
86
+ throw new Error(`The ${label} must be a base64url-encoded Ed25519 private key (it does not decode as base64url)`, { cause: error });
87
+ }
88
+ if (decoded.length !== 32) {
89
+ throw new Error(`The ${label} must be a base64url-encoded 32-byte Ed25519 private key (it decoded to ${decoded.length} bytes)`);
90
+ }
91
+ return uint8ArrayToString(decoded, 'base64url');
92
+ }
93
+
94
+ /**
95
+ * Derive the base64url ed25519 public key embedded in an Ed25519 libp2p PeerId.
96
+ *
97
+ * An Ed25519 PeerId is an identity multihash of the public key, so
98
+ * `peerIdFromString(id).publicKey.raw` is the 32-byte ed25519 key whose
99
+ * base64url form matches the `OwnerKey.Key` representation (and
100
+ * `ed25519KeyPairFromLibp2p().publicKeyB64`). Returns null for a non-Ed25519
101
+ * id, a missing embedded key, or any parse failure — callers treat null as
102
+ * "not an owner" rather than throwing.
103
+ *
104
+ * Lives here, beside the other key bridges, so the lightweight verifiers in
105
+ * `peer-authorization.ts` can check a stored `PeerKey` against a row's `PeerId`
106
+ * without importing the seed-bootstrap runtime.
107
+ */
108
+ export function ed25519PublicKeyB64FromPeerId(peerId: string): string | null {
109
+ try {
110
+ const parsed = peerIdFromString(peerId);
111
+ if (parsed.type !== 'Ed25519' || !parsed.publicKey) {
112
+ return null;
113
+ }
114
+ return uint8ArrayToString(parsed.publicKey.raw, 'base64url');
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /** Longest rejected key value echoed back in an error — a real key is 43 base64url chars. */
121
+ const REJECTED_VALUE_ECHO_LIMIT = 64;
122
+
123
+ /**
124
+ * Render a rejected value for an error message, capped: owner keys now reach this check
125
+ * from remote-supplied fields (a cadre invitation's `ownerKeys`, a donation request's
126
+ * `ownerKeys`), so an unbounded echo would let a peer turn one junk string into a
127
+ * megabyte of log line.
128
+ */
129
+ function describeRejected(value: string): string {
130
+ return value.length <= REJECTED_VALUE_ECHO_LIMIT
131
+ ? value
132
+ : `${value.slice(0, REJECTED_VALUE_ECHO_LIMIT)}… (${value.length} chars)`;
133
+ }
134
+
135
+ /**
136
+ * Reject a value that isn't shaped like a base64url-encoded 32-byte Ed25519 public key
137
+ * before it reaches the control database, where a malformed key would otherwise sit
138
+ * indistinguishable from a real one until some later, unrelated signature check fails.
139
+ *
140
+ * Trims first, reusing the blank-value message for an empty result so that case keeps its
141
+ * existing wording instead of a confusing base64url error. Every rejection names the offending
142
+ * value (capped, see {@link describeRejected}) — with several keys in one batch, the message is
143
+ * the only thing that says WHICH one. Does not check the decoded bytes
144
+ * are a valid point on the Ed25519 curve — a well-formed-but-off-curve key still fails
145
+ * signature verification later exactly like any other wrong key.
146
+ *
147
+ * @param value - Candidate base64url-encoded Ed25519 public key.
148
+ * @param label - Human-readable name of the field, used in error messages.
149
+ * @returns The trimmed value, so callers write the same bytes they validated.
150
+ */
151
+ export function requireEd25519PublicKeyB64(value: string, label: string): string {
152
+ const trimmed = value.trim();
153
+ if (trimmed.length === 0) {
154
+ throw new Error(`A ${label} is required (received an empty or whitespace-only value)`);
155
+ }
156
+
157
+ let decoded: Uint8Array;
158
+ try {
159
+ decoded = uint8ArrayFromString(trimmed, 'base64url');
160
+ } catch (error) {
161
+ throw new Error(
162
+ `A ${label} must be a base64url-encoded Ed25519 public key (could not decode "${describeRejected(trimmed)}" as base64url)`,
163
+ { cause: error },
164
+ );
165
+ }
166
+
167
+ if (decoded.length !== 32) {
168
+ throw new Error(
169
+ `A ${label} must be a base64url-encoded 32-byte Ed25519 public key ("${describeRejected(trimmed)}" decoded to ${decoded.length} bytes)`,
170
+ );
171
+ }
172
+
173
+ return trimmed;
174
+ }