@serfab/cadre-core 0.6.0 → 0.8.1
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 +9 -8
- package/dist/authority-key.d.ts +41 -0
- package/dist/authority-key.js +52 -0
- package/dist/authority-key.js.map +1 -0
- package/dist/cadre-node.d.ts +777 -11
- package/dist/cadre-node.js +1912 -82
- 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 +259 -5
- package/dist/control-database.js +541 -128
- 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 +199 -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/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 +26 -0
- package/dist/peer-authorization.js +40 -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-notifier-apns.d.ts +53 -0
- package/dist/push-notifier-apns.js +195 -0
- package/dist/push-notifier-apns.js.map +1 -0
- package/dist/push-notifier-fcm.d.ts +38 -0
- package/dist/push-notifier-fcm.js +183 -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 +71 -0
- package/dist/push-notifier.js +50 -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 +210 -7
- package/dist/seed-bootstrap.js +429 -131
- 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 -89
- 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 +413 -23
- package/dist/types.js +22 -5
- package/dist/types.js.map +1 -1
- package/package.json +14 -8
- package/src/authority-key.ts +68 -0
- package/src/cadre-node.ts +2937 -929
- 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 +931 -383
- package/src/control-formation-recorder.ts +151 -0
- package/src/control-schema.ts +198 -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/hibernation-manager.ts +197 -28
- package/src/index.ts +273 -79
- package/src/key-store-file.ts +203 -0
- package/src/key-store.ts +99 -0
- package/src/peer-authorization.ts +53 -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-notifier-apns.ts +243 -0
- package/src/push-notifier-fcm.ts +221 -0
- package/src/push-notifier-shared.ts +28 -0
- package/src/push-notifier.ts +93 -0
- package/src/schema-verification.ts +18 -4
- package/src/seed-bootstrap.ts +504 -171
- 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 -126
- 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 +1012 -575
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* push-notifier.ts — the platform-push **delivery** seam for strand-wake.
|
|
3
|
+
*
|
|
4
|
+
* Given a resolved mobile peer token, send one `strand-wake` data message over
|
|
5
|
+
* the right platform channel (FCM for Android, APNs for iOS). This module owns no
|
|
6
|
+
* policy — *who* to wake and *when* is the fan-out (`push-fanout.ts`). It is the
|
|
7
|
+
* reusable, credential-injected, transport-injected unit the fan-out constructs
|
|
8
|
+
* inside a participating `CadreNode` when push credentials are configured.
|
|
9
|
+
*
|
|
10
|
+
* `createPushNotifier` returns a router that dispatches by `msg.platform` to an
|
|
11
|
+
* `FcmPushNotifier` and/or `ApnsPushNotifier`, constructing only the
|
|
12
|
+
* implementations whose credentials are present. Failures are values, never
|
|
13
|
+
* thrown — mirroring `ServiceWakeResult` / `resolveDeviceToken`'s no-throw
|
|
14
|
+
* conventions so a best-effort fan-out can branch on the outcome.
|
|
15
|
+
*
|
|
16
|
+
* Server-only by construction: the FCM/APNs implementations reach for
|
|
17
|
+
* `node:crypto` / `node:http2`. They are never pulled into the RN/browser bundle
|
|
18
|
+
* because the cross-platform `index.ts` re-exports only the *types* here (a
|
|
19
|
+
* type-only re-export is erased at emit), and the only runtime constructor of a
|
|
20
|
+
* notifier is the in-package fan-out that runs inside a Node `CadreNode`. A
|
|
21
|
+
* cross-platform consumer imports the payload contract + types, never this value.
|
|
22
|
+
*/
|
|
23
|
+
import { createFcmPushNotifier } from './push-notifier-fcm.js';
|
|
24
|
+
import { createApnsPushNotifier } from './push-notifier-apns.js';
|
|
25
|
+
/**
|
|
26
|
+
* Build a {@link PushNotifier} router over the configured credentials. Only the
|
|
27
|
+
* platforms whose credentials are present get a backing implementation; a `send`
|
|
28
|
+
* for an unconfigured platform returns a best-effort `no <platform> credentials`
|
|
29
|
+
* failure rather than throwing.
|
|
30
|
+
*/
|
|
31
|
+
export function createPushNotifier(creds, deps = {}) {
|
|
32
|
+
const fcm = creds.fcm ? createFcmPushNotifier(creds.fcm, deps.fcm) : undefined;
|
|
33
|
+
const apns = creds.apns ? createApnsPushNotifier(creds.apns, deps.apns) : undefined;
|
|
34
|
+
async function send(msg) {
|
|
35
|
+
if (msg.platform === 'fcm')
|
|
36
|
+
return fcm ? fcm.send(msg) : noCreds('fcm');
|
|
37
|
+
if (msg.platform === 'apns')
|
|
38
|
+
return apns ? apns.send(msg) : noCreds('apns');
|
|
39
|
+
return { ok: false, unregistered: false, error: `unknown platform ${String(msg.platform)}` };
|
|
40
|
+
}
|
|
41
|
+
async function close() {
|
|
42
|
+
await Promise.all([fcm?.close(), apns?.close()]);
|
|
43
|
+
}
|
|
44
|
+
return { send, close };
|
|
45
|
+
}
|
|
46
|
+
/** Best-effort no-op result for a platform with no configured credentials. */
|
|
47
|
+
function noCreds(platform) {
|
|
48
|
+
return { ok: false, unregistered: false, error: `no ${platform} credentials` };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=push-notifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"push-notifier.js","sourceRoot":"","sources":["../src/push-notifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAAE,qBAAqB,EAAoB,MAAM,wBAAwB,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAqB,MAAM,yBAAyB,CAAC;AAwCpF;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAsB,EAAE,OAAyB,EAAE;IACpF,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,qBAAqB,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC/E,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,sBAAsB,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAEpF,KAAK,UAAU,IAAI,CAAC,GAAgB;QAClC,IAAI,GAAG,CAAC,QAAQ,KAAK,KAAK;YAAE,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACxE,IAAI,GAAG,CAAC,QAAQ,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC5E,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,oBAAoB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;IAC/F,CAAC;IAED,KAAK,UAAU,KAAK;QAClB,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACnD,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACzB,CAAC;AAED,8EAA8E;AAC9E,SAAS,OAAO,CAAC,QAAsB;IACrC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,QAAQ,cAAc,EAAE,CAAC;AACjF,CAAC"}
|
|
@@ -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,8 +1,39 @@
|
|
|
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 `AuthorityKey.Key` representation (and
|
|
22
|
+
* `authorityKeyFromLibp2p().publicKeyB64`). Returns null for a non-Ed25519
|
|
23
|
+
* id, a missing embedded key, or any parse failure — callers treat null as
|
|
24
|
+
* "not an authority" 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
|
*/
|
|
@@ -13,6 +44,37 @@ export interface SeedBootstrapConfig {
|
|
|
13
44
|
authorityPrivateKey?: string;
|
|
14
45
|
/** Authority public key (base64url) - derived from private key if not provided */
|
|
15
46
|
authorityPublicKey?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Optional async resolver returning the multiaddrs to embed in invites.
|
|
49
|
+
* When unset, `libp2pNode.getMultiaddrs()` is used. Hosts behind NAT supply
|
|
50
|
+
* this (via `@serfab/cadre-host`'s NatService) to substitute the host's
|
|
51
|
+
* DDNS hostname and externally-mapped port.
|
|
52
|
+
*/
|
|
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 authority
|
|
57
|
+
* keys (NOT the seed body). Defaults to `dbAnchoredTrustPolicy()`, which
|
|
58
|
+
* rejects any signer not already in the `AuthorityKey` 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;
|
|
16
78
|
}
|
|
17
79
|
/**
|
|
18
80
|
* Event callbacks for seed-related events
|
|
@@ -30,13 +92,18 @@ export interface SeedEventCallbacks {
|
|
|
30
92
|
*
|
|
31
93
|
* Seeds solve the cold-start problem: new nodes need control data to validate
|
|
32
94
|
* connections, but can't get data without connecting first. Seeds pre-populate
|
|
33
|
-
* the new node's cache with peer information
|
|
95
|
+
* the new node's cache with peer information.
|
|
34
96
|
*/
|
|
35
97
|
export declare class SeedBootstrapService {
|
|
36
98
|
private readonly config;
|
|
37
99
|
private libp2pNode;
|
|
38
100
|
private controlDatabase;
|
|
39
101
|
private readonly authorityPublicKey;
|
|
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;
|
|
40
107
|
private eventCallbacks;
|
|
41
108
|
constructor(config: SeedBootstrapConfig);
|
|
42
109
|
/**
|
|
@@ -45,14 +112,118 @@ export declare class SeedBootstrapService {
|
|
|
45
112
|
*/
|
|
46
113
|
setEventCallbacks(callbacks: SeedEventCallbacks): void;
|
|
47
114
|
/**
|
|
48
|
-
*
|
|
115
|
+
* Whether this service holds an authority private key, i.e. can produce the
|
|
116
|
+
* authority signatures that gate `CadrePeer` / `DeviceToken` inserts, deletes,
|
|
117
|
+
* and re-authorizations. A seed-listener-only service (`enableSeedListener`,
|
|
118
|
+
* no authority key) returns false: it can receive/apply seeds but cannot author
|
|
119
|
+
* or re-issue authority writes. Used by the write-while-alone re-replication
|
|
120
|
+
* drain to skip authority work on a non-authority node.
|
|
49
121
|
*/
|
|
50
|
-
|
|
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;
|
|
51
138
|
/**
|
|
52
139
|
* Authorize a new peer to join the cadre.
|
|
53
140
|
* Signs the peer ID with the authority key and inserts into CadrePeer table.
|
|
141
|
+
*
|
|
142
|
+
* The authority 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 authority cannot produce
|
|
148
|
+
* the peer's self-signature, so the peer must self-publish (see
|
|
149
|
+
* {@link CadreNode.registerSelf}) before the row resolves.
|
|
54
150
|
*/
|
|
55
151
|
authorizePeer(options: AuthorizePeerOptions): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Authority-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 authority (it holds the authority key): the row is authority-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 authority-signed `CadrePeer` INSERT. Signs `digest(peerId)` with the
|
|
163
|
+
* authority key (satisfying `AuthorizedInsert`) and writes the full record
|
|
164
|
+
* row. The authority signature does NOT cover the address columns — those are
|
|
165
|
+
* vouched only as far as the authority asserts them, and a peer's own `Sig`
|
|
166
|
+
* (when present) is what makes the row resolvable.
|
|
167
|
+
*/
|
|
168
|
+
private insertCadrePeerRow;
|
|
169
|
+
/**
|
|
170
|
+
* Authority-signed INSERT of a peer's OWN self-signed `DeviceToken` row.
|
|
171
|
+
*
|
|
172
|
+
* Counterpart to {@link insertSelfPeerRecord} for the device-token registry: the
|
|
173
|
+
* row is authority-signed (satisfying `DeviceToken.AuthorizedInsert`, which vouches
|
|
174
|
+
* membership exactly as `CadrePeer.AuthorizedInsert` does) AND carries the peer's
|
|
175
|
+
* own self-`Sig` over the token payload. The authority signature covers only the
|
|
176
|
+
* PeerId — it does NOT vouch the token contents; the peer's `Sig` (verified at
|
|
177
|
+
* resolve time against the bound `CadrePeer.PublicKey`) is what makes the row
|
|
178
|
+
* resolvable. Used by {@link CadreNode.registerDeviceToken} for the first publish
|
|
179
|
+
* when the node is its own authority.
|
|
180
|
+
*/
|
|
181
|
+
insertSelfDeviceToken(record: DeviceTokenRecord): Promise<void>;
|
|
182
|
+
/**
|
|
183
|
+
* Authority-signed DELETE of a peer's `DeviceToken` row (logout / token
|
|
184
|
+
* invalidation). The `DeviceToken.AuthorizedInsert` constraint gates both insert
|
|
185
|
+
* AND delete on an authority signature over `digest(old.PeerId)`, so — like
|
|
186
|
+
* {@link removePeer} for `CadrePeer` — clearing a token requires the authority key.
|
|
187
|
+
*/
|
|
188
|
+
deleteDeviceToken(peerId: string): Promise<void>;
|
|
189
|
+
/**
|
|
190
|
+
* Sign a peer ID with the authority key for a `CadrePeer` / `DeviceToken`
|
|
191
|
+
* insert-or-delete. The signed bytes come from the shared
|
|
192
|
+
* {@link peerAuthorizationDigest} helper so the offline `cadre enroll register`
|
|
193
|
+
* verifier checks the exact same construction. Throws if no authority key is set.
|
|
194
|
+
*/
|
|
195
|
+
private signPeerAuthorization;
|
|
196
|
+
/**
|
|
197
|
+
* Remove a peer from the cadre by authority signature.
|
|
198
|
+
*
|
|
199
|
+
* The constraint over CadrePeer's `check on insert, delete` validates a
|
|
200
|
+
* signature over `digest(old.PeerId)` by an authority key. We use the same
|
|
201
|
+
* digest pattern as authorizePeer.
|
|
202
|
+
*/
|
|
203
|
+
removePeer(peerId: string): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Authority "re-touch" of an existing `CadrePeer` membership row: bump
|
|
206
|
+
* `UpdatedAt` under the authority branch of `CadrePeer.AuthorizedUpdate` (a
|
|
207
|
+
* signature over `digest(peerId)` — the same construction {@link authorizePeer}
|
|
208
|
+
* uses) so the row is re-emitted as a fresh, broadcasting transaction.
|
|
209
|
+
*
|
|
210
|
+
* This is the write-while-alone re-replication primitive
|
|
211
|
+
* (`control-write-ensure-replicated`): a membership row that committed
|
|
212
|
+
* local-only (its block's cluster ≤1 at insert) is pushed to the cohort once it
|
|
213
|
+
* grows, by re-issuing this monotonic bump. It is an UPDATE (not the original
|
|
214
|
+
* INSERT) because the row already exists locally; a re-INSERT would hit the
|
|
215
|
+
* `PeerId` PK. Only the freshness stamp changes — `PublicKey` / `Multiaddr` /
|
|
216
|
+
* `Sig` are left intact — so it is safe over a row whose peer has not
|
|
217
|
+
* self-published (`Sig` null); the caller must skip a row that already carries a
|
|
218
|
+
* self-`Sig` (that row is the owning peer's to refresh, and bumping `UpdatedAt`
|
|
219
|
+
* without re-signing would invalidate its self-signature).
|
|
220
|
+
*
|
|
221
|
+
* @param peerId - the membership row to re-touch.
|
|
222
|
+
* @param updatedAt - the strictly-increasing freshness stamp to write.
|
|
223
|
+
* @throws if no authority private key is configured (a non-authority cannot
|
|
224
|
+
* re-sign another peer's row) or the control database is not initialized.
|
|
225
|
+
*/
|
|
226
|
+
reauthorizePeer(peerId: string, updatedAt: number): Promise<void>;
|
|
56
227
|
/**
|
|
57
228
|
* Create a seed from the current control network state.
|
|
58
229
|
* The seed contains peer information and is signed by an authority.
|
|
@@ -60,9 +231,22 @@ export declare class SeedBootstrapService {
|
|
|
60
231
|
createSeed(): Promise<ControlNetworkSeed>;
|
|
61
232
|
/**
|
|
62
233
|
* Apply a seed to populate the peer cache and enable connections.
|
|
63
|
-
*
|
|
234
|
+
*
|
|
235
|
+
* Validates the seed signature, then evaluates a trust anchor for the
|
|
236
|
+
* `signerKey` that does NOT come from the seed body: the receiver's
|
|
237
|
+
* `AuthorityKey` table (DB-anchored), optionally augmented by pinned keys or
|
|
238
|
+
* TOFU via the configured/overriding `SeedTrustPolicy`. A forged
|
|
239
|
+
* self-asserting seed — one that merely lists its own signer as an authority
|
|
240
|
+
* peer — no longer passes.
|
|
241
|
+
*
|
|
242
|
+
* @param seed - The seed to apply (already transport-decoded).
|
|
243
|
+
* @param options.trustPolicy - Per-call policy override (e.g. a
|
|
244
|
+
* `pinnedKeyTrustPolicy` derived from a `CadreInvite`) used instead of the
|
|
245
|
+
* service-configured default for this seed only.
|
|
64
246
|
*/
|
|
65
|
-
applySeed(seed: ControlNetworkSeed
|
|
247
|
+
applySeed(seed: ControlNetworkSeed, options?: {
|
|
248
|
+
trustPolicy?: SeedTrustPolicy;
|
|
249
|
+
}): Promise<ApplySeedResult>;
|
|
66
250
|
/**
|
|
67
251
|
* Encode a seed for out-of-band delivery (e.g., QR code, copy/paste).
|
|
68
252
|
*/
|
|
@@ -86,12 +270,31 @@ export declare class SeedBootstrapService {
|
|
|
86
270
|
validateSeedSignature(seed: ControlNetworkSeed): boolean;
|
|
87
271
|
/**
|
|
88
272
|
* Query peers from the control database.
|
|
273
|
+
*
|
|
274
|
+
* Authority identity is sourced from the `AuthorityKey` table, not from the
|
|
275
|
+
* transport peer ID. An Ed25519 libp2p PeerId embeds its public key (identity
|
|
276
|
+
* multihash), so each peer's ed25519 key is derivable from its `PeerId`; a
|
|
277
|
+
* peer is an authority iff that derived key is in the `AuthorityKey` set.
|
|
278
|
+
* This makes any authority node markable — not just the local one — and ties
|
|
279
|
+
* `isAuthority` to the control table rather than to `peerId === self`.
|
|
89
280
|
*/
|
|
90
281
|
private queryPeers;
|
|
91
282
|
/**
|
|
92
|
-
* Register the seed protocol handler.
|
|
283
|
+
* Register the seed protocol handler. The inbound closure just delegates to
|
|
284
|
+
* {@link handleSeedStream} — extracted as a method so it has a unit-test seam
|
|
285
|
+
* (mirroring wake's `handleStream`) the inline closure never had.
|
|
93
286
|
*/
|
|
94
287
|
private registerProtocolHandler;
|
|
288
|
+
/**
|
|
289
|
+
* Read one inbound seed frame, apply it, and write the ack.
|
|
290
|
+
*
|
|
291
|
+
* Hardened against a buggy/compromised own-cadre node: a concurrency cap (over
|
|
292
|
+
* {@link maxConcurrentSeeds}, reply without applying), a read timeout (a peer
|
|
293
|
+
* that never half-closes is aborted inside `readStreamToEnd`), and the existing
|
|
294
|
+
* malformed/oversized-frame guard — all reported as a non-accepting
|
|
295
|
+
* {@link SeedAckMessage} rather than a dropped/hung stream.
|
|
296
|
+
*/
|
|
297
|
+
private handleSeedStream;
|
|
95
298
|
/**
|
|
96
299
|
* Shutdown the service.
|
|
97
300
|
*/
|