@serfab/cadre-core 0.7.1 → 0.9.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 (170) hide show
  1. package/README.md +11 -10
  2. package/dist/cadre-node.d.ts +818 -19
  3. package/dist/cadre-node.js +1952 -99
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/canonical-datetime.d.ts +33 -0
  6. package/dist/canonical-datetime.js +38 -0
  7. package/dist/canonical-datetime.js.map +1 -0
  8. package/dist/canonical-json.d.ts +12 -0
  9. package/dist/canonical-json.js +33 -0
  10. package/dist/canonical-json.js.map +1 -0
  11. package/dist/control-cohort.d.ts +63 -0
  12. package/dist/control-cohort.js +80 -0
  13. package/dist/control-cohort.js.map +1 -0
  14. package/dist/control-database.d.ts +279 -8
  15. package/dist/control-database.js +563 -134
  16. package/dist/control-database.js.map +1 -1
  17. package/dist/control-formation-recorder.d.ts +89 -0
  18. package/dist/control-formation-recorder.js +139 -0
  19. package/dist/control-formation-recorder.js.map +1 -0
  20. package/dist/control-schema.d.ts +12 -0
  21. package/dist/control-schema.js +223 -0
  22. package/dist/control-schema.js.map +1 -0
  23. package/dist/control-stream.d.ts +54 -0
  24. package/dist/control-stream.js +104 -0
  25. package/dist/control-stream.js.map +1 -0
  26. package/dist/device-token.d.ts +50 -0
  27. package/dist/device-token.js +66 -0
  28. package/dist/device-token.js.map +1 -0
  29. package/dist/diagnostics/connection-path.d.ts +118 -0
  30. package/dist/diagnostics/connection-path.js +204 -0
  31. package/dist/diagnostics/connection-path.js.map +1 -0
  32. package/dist/diagnostics/webrtc-turn-tracker.d.ts +50 -0
  33. package/dist/diagnostics/webrtc-turn-tracker.js +196 -0
  34. package/dist/diagnostics/webrtc-turn-tracker.js.map +1 -0
  35. package/dist/ed25519-key.d.ts +42 -0
  36. package/dist/ed25519-key.js +51 -0
  37. package/dist/ed25519-key.js.map +1 -0
  38. package/dist/hibernation-manager.d.ts +79 -0
  39. package/dist/hibernation-manager.js +173 -23
  40. package/dist/hibernation-manager.js.map +1 -1
  41. package/dist/index.d.ts +25 -4
  42. package/dist/index.js +59 -4
  43. package/dist/index.js.map +1 -1
  44. package/dist/key-store-file.d.ts +52 -0
  45. package/dist/key-store-file.js +194 -0
  46. package/dist/key-store-file.js.map +1 -0
  47. package/dist/key-store.d.ts +75 -0
  48. package/dist/key-store.js +66 -0
  49. package/dist/key-store.js.map +1 -0
  50. package/dist/peer-authorization.d.ts +48 -0
  51. package/dist/peer-authorization.js +66 -0
  52. package/dist/peer-authorization.js.map +1 -0
  53. package/dist/peer-record.d.ts +89 -0
  54. package/dist/peer-record.js +117 -0
  55. package/dist/peer-record.js.map +1 -0
  56. package/dist/push-credentials.d.ts +34 -0
  57. package/dist/push-credentials.js +81 -0
  58. package/dist/push-credentials.js.map +1 -0
  59. package/dist/push-fanout.d.ts +137 -0
  60. package/dist/push-fanout.js +197 -0
  61. package/dist/push-fanout.js.map +1 -0
  62. package/dist/push-node.d.ts +38 -0
  63. package/dist/push-node.js +48 -0
  64. package/dist/push-node.js.map +1 -0
  65. package/dist/push-notifier-apns.d.ts +61 -0
  66. package/dist/push-notifier-apns.js +199 -0
  67. package/dist/push-notifier-apns.js.map +1 -0
  68. package/dist/push-notifier-fcm.d.ts +44 -0
  69. package/dist/push-notifier-fcm.js +182 -0
  70. package/dist/push-notifier-fcm.js.map +1 -0
  71. package/dist/push-notifier-shared.d.ts +18 -0
  72. package/dist/push-notifier-shared.js +25 -0
  73. package/dist/push-notifier-shared.js.map +1 -0
  74. package/dist/push-notifier.d.ts +52 -0
  75. package/dist/push-notifier.js +22 -0
  76. package/dist/push-notifier.js.map +1 -0
  77. package/dist/schema-verification.d.ts +11 -2
  78. package/dist/schema-verification.js +15 -4
  79. package/dist/schema-verification.js.map +1 -1
  80. package/dist/seed-bootstrap.d.ts +228 -21
  81. package/dist/seed-bootstrap.js +489 -168
  82. package/dist/seed-bootstrap.js.map +1 -1
  83. package/dist/seed-trust-policy.d.ts +63 -0
  84. package/dist/seed-trust-policy.js +56 -0
  85. package/dist/seed-trust-policy.js.map +1 -0
  86. package/dist/strand-addr-protocol.d.ts +132 -0
  87. package/dist/strand-addr-protocol.js +313 -0
  88. package/dist/strand-addr-protocol.js.map +1 -0
  89. package/dist/strand-cohort.d.ts +54 -0
  90. package/dist/strand-cohort.js +38 -0
  91. package/dist/strand-cohort.js.map +1 -0
  92. package/dist/strand-database.d.ts +54 -6
  93. package/dist/strand-database.js +88 -100
  94. package/dist/strand-database.js.map +1 -1
  95. package/dist/strand-formation-manager.d.ts +79 -19
  96. package/dist/strand-formation-manager.js +196 -104
  97. package/dist/strand-formation-manager.js.map +1 -1
  98. package/dist/strand-formation-protocol.d.ts +162 -0
  99. package/dist/strand-formation-protocol.js +279 -0
  100. package/dist/strand-formation-protocol.js.map +1 -0
  101. package/dist/strand-instance-manager.d.ts +73 -4
  102. package/dist/strand-instance-manager.js +167 -41
  103. package/dist/strand-instance-manager.js.map +1 -1
  104. package/dist/strand-member-key.d.ts +29 -0
  105. package/dist/strand-member-key.js +38 -0
  106. package/dist/strand-member-key.js.map +1 -0
  107. package/dist/strand-member-registry.d.ts +98 -0
  108. package/dist/strand-member-registry.js +129 -0
  109. package/dist/strand-member-registry.js.map +1 -0
  110. package/dist/strand-membership-writer.d.ts +361 -0
  111. package/dist/strand-membership-writer.js +449 -0
  112. package/dist/strand-membership-writer.js.map +1 -0
  113. package/dist/strand-solicitation.d.ts +77 -2
  114. package/dist/strand-solicitation.js +25 -4
  115. package/dist/strand-solicitation.js.map +1 -1
  116. package/dist/strand-wake-payload.d.ts +30 -0
  117. package/dist/strand-wake-payload.js +20 -0
  118. package/dist/strand-wake-payload.js.map +1 -0
  119. package/dist/strand-wake-protocol.d.ts +114 -0
  120. package/dist/strand-wake-protocol.js +257 -0
  121. package/dist/strand-wake-protocol.js.map +1 -0
  122. package/dist/strand-watcher.d.ts +6 -2
  123. package/dist/strand-watcher.js +57 -17
  124. package/dist/strand-watcher.js.map +1 -1
  125. package/dist/types.d.ts +436 -33
  126. package/dist/types.js +22 -5
  127. package/dist/types.js.map +1 -1
  128. package/package.json +18 -8
  129. package/src/cadre-node.ts +2981 -948
  130. package/src/canonical-datetime.ts +39 -0
  131. package/src/canonical-json.ts +30 -0
  132. package/src/control-cohort.ts +103 -0
  133. package/src/control-database.ts +948 -383
  134. package/src/control-formation-recorder.ts +151 -0
  135. package/src/control-schema.ts +222 -0
  136. package/src/control-stream.ts +128 -0
  137. package/src/device-token.ts +89 -0
  138. package/src/diagnostics/connection-path.ts +277 -0
  139. package/src/diagnostics/webrtc-turn-tracker.ts +222 -0
  140. package/src/ed25519-key.ts +69 -0
  141. package/src/hibernation-manager.ts +197 -28
  142. package/src/index.ts +275 -79
  143. package/src/key-store-file.ts +203 -0
  144. package/src/key-store.ts +99 -0
  145. package/src/peer-authorization.ts +81 -0
  146. package/src/peer-record.ts +146 -0
  147. package/src/push-credentials.ts +84 -0
  148. package/src/push-fanout.ts +272 -0
  149. package/src/push-node.ts +64 -0
  150. package/src/push-notifier-apns.ts +247 -0
  151. package/src/push-notifier-fcm.ts +220 -0
  152. package/src/push-notifier-shared.ts +28 -0
  153. package/src/push-notifier.ts +52 -0
  154. package/src/schema-verification.ts +18 -4
  155. package/src/seed-bootstrap.ts +563 -223
  156. package/src/seed-trust-policy.ts +107 -0
  157. package/src/strand-addr-protocol.ts +410 -0
  158. package/src/strand-cohort.ts +77 -0
  159. package/src/strand-database.ts +113 -141
  160. package/src/strand-formation-manager.ts +233 -142
  161. package/src/strand-formation-protocol.ts +391 -0
  162. package/src/strand-instance-manager.ts +207 -56
  163. package/src/strand-member-key.ts +39 -0
  164. package/src/strand-member-registry.ts +177 -0
  165. package/src/strand-membership-writer.ts +640 -0
  166. package/src/strand-solicitation.ts +93 -4
  167. package/src/strand-wake-payload.ts +32 -0
  168. package/src/strand-wake-protocol.ts +331 -0
  169. package/src/strand-watcher.ts +61 -17
  170. package/src/types.ts +1032 -582
@@ -0,0 +1,52 @@
1
+ /**
2
+ * push-notifier.ts — the platform-push **delivery** contract for strand-wake.
3
+ *
4
+ * This module is the cross-platform-safe *interface* half: the message shape
5
+ * ({@link PushMessage}), the outcome value ({@link PushSendResult}), and the
6
+ * {@link PushNotifier} port a `CadreNode` sends over. It carries ZERO runtime
7
+ * dependencies and imports no implementation — so the RN/browser entry graph can
8
+ * reference the type without ever resolving `node:crypto` / `node:http2`.
9
+ *
10
+ * The concrete FCM/APNs implementations (`push-notifier-fcm.ts` /
11
+ * `push-notifier-apns.ts`) and the `createPushNotifier` router that builds them
12
+ * live behind the Node-only subpath `@serfab/cadre-core/push-node`
13
+ * (`push-node.ts`). A Node host constructs a notifier from that subpath and
14
+ * injects the instance into `CadreNodeConfig.push.notifier`; the cross-platform
15
+ * core never constructs one, so the Node-only builtins stay out of its graph.
16
+ *
17
+ * Keep this file zero-import (beyond erased type-only imports): the whole point
18
+ * of the seam is that referencing the interface can never drag an implementation
19
+ * module into a bundler's graph.
20
+ */
21
+ import type { PushPlatform } from './types.js';
22
+ import type { StrandWakePayload } from './strand-wake-payload.js';
23
+ /** One strand-wake data message addressed to one device. */
24
+ export interface PushMessage {
25
+ /** The resolved `DeviceToken.token` for the target peer. */
26
+ token: string;
27
+ /** Which channel — selects FCM vs APNs. */
28
+ platform: PushPlatform;
29
+ /** The strand-wake payload `{ type:'strand-wake', strandId, reason }`. */
30
+ payload: StrandWakePayload;
31
+ }
32
+ /**
33
+ * Outcome of a single {@link PushNotifier.send}. A delivery failure is a value,
34
+ * never a thrown error. `unregistered: true` means the platform reported the
35
+ * token is permanently invalid (FCM 404 `UNREGISTERED` / 400 naming the token;
36
+ * APNs 410 `Unregistered` / 400 `BadDeviceToken`) — the caller should expire the
37
+ * stale `DeviceToken`. Any other failure is `unregistered: false` and is treated
38
+ * as best-effort/transient (no retry storm).
39
+ */
40
+ export type PushSendResult = {
41
+ ok: true;
42
+ } | {
43
+ ok: false;
44
+ unregistered: boolean;
45
+ error: string;
46
+ };
47
+ export interface PushNotifier {
48
+ /** Send one strand-wake data message to one device. Never throws. */
49
+ send(msg: PushMessage): Promise<PushSendResult>;
50
+ /** Release transport resources (e.g. the APNs HTTP/2 session). */
51
+ close(): Promise<void>;
52
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * push-notifier.ts — the platform-push **delivery** contract for strand-wake.
3
+ *
4
+ * This module is the cross-platform-safe *interface* half: the message shape
5
+ * ({@link PushMessage}), the outcome value ({@link PushSendResult}), and the
6
+ * {@link PushNotifier} port a `CadreNode` sends over. It carries ZERO runtime
7
+ * dependencies and imports no implementation — so the RN/browser entry graph can
8
+ * reference the type without ever resolving `node:crypto` / `node:http2`.
9
+ *
10
+ * The concrete FCM/APNs implementations (`push-notifier-fcm.ts` /
11
+ * `push-notifier-apns.ts`) and the `createPushNotifier` router that builds them
12
+ * live behind the Node-only subpath `@serfab/cadre-core/push-node`
13
+ * (`push-node.ts`). A Node host constructs a notifier from that subpath and
14
+ * injects the instance into `CadreNodeConfig.push.notifier`; the cross-platform
15
+ * core never constructs one, so the Node-only builtins stay out of its graph.
16
+ *
17
+ * Keep this file zero-import (beyond erased type-only imports): the whole point
18
+ * of the seam is that referencing the interface can never drag an implementation
19
+ * module into a bundler's graph.
20
+ */
21
+ export {};
22
+ //# sourceMappingURL=push-notifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"push-notifier.js","sourceRoot":"","sources":["../src/push-notifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG"}
@@ -31,7 +31,16 @@ export declare function verifySchema(schema: string, version: string, signature:
31
31
  * Assert that an SAppConfig has a valid schema signature.
32
32
  * Throws SchemaVerificationError on failure.
33
33
  *
34
+ * Fail-closed by default: when `options.requireSignature` is not explicitly
35
+ * `false`, an absent signature is rejected with reason `'missing signature'`,
36
+ * distinct from the `'invalid signature'` (tampered/wrong-key) case. The
37
+ * relaxation (`requireSignature: false`) only excuses *absence* of a signature;
38
+ * a present-but-bad signature still throws.
39
+ *
34
40
  * @param sAppConfig - The sApp configuration to verify
35
- * @throws SchemaVerificationError if the signature is invalid
41
+ * @param options - Verification policy; `requireSignature` defaults to `true`
42
+ * @throws SchemaVerificationError if the signature is missing (when required) or invalid
36
43
  */
37
- export declare function assertSchemaSignature(sAppConfig: SAppConfig): void;
44
+ export declare function assertSchemaSignature(sAppConfig: SAppConfig, options?: {
45
+ requireSignature?: boolean;
46
+ }): void;
@@ -18,7 +18,7 @@ export class SchemaVerificationError extends Error {
18
18
  */
19
19
  function schemaDigest(schema, version) {
20
20
  const payload = JSON.stringify({ schema, version });
21
- return digest(payload, 'sha256', 'utf8', 'base64url');
21
+ return digest([payload], 'sha256', 'base64url');
22
22
  }
23
23
  /**
24
24
  * Sign an sApp schema with the author's ed25519 private key.
@@ -56,13 +56,24 @@ export function verifySchema(schema, version, signature, authorPublicKey) {
56
56
  * Assert that an SAppConfig has a valid schema signature.
57
57
  * Throws SchemaVerificationError on failure.
58
58
  *
59
+ * Fail-closed by default: when `options.requireSignature` is not explicitly
60
+ * `false`, an absent signature is rejected with reason `'missing signature'`,
61
+ * distinct from the `'invalid signature'` (tampered/wrong-key) case. The
62
+ * relaxation (`requireSignature: false`) only excuses *absence* of a signature;
63
+ * a present-but-bad signature still throws.
64
+ *
59
65
  * @param sAppConfig - The sApp configuration to verify
60
- * @throws SchemaVerificationError if the signature is invalid
66
+ * @param options - Verification policy; `requireSignature` defaults to `true`
67
+ * @throws SchemaVerificationError if the signature is missing (when required) or invalid
61
68
  */
62
- export function assertSchemaSignature(sAppConfig) {
69
+ export function assertSchemaSignature(sAppConfig, options) {
63
70
  const { id, version, schema, signature } = sAppConfig;
71
+ const requireSignature = options?.requireSignature ?? true;
64
72
  if (!signature) {
65
- log('No signature provided for %s v%s — skipping verification', id, version);
73
+ if (requireSignature) {
74
+ throw new SchemaVerificationError(id ?? '', version, 'missing signature');
75
+ }
76
+ log('No signature provided for %s v%s — skipping verification (policy relaxed)', id, version);
66
77
  return;
67
78
  }
68
79
  if (!id) {
@@ -1 +1 @@
1
- {"version":3,"file":"schema-verification.js","sourceRoot":"","sources":["../src/schema-verification.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAGzE,MAAM,GAAG,GAAG,KAAK,CAAC,4BAA4B,CAAC,CAAC;AAEhD;;GAEG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YACkB,MAAc,EACd,OAAe,EAC/B,MAAc;QAEd,KAAK,CAAC,uCAAuC,MAAM,KAAK,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC;QAJ9D,WAAM,GAAN,MAAM,CAAQ;QACd,YAAO,GAAP,OAAO,CAAQ;QAI/B,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,MAAc,EAAE,OAAe;IACnD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAW,CAAC;AAClE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,OAAe,EAAE,gBAAwB;IAClF,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,CAAW,CAAC;AAC/F,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,OAAe,EAAE,SAAiB,EAAE,eAAuB;IACtG,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACxC,OAAO,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IACjG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;QAC5C,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,UAAsB;IAC1D,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,UAAU,CAAC;IAEtD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,GAAG,CAAC,0DAA0D,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;QAC7E,OAAO;IACT,CAAC;IAED,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,uBAAuB,CAAC,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,2BAA2B,CAAC,CAAC;IACpF,CAAC;IAED,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,uBAAuB,CAAC,EAAE,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACtE,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"schema-verification.js","sourceRoot":"","sources":["../src/schema-verification.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,mCAAmC,CAAC;AAGzE,MAAM,GAAG,GAAG,KAAK,CAAC,4BAA4B,CAAC,CAAC;AAEhD;;GAEG;AACH,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAChD,YACkB,MAAc,EACd,OAAe,EAC/B,MAAc;QAEd,KAAK,CAAC,uCAAuC,MAAM,KAAK,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC;QAJ9D,WAAM,GAAN,MAAM,CAAQ;QACd,YAAO,GAAP,OAAO,CAAQ;QAI/B,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,MAAc,EAAE,OAAe;IACnD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,WAAW,CAAW,CAAC;AAC5D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,OAAe,EAAE,gBAAwB;IAClF,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,OAAO,IAAI,CAAC,CAAC,EAAE,gBAAgB,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,CAAW,CAAC;AAC/F,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,EAAE,OAAe,EAAE,SAAiB,EAAE,eAAuB;IACtG,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACxC,OAAO,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IACjG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,GAAG,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;QAC5C,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CACnC,UAAsB,EACtB,OAAwC;IAExC,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,UAAU,CAAC;IACtD,MAAM,gBAAgB,GAAG,OAAO,EAAE,gBAAgB,IAAI,IAAI,CAAC;IAE3D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,IAAI,gBAAgB,EAAE,CAAC;YACrB,MAAM,IAAI,uBAAuB,CAAC,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;QAC5E,CAAC;QACD,GAAG,CAAC,2EAA2E,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;QAC9F,OAAO;IACT,CAAC;IAED,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,IAAI,uBAAuB,CAAC,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,2BAA2B,CAAC,CAAC;IACpF,CAAC;IAED,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,CAAC,EAAE,CAAC;QAClD,MAAM,IAAI,uBAAuB,CAAC,EAAE,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC;IACtE,CAAC;AACH,CAAC"}
@@ -1,18 +1,49 @@
1
1
  import type { Libp2p } from '@libp2p/interface';
2
- import type { ControlNetworkSeed, SeedAckMessage, AuthorizePeerOptions, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite } from './types.js';
2
+ import type { ControlNetworkSeed, SeedAckMessage, AuthorizePeerOptions, ApplySeedResult, AddDroneOptions, AddPhoneOptions, DroneInitResult, InviteResult, CadreInvite, PeerAddressRecord, DeviceTokenRecord } from './types.js';
3
3
  import type { ControlDatabase } from './control-database.js';
4
+ import { type SeedTrustPolicy } from './seed-trust-policy.js';
4
5
  /** Protocol ID for seed delivery */
5
6
  export declare const SEED_PROTOCOL = "/sereus/seed/1.0.0";
7
+ /**
8
+ * Decode a 4-byte big-endian length-prefixed frame; returns the body bytes.
9
+ *
10
+ * Guards every parse site against malformed input: a buffer too short to hold
11
+ * the prefix, a declared length exceeding `maxLength`, and a declared length
12
+ * exceeding the bytes actually present. Returns a view (`subarray`, no copy) —
13
+ * the body is handed straight to `TextDecoder`.
14
+ */
15
+ export declare function decodeLengthPrefixedFrame(data: Uint8Array, maxLength?: number): Uint8Array;
16
+ /**
17
+ * Derive the base64url ed25519 public key embedded in an Ed25519 libp2p PeerId.
18
+ *
19
+ * An Ed25519 PeerId is an identity multihash of the public key, so
20
+ * `peerIdFromString(id).publicKey.raw` is the 32-byte ed25519 key whose
21
+ * base64url form matches the `OwnerKey.Key` representation (and
22
+ * `ed25519KeyPairFromLibp2p().publicKeyB64`). Returns null for a non-Ed25519
23
+ * id, a missing embedded key, or any parse failure — callers treat null as
24
+ * "not an owner" rather than throwing.
25
+ */
26
+ export declare function ed25519PublicKeyB64FromPeerId(peerId: string): string | null;
27
+ /**
28
+ * Canonical byte representation of the authenticated seed fields.
29
+ *
30
+ * Routes both the creator (`createSeed`) and the verifier
31
+ * (`validateSeedSignature`) through one builder so the signed bytes are
32
+ * identical regardless of key insertion order. `canonicalJson` sorts keys and
33
+ * drops `undefined`, so the signed payload is exactly `{ partyId, peers }` —
34
+ * the fields the producer actually emits.
35
+ */
36
+ export declare function canonicalSeedPayload(seed: Pick<ControlNetworkSeed, 'partyId' | 'peers'>): string;
6
37
  /**
7
38
  * Configuration for the SeedBootstrapService
8
39
  */
9
40
  export interface SeedBootstrapConfig {
10
41
  /** Party ID for this cadre */
11
42
  partyId: string;
12
- /** Authority private key for signing seeds and peer authorizations (base64url) */
13
- authorityPrivateKey?: string;
14
- /** Authority public key (base64url) - derived from private key if not provided */
15
- authorityPublicKey?: string;
43
+ /** Owner private key for signing seeds and peer authorizations (base64url) */
44
+ ownerPrivateKey?: string;
45
+ /** Owner public key (base64url) - derived from private key if not provided */
46
+ ownerPublicKey?: string;
16
47
  /**
17
48
  * Optional async resolver returning the multiaddrs to embed in invites.
18
49
  * When unset, `libp2pNode.getMultiaddrs()` is used. Hosts behind NAT supply
@@ -20,6 +51,30 @@ export interface SeedBootstrapConfig {
20
51
  * DDNS hostname and externally-mapped port.
21
52
  */
22
53
  inviteAddressResolver?: () => Promise<string[]>;
54
+ /**
55
+ * Trust anchor for incoming seeds. Decides whether a signature-verified
56
+ * `signerKey` should be trusted, against the receiver's known owner
57
+ * keys (NOT the seed body). Defaults to `dbAnchoredTrustPolicy()`, which
58
+ * rejects any signer not already in the `OwnerKey` table. An enrollment
59
+ * caller can pass a per-seed override to `applySeed` instead.
60
+ *
61
+ * A `CadreNode` forwards its node-wide `CadreNodeConfig.seedTrustPolicy` here
62
+ * — that is the only seam the inbound libp2p seed-protocol handler can use,
63
+ * since a network-delivered seed has no per-call override.
64
+ */
65
+ trustPolicy?: SeedTrustPolicy;
66
+ /**
67
+ * Time the inbound seed handler waits for the seed frame before aborting the
68
+ * read (ms). Defaults to {@link DEFAULT_SEED_READ_TIMEOUT_MS}. Bounds a
69
+ * buggy/compromised own-cadre node that opens a stream and never half-closes.
70
+ */
71
+ seedReadTimeoutMs?: number;
72
+ /**
73
+ * Cap on concurrent inbound seed streams (defaults to
74
+ * {@link DEFAULT_MAX_CONCURRENT_SEEDS}). Over the cap, a non-accepting ack is
75
+ * returned without applying any seed.
76
+ */
77
+ maxConcurrentSeeds?: number;
23
78
  }
24
79
  /**
25
80
  * Event callbacks for seed-related events
@@ -37,13 +92,18 @@ export interface SeedEventCallbacks {
37
92
  *
38
93
  * Seeds solve the cold-start problem: new nodes need control data to validate
39
94
  * connections, but can't get data without connecting first. Seeds pre-populate
40
- * the new node's cache with peer information and optionally transactions.
95
+ * the new node's cache with peer information.
41
96
  */
42
97
  export declare class SeedBootstrapService {
43
98
  private readonly config;
44
99
  private libp2pNode;
45
100
  private controlDatabase;
46
- private readonly authorityPublicKey;
101
+ private readonly ownerPublicKey;
102
+ private readonly trustPolicy;
103
+ private readonly seedReadTimeoutMs;
104
+ private readonly maxConcurrentSeeds;
105
+ /** In-flight inbound seed streams, used to enforce {@link maxConcurrentSeeds}. */
106
+ private activeStreams;
47
107
  private eventCallbacks;
48
108
  constructor(config: SeedBootstrapConfig);
49
109
  /**
@@ -52,32 +112,160 @@ export declare class SeedBootstrapService {
52
112
  */
53
113
  setEventCallbacks(callbacks: SeedEventCallbacks): void;
54
114
  /**
55
- * Initialize the service with libp2p node and control database
115
+ * Whether this service holds an owner private key, i.e. can produce the
116
+ * owner signatures that gate `CadrePeer` / `DeviceToken` inserts, deletes,
117
+ * and re-authorizations. A seed-listener-only service (`enableSeedListener`,
118
+ * no owner key) returns false: it can receive/apply seeds but cannot author
119
+ * or re-issue owner writes. Used by the write-while-alone re-replication
120
+ * drain to skip owner work on a non-owner node.
56
121
  */
57
- initialize(libp2pNode: Libp2p, controlDatabase: ControlDatabase): void;
122
+ canAuthorize(): boolean;
123
+ /**
124
+ * Initialize the service with libp2p node and control database.
125
+ *
126
+ * `registerHandler` (default true) gates registration of the shared inbound
127
+ * `/sereus/seed/1.0.0` handler on `libp2pNode`. Persistent services
128
+ * (`initializeSeedBootstrap`, `enableSeedListener`) own that handler and leave
129
+ * it on. The throwaway temp services CadreNode builds in `applySeed` /
130
+ * `dialInvite` pass `false`: they only need the stored `libp2pNode` /
131
+ * `controlDatabase` for dialing and known-key lookup, and must NOT bind a
132
+ * discarded closure to the shared node (a handler leak, and a second
133
+ * `handle()` of the same protocol throws `DuplicateProtocolHandlerError`).
134
+ */
135
+ initialize(libp2pNode: Libp2p, controlDatabase: ControlDatabase, options?: {
136
+ registerHandler?: boolean;
137
+ }): void;
58
138
  /**
59
139
  * Authorize a new peer to join the cadre.
60
- * Signs the peer ID with the authority key and inserts into CadrePeer table.
140
+ * Signs the peer ID with the owner key and inserts into CadrePeer table.
141
+ *
142
+ * The owner vouches the `PublicKey <-> PeerId` binding: rather than trust a
143
+ * caller-supplied key, the binding is enforced by construction — `PublicKey` is
144
+ * DERIVED from the (Ed25519) `peerId`. A non-Ed25519 peer id yields a null
145
+ * `PublicKey`, and such a row can never be self-updated (it has no key to
146
+ * verify against), which is correct. The row is inserted with a fresh
147
+ * `UpdatedAt` but no self-signature (`Sig` null) — the owner cannot produce
148
+ * the peer's self-signature, so the peer must self-publish (see
149
+ * {@link CadreNode.registerSelf}) before the row resolves.
61
150
  */
62
151
  authorizePeer(options: AuthorizePeerOptions): Promise<void>;
63
152
  /**
64
- * Remove a peer from the cadre by authority signature.
153
+ * Owner-signed INSERT of this node's OWN self-signed address record.
154
+ *
155
+ * Used by {@link CadreNode.registerSelf} when the node is not yet a member and
156
+ * is its own owner (it holds the owner key): the row is owner-signed
157
+ * (satisfying `AuthorizedInsert`) AND carries a valid self-`Sig`, so it resolves
158
+ * immediately without a follow-up self-update.
159
+ */
160
+ insertSelfPeerRecord(record: PeerAddressRecord): Promise<void>;
161
+ /**
162
+ * Shared owner-signed `CadrePeer` INSERT. Mints a fresh single-use `StampId`
163
+ * and signs the voucher digest `digest(peerId, stampId)` ({@link cadrePeerVoucherDigest})
164
+ * with the owner key (satisfying `AuthorizedInsert`), then writes the full record
165
+ * row with the vouching (owner, signature) persisted into VouchOwner/VouchSig.
166
+ * The owner signature does NOT cover the address columns — those are vouched only
167
+ * as far as the owner asserts them, and a peer's own `Sig` (when present) is what
168
+ * makes the row resolvable.
169
+ */
170
+ private insertCadrePeerRow;
171
+ /**
172
+ * Owner-signed INSERT of a peer's OWN self-signed `DeviceToken` row.
65
173
  *
66
- * The constraint over CadrePeer's `check on insert, delete` validates a
67
- * signature over `digest(old.PeerId, 'sha256', 'utf8')` by an authority
68
- * key. We use the same digest pattern as authorizePeer.
174
+ * Counterpart to {@link insertSelfPeerRecord} for the device-token registry: the
175
+ * row is owner-signed (satisfying `DeviceToken.AuthorizedInsert`, which vouches
176
+ * membership exactly as `CadrePeer.AuthorizedInsert` does) AND carries the peer's
177
+ * own self-`Sig` over the token payload. The owner signature covers only the
178
+ * PeerId — it does NOT vouch the token contents; the peer's `Sig` (verified at
179
+ * resolve time against the bound `CadrePeer.PublicKey`) is what makes the row
180
+ * resolvable. Used by {@link CadreNode.registerDeviceToken} for the first publish
181
+ * when the node is its own owner.
182
+ */
183
+ insertSelfDeviceToken(record: DeviceTokenRecord): Promise<void>;
184
+ /**
185
+ * Owner-signed DELETE of a peer's `DeviceToken` row (logout / token
186
+ * invalidation). The `DeviceToken.AuthorizedInsert` constraint gates both insert
187
+ * AND delete on an owner signature over `digest(old.PeerId)`, so — like
188
+ * {@link removePeer} for `CadrePeer` — clearing a token requires the owner key.
189
+ */
190
+ deleteDeviceToken(peerId: string): Promise<void>;
191
+ /**
192
+ * Sign a peer ID with the owner key for a `CadrePeer` / `DeviceToken`
193
+ * insert-or-delete. The signed bytes come from the shared
194
+ * {@link peerAuthorizationDigest} helper so the offline `cadre enroll register`
195
+ * verifier checks the exact same construction. Throws if no owner key is set.
196
+ */
197
+ private signPeerAuthorization;
198
+ /**
199
+ * Return the configured owner private key, or throw if none is set. The single
200
+ * precondition gate for every owner-signed write. {@link removePeer} /
201
+ * {@link reauthorizePeer} read the row's `StampId` from the DB BEFORE they sign, so
202
+ * they call this up front — otherwise a keyless service would either surface the
203
+ * wrong "Control database not initialized" error or, worse, silently no-op when the
204
+ * target row is absent (the early `stampId === null` return) instead of rejecting.
205
+ */
206
+ private requireOwnerPrivateKey;
207
+ /**
208
+ * Sign a base64url digest with the owner key (ed25519). The single place the
209
+ * owner private key is applied; callers pass the canonical digest for the specific
210
+ * action ({@link cadrePeerVoucherDigest} / {@link cadrePeerRemoveDigest} /
211
+ * {@link peerAuthorizationDigest}). Throws if no owner key is set.
212
+ */
213
+ private signDigest;
214
+ /**
215
+ * Remove a peer from the cadre by owner signature.
216
+ *
217
+ * The `CadrePeer.AuthorizedDelete` (`check on delete`) constraint validates a
218
+ * signature over the DISTINCT 'remove'-scoped digest `digest(old.PeerId, old.StampId,
219
+ * 'remove')` ({@link cadrePeerRemoveDigest}) by an owner key — deliberately NOT the
220
+ * insert voucher digest, so the row's stored `VouchSig` can never be replayed to delete.
69
221
  */
70
222
  removePeer(peerId: string): Promise<void>;
223
+ /**
224
+ * Owner "re-touch" of an existing `CadrePeer` membership row: bump
225
+ * `UpdatedAt` under the owner branch of `CadrePeer.AuthorizedUpdate` (a
226
+ * signature over `digest(peerId)` — the same construction {@link authorizePeer}
227
+ * uses) so the row is re-emitted as a fresh, broadcasting transaction.
228
+ *
229
+ * This is the write-while-alone re-replication primitive
230
+ * (`control-write-ensure-replicated`): a membership row that committed
231
+ * local-only (its block's cluster ≤1 at insert) is pushed to the cohort once it
232
+ * grows, by re-issuing this monotonic bump. It is an UPDATE (not the original
233
+ * INSERT) because the row already exists locally; a re-INSERT would hit the
234
+ * `PeerId` PK. Only the freshness stamp changes — `PublicKey` / `Multiaddr` /
235
+ * `Sig` are left intact — so it is safe over a row whose peer has not
236
+ * self-published (`Sig` null); the caller must skip a row that already carries a
237
+ * self-`Sig` (that row is the owning peer's to refresh, and bumping `UpdatedAt`
238
+ * without re-signing would invalidate its self-signature).
239
+ *
240
+ * @param peerId - the membership row to re-touch.
241
+ * @param updatedAt - the strictly-increasing freshness stamp to write.
242
+ * @throws if no owner private key is configured (a non-owner cannot
243
+ * re-sign another peer's row) or the control database is not initialized.
244
+ */
245
+ reauthorizePeer(peerId: string, updatedAt: number): Promise<void>;
71
246
  /**
72
247
  * Create a seed from the current control network state.
73
- * The seed contains peer information and is signed by an authority.
248
+ * The seed contains peer information and is signed by an owner.
74
249
  */
75
250
  createSeed(): Promise<ControlNetworkSeed>;
76
251
  /**
77
252
  * Apply a seed to populate the peer cache and enable connections.
78
- * Validates the seed signature before applying.
253
+ *
254
+ * Validates the seed signature, then evaluates a trust anchor for the
255
+ * `signerKey` that does NOT come from the seed body: the receiver's
256
+ * `OwnerKey` table (DB-anchored), optionally augmented by pinned keys or
257
+ * TOFU via the configured/overriding `SeedTrustPolicy`. A forged
258
+ * self-asserting seed — one that merely lists its own signer as an owner
259
+ * peer — no longer passes.
260
+ *
261
+ * @param seed - The seed to apply (already transport-decoded).
262
+ * @param options.trustPolicy - Per-call policy override (e.g. a
263
+ * `pinnedKeyTrustPolicy` derived from a `CadreInvite`) used instead of the
264
+ * service-configured default for this seed only.
79
265
  */
80
- applySeed(seed: ControlNetworkSeed): Promise<ApplySeedResult>;
266
+ applySeed(seed: ControlNetworkSeed, options?: {
267
+ trustPolicy?: SeedTrustPolicy;
268
+ }): Promise<ApplySeedResult>;
81
269
  /**
82
270
  * Encode a seed for out-of-band delivery (e.g., QR code, copy/paste).
83
271
  */
@@ -101,12 +289,31 @@ export declare class SeedBootstrapService {
101
289
  validateSeedSignature(seed: ControlNetworkSeed): boolean;
102
290
  /**
103
291
  * Query peers from the control database.
292
+ *
293
+ * Owner identity is sourced from the `OwnerKey` table, not from the
294
+ * transport peer ID. An Ed25519 libp2p PeerId embeds its public key (identity
295
+ * multihash), so each peer's ed25519 key is derivable from its `PeerId`; a
296
+ * peer is an owner iff that derived key is in the `OwnerKey` set.
297
+ * This makes any owner node markable — not just the local one — and ties
298
+ * `isOwner` to the control table rather than to `peerId === self`.
104
299
  */
105
300
  private queryPeers;
106
301
  /**
107
- * Register the seed protocol handler.
302
+ * Register the seed protocol handler. The inbound closure just delegates to
303
+ * {@link handleSeedStream} — extracted as a method so it has a unit-test seam
304
+ * (mirroring wake's `handleStream`) the inline closure never had.
108
305
  */
109
306
  private registerProtocolHandler;
307
+ /**
308
+ * Read one inbound seed frame, apply it, and write the ack.
309
+ *
310
+ * Hardened against a buggy/compromised own-cadre node: a concurrency cap (over
311
+ * {@link maxConcurrentSeeds}, reply without applying), a read timeout (a peer
312
+ * that never half-closes is aborted inside `readStreamToEnd`), and the existing
313
+ * malformed/oversized-frame guard — all reported as a non-accepting
314
+ * {@link SeedAckMessage} rather than a dropped/hung stream.
315
+ */
316
+ private handleSeedStream;
110
317
  /**
111
318
  * Shutdown the service.
112
319
  */
@@ -169,11 +376,11 @@ export declare class SeedBootstrapService {
169
376
  */
170
377
  decodeInvite(encoded: string): CadreInvite;
171
378
  /**
172
- * Dial an authority from an invite.
173
- * Use this on a phone after receiving an invite to connect to the authority.
379
+ * Dial an owner from an invite.
380
+ * Use this on a phone after receiving an invite to connect to the owner.
174
381
  *
175
382
  * @param invite - The invite received out-of-band
176
- * @returns Connection to the authority
383
+ * @returns Connection to the owner
177
384
  */
178
385
  dialInvite(invite: CadreInvite): Promise<void>;
179
386
  }