@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.
- package/README.md +11 -10
- package/dist/cadre-node.d.ts +818 -19
- package/dist/cadre-node.js +1952 -99
- package/dist/cadre-node.js.map +1 -1
- package/dist/canonical-datetime.d.ts +33 -0
- package/dist/canonical-datetime.js +38 -0
- package/dist/canonical-datetime.js.map +1 -0
- package/dist/canonical-json.d.ts +12 -0
- package/dist/canonical-json.js +33 -0
- package/dist/canonical-json.js.map +1 -0
- package/dist/control-cohort.d.ts +63 -0
- package/dist/control-cohort.js +80 -0
- package/dist/control-cohort.js.map +1 -0
- package/dist/control-database.d.ts +279 -8
- package/dist/control-database.js +563 -134
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +89 -0
- package/dist/control-formation-recorder.js +139 -0
- package/dist/control-formation-recorder.js.map +1 -0
- package/dist/control-schema.d.ts +12 -0
- package/dist/control-schema.js +223 -0
- package/dist/control-schema.js.map +1 -0
- package/dist/control-stream.d.ts +54 -0
- package/dist/control-stream.js +104 -0
- package/dist/control-stream.js.map +1 -0
- package/dist/device-token.d.ts +50 -0
- package/dist/device-token.js +66 -0
- package/dist/device-token.js.map +1 -0
- package/dist/diagnostics/connection-path.d.ts +118 -0
- package/dist/diagnostics/connection-path.js +204 -0
- package/dist/diagnostics/connection-path.js.map +1 -0
- package/dist/diagnostics/webrtc-turn-tracker.d.ts +50 -0
- package/dist/diagnostics/webrtc-turn-tracker.js +196 -0
- package/dist/diagnostics/webrtc-turn-tracker.js.map +1 -0
- package/dist/ed25519-key.d.ts +42 -0
- package/dist/ed25519-key.js +51 -0
- package/dist/ed25519-key.js.map +1 -0
- package/dist/hibernation-manager.d.ts +79 -0
- package/dist/hibernation-manager.js +173 -23
- package/dist/hibernation-manager.js.map +1 -1
- package/dist/index.d.ts +25 -4
- package/dist/index.js +59 -4
- package/dist/index.js.map +1 -1
- package/dist/key-store-file.d.ts +52 -0
- package/dist/key-store-file.js +194 -0
- package/dist/key-store-file.js.map +1 -0
- package/dist/key-store.d.ts +75 -0
- package/dist/key-store.js +66 -0
- package/dist/key-store.js.map +1 -0
- package/dist/peer-authorization.d.ts +48 -0
- package/dist/peer-authorization.js +66 -0
- package/dist/peer-authorization.js.map +1 -0
- package/dist/peer-record.d.ts +89 -0
- package/dist/peer-record.js +117 -0
- package/dist/peer-record.js.map +1 -0
- package/dist/push-credentials.d.ts +34 -0
- package/dist/push-credentials.js +81 -0
- package/dist/push-credentials.js.map +1 -0
- package/dist/push-fanout.d.ts +137 -0
- package/dist/push-fanout.js +197 -0
- package/dist/push-fanout.js.map +1 -0
- package/dist/push-node.d.ts +38 -0
- package/dist/push-node.js +48 -0
- package/dist/push-node.js.map +1 -0
- package/dist/push-notifier-apns.d.ts +61 -0
- package/dist/push-notifier-apns.js +199 -0
- package/dist/push-notifier-apns.js.map +1 -0
- package/dist/push-notifier-fcm.d.ts +44 -0
- package/dist/push-notifier-fcm.js +182 -0
- package/dist/push-notifier-fcm.js.map +1 -0
- package/dist/push-notifier-shared.d.ts +18 -0
- package/dist/push-notifier-shared.js +25 -0
- package/dist/push-notifier-shared.js.map +1 -0
- package/dist/push-notifier.d.ts +52 -0
- package/dist/push-notifier.js +22 -0
- package/dist/push-notifier.js.map +1 -0
- package/dist/schema-verification.d.ts +11 -2
- package/dist/schema-verification.js +15 -4
- package/dist/schema-verification.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +228 -21
- package/dist/seed-bootstrap.js +489 -168
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +63 -0
- package/dist/seed-trust-policy.js +56 -0
- package/dist/seed-trust-policy.js.map +1 -0
- package/dist/strand-addr-protocol.d.ts +132 -0
- package/dist/strand-addr-protocol.js +313 -0
- package/dist/strand-addr-protocol.js.map +1 -0
- package/dist/strand-cohort.d.ts +54 -0
- package/dist/strand-cohort.js +38 -0
- package/dist/strand-cohort.js.map +1 -0
- package/dist/strand-database.d.ts +54 -6
- package/dist/strand-database.js +88 -100
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +79 -19
- package/dist/strand-formation-manager.js +196 -104
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +162 -0
- package/dist/strand-formation-protocol.js +279 -0
- package/dist/strand-formation-protocol.js.map +1 -0
- package/dist/strand-instance-manager.d.ts +73 -4
- package/dist/strand-instance-manager.js +167 -41
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +29 -0
- package/dist/strand-member-key.js +38 -0
- package/dist/strand-member-key.js.map +1 -0
- package/dist/strand-member-registry.d.ts +98 -0
- package/dist/strand-member-registry.js +129 -0
- package/dist/strand-member-registry.js.map +1 -0
- package/dist/strand-membership-writer.d.ts +361 -0
- package/dist/strand-membership-writer.js +449 -0
- package/dist/strand-membership-writer.js.map +1 -0
- package/dist/strand-solicitation.d.ts +77 -2
- package/dist/strand-solicitation.js +25 -4
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-payload.d.ts +30 -0
- package/dist/strand-wake-payload.js +20 -0
- package/dist/strand-wake-payload.js.map +1 -0
- package/dist/strand-wake-protocol.d.ts +114 -0
- package/dist/strand-wake-protocol.js +257 -0
- package/dist/strand-wake-protocol.js.map +1 -0
- package/dist/strand-watcher.d.ts +6 -2
- package/dist/strand-watcher.js +57 -17
- package/dist/strand-watcher.js.map +1 -1
- package/dist/types.d.ts +436 -33
- package/dist/types.js +22 -5
- package/dist/types.js.map +1 -1
- package/package.json +18 -8
- package/src/cadre-node.ts +2981 -948
- package/src/canonical-datetime.ts +39 -0
- package/src/canonical-json.ts +30 -0
- package/src/control-cohort.ts +103 -0
- package/src/control-database.ts +948 -383
- package/src/control-formation-recorder.ts +151 -0
- package/src/control-schema.ts +222 -0
- package/src/control-stream.ts +128 -0
- package/src/device-token.ts +89 -0
- package/src/diagnostics/connection-path.ts +277 -0
- package/src/diagnostics/webrtc-turn-tracker.ts +222 -0
- package/src/ed25519-key.ts +69 -0
- package/src/hibernation-manager.ts +197 -28
- package/src/index.ts +275 -79
- package/src/key-store-file.ts +203 -0
- package/src/key-store.ts +99 -0
- package/src/peer-authorization.ts +81 -0
- package/src/peer-record.ts +146 -0
- package/src/push-credentials.ts +84 -0
- package/src/push-fanout.ts +272 -0
- package/src/push-node.ts +64 -0
- package/src/push-notifier-apns.ts +247 -0
- package/src/push-notifier-fcm.ts +220 -0
- package/src/push-notifier-shared.ts +28 -0
- package/src/push-notifier.ts +52 -0
- package/src/schema-verification.ts +18 -4
- package/src/seed-bootstrap.ts +563 -223
- package/src/seed-trust-policy.ts +107 -0
- package/src/strand-addr-protocol.ts +410 -0
- package/src/strand-cohort.ts +77 -0
- package/src/strand-database.ts +113 -141
- package/src/strand-formation-manager.ts +233 -142
- package/src/strand-formation-protocol.ts +391 -0
- package/src/strand-instance-manager.ts +207 -56
- package/src/strand-member-key.ts +39 -0
- package/src/strand-member-registry.ts +177 -0
- package/src/strand-membership-writer.ts +640 -0
- package/src/strand-solicitation.ts +93 -4
- package/src/strand-wake-payload.ts +32 -0
- package/src/strand-wake-protocol.ts +331 -0
- package/src/strand-watcher.ts +61 -17
- 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
|
-
* @
|
|
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
|
|
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', '
|
|
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
|
-
* @
|
|
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
|
-
|
|
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,
|
|
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"}
|
package/dist/seed-bootstrap.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
173
|
-
* Use this on a phone after receiving an invite to connect to the
|
|
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
|
|
383
|
+
* @returns Connection to the owner
|
|
177
384
|
*/
|
|
178
385
|
dialInvite(invite: CadreInvite): Promise<void>;
|
|
179
386
|
}
|