@serfab/cadre-core 0.7.1 → 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 +770 -11
- package/dist/cadre-node.js +1900 -88
- 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 +197 -9
- package/dist/seed-bootstrap.js +396 -137
- 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 +406 -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 -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 +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 +464 -188
- 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 +1012 -582
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
|
-
import {
|
|
2
|
+
import { FormationListener, dialFormation, isValidResponderCreatesResult } from './strand-formation-protocol.js';
|
|
3
3
|
const log = debug('sereus:cadre:formation-manager');
|
|
4
4
|
/**
|
|
5
|
-
* StrandFormationManager
|
|
6
|
-
*
|
|
5
|
+
* StrandFormationManager drives the native cadre-core formation transport
|
|
6
|
+
* (`strand-formation-protocol.ts`) from cadre-core's strand-solicitation interfaces.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Responder side: a {@link FormationListener} wires the inbound protocol to
|
|
9
|
+
* {@link FormationUsageRecorder} (token), {@link DisclosureValidator} (identity),
|
|
10
|
+
* and {@link StrandProvisioner} (provisioning), disclosing this party's real
|
|
11
|
+
* identity + cadre only after validation.
|
|
12
|
+
*
|
|
13
|
+
* Initiator side: {@link formStrand} dials the responder carrying the real
|
|
14
|
+
* disclosure/token/cadre, then validates the responder's result via the
|
|
15
|
+
* {@link FormationResponseValidator} (or a built-in structural check).
|
|
12
16
|
*/
|
|
13
17
|
export class StrandFormationManager {
|
|
14
18
|
constructor(options) {
|
|
15
19
|
this.registeredNodes = new Set();
|
|
20
|
+
this.dialerSessions = 0;
|
|
16
21
|
this.disclosureValidator = options.disclosureValidator;
|
|
17
22
|
this.formationUsageRecorder = options.formationUsageRecorder;
|
|
18
23
|
this.strandProvisioner = options.strandProvisioner;
|
|
24
|
+
this.formationResponseValidator = options.formationResponseValidator;
|
|
19
25
|
this.partyId = options.partyId;
|
|
20
26
|
this.cadrePeerAddrs = options.cadrePeerAddrs ?? [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
this.sessionManager = createBootstrapManager(hooks, config);
|
|
27
|
+
this.config = options.config ?? {};
|
|
28
|
+
this.listener = new FormationListener({
|
|
29
|
+
validateToken: (token) => this.validateToken(token),
|
|
30
|
+
validateDisclosure: (token, disclosure) => this.validateDisclosure(token, disclosure),
|
|
31
|
+
provisionStrand: (token, initiatorPartyId, disclosure) => this.provisionAsResponder(token, initiatorPartyId, disclosure),
|
|
32
|
+
getResponderIdentity: () => ({ partyId: this.partyId, cadrePeerAddrs: this.cadrePeerAddrs }),
|
|
33
|
+
sessionTimeoutMs: this.config.sessionTimeoutMs,
|
|
34
|
+
stepTimeoutMs: this.config.stepTimeoutMs,
|
|
35
|
+
maxConcurrentSessions: this.config.maxConcurrentSessions
|
|
36
|
+
});
|
|
32
37
|
log('StrandFormationManager created for party: %s', this.partyId);
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
@@ -40,7 +45,7 @@ export class StrandFormationManager {
|
|
|
40
45
|
log('Node already registered');
|
|
41
46
|
return;
|
|
42
47
|
}
|
|
43
|
-
this.
|
|
48
|
+
this.listener.register(node, protocolId ?? this.config.protocolId);
|
|
44
49
|
this.registeredNodes.add(node);
|
|
45
50
|
log('Registered as responder on node');
|
|
46
51
|
}
|
|
@@ -51,112 +56,199 @@ export class StrandFormationManager {
|
|
|
51
56
|
if (!this.registeredNodes.has(node)) {
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
54
|
-
this.
|
|
59
|
+
this.listener.unregister(node, protocolId ?? this.config.protocolId);
|
|
55
60
|
this.registeredNodes.delete(node);
|
|
56
61
|
log('Unregistered from node');
|
|
57
62
|
}
|
|
58
63
|
/**
|
|
59
|
-
* Form a strand with a responder via an open invitation.
|
|
64
|
+
* Form a strand with a responder via an open invitation (initiator side).
|
|
60
65
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* 3. Returns the result as a FormStrandResult
|
|
66
|
+
* Builds a contact message carrying the real token + disclosure + this party's
|
|
67
|
+
* real cadre addresses, dials the responder over the native protocol, and
|
|
68
|
+
* validates the responder's result before returning.
|
|
65
69
|
*/
|
|
66
70
|
async formStrand(invitation, disclosure, node) {
|
|
67
71
|
log('Forming strand with invitation token: %s', invitation.token);
|
|
68
|
-
|
|
69
|
-
const link = {
|
|
70
|
-
responderPeerAddrs: invitation.bootstrap,
|
|
72
|
+
const contact = {
|
|
71
73
|
token: invitation.token,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// The disclosure is passed via the identityBundle in the contact message
|
|
76
|
-
// We store it temporarily so the hooks can access it
|
|
77
|
-
// Note: strand-proto's DialerSession constructs the contact message internally
|
|
78
|
-
// We need to extend this to pass the disclosure - for now we use partyId
|
|
79
|
-
const result = await this.sessionManager.initiateBootstrap(link, node);
|
|
80
|
-
log('Strand formed: %s', result.strand.strandId);
|
|
81
|
-
return {
|
|
82
|
-
memberKey: this.partyId, // The initiator's member key
|
|
83
|
-
invitePrivateKey: '', // Would come from key generation
|
|
84
|
-
strandId: result.strand.strandId
|
|
74
|
+
partyId: disclosure.partyId ?? this.partyId,
|
|
75
|
+
disclosure,
|
|
76
|
+
cadrePeerAddrs: this.cadrePeerAddrs
|
|
85
77
|
};
|
|
78
|
+
this.dialerSessions++;
|
|
79
|
+
try {
|
|
80
|
+
const provision = await dialFormation(node, {
|
|
81
|
+
contact,
|
|
82
|
+
responderAddrs: invitation.bootstrap,
|
|
83
|
+
validateResponse: (response) => this.validateResponse(invitation, disclosure, response),
|
|
84
|
+
sessionTimeoutMs: this.config.sessionTimeoutMs,
|
|
85
|
+
stepTimeoutMs: this.config.stepTimeoutMs,
|
|
86
|
+
protocolId: this.config.protocolId
|
|
87
|
+
});
|
|
88
|
+
log('Strand formed: %s', provision.strand.strandId);
|
|
89
|
+
return {
|
|
90
|
+
memberKey: contact.partyId,
|
|
91
|
+
invitePrivateKey: '',
|
|
92
|
+
strandId: provision.strand.strandId,
|
|
93
|
+
// The host strand's membership key, delivered through the protocol (provision-then-record).
|
|
94
|
+
// Undefined for an open strand. Kept separate from invitePrivateKey (the initiator's
|
|
95
|
+
// generated signing key), which is set by the StrandSolicitationService layer.
|
|
96
|
+
memberPrivateKey: provision.memberPrivateKey
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
this.dialerSessions--;
|
|
101
|
+
}
|
|
86
102
|
}
|
|
87
103
|
/**
|
|
88
104
|
* Get the number of active sessions
|
|
89
105
|
*/
|
|
90
106
|
getActiveSessionCounts() {
|
|
91
|
-
return this.
|
|
107
|
+
return { listeners: this.listener.activeCount, dialers: this.dialerSessions };
|
|
108
|
+
}
|
|
109
|
+
// ── Responder-side hooks ─────────────────────────────────────────────────────
|
|
110
|
+
async validateToken(token) {
|
|
111
|
+
if (!this.formationUsageRecorder) {
|
|
112
|
+
// No recorder configured — accept all tokens.
|
|
113
|
+
return { valid: true };
|
|
114
|
+
}
|
|
115
|
+
const tokenCheck = await this.formationUsageRecorder.isTokenValid(token);
|
|
116
|
+
if (!tokenCheck.valid) {
|
|
117
|
+
log('Token invalid: %s', token);
|
|
118
|
+
return { valid: false };
|
|
119
|
+
}
|
|
120
|
+
if (await this.formationUsageRecorder.isTokenUsed(token)) {
|
|
121
|
+
log('Token already used: %s', token);
|
|
122
|
+
return { valid: false };
|
|
123
|
+
}
|
|
124
|
+
return { valid: true };
|
|
125
|
+
}
|
|
126
|
+
async validateDisclosure(token, disclosure) {
|
|
127
|
+
if (!this.disclosureValidator) {
|
|
128
|
+
// No validator configured — accept all disclosures.
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return this.disclosureValidator.validateDisclosure(token, disclosure);
|
|
92
132
|
}
|
|
93
133
|
/**
|
|
94
|
-
*
|
|
134
|
+
* Responder-side provisioning. Routes on how the invite resolves
|
|
135
|
+
* ({@link ResolvedHostStrand}), and can REJECT post-validation by returning a
|
|
136
|
+
* {@link ResponderProvisionOutcome} with `approved: false` — `runSession` turns that
|
|
137
|
+
* into a clean, non-disclosing reply instead of a dropped result frame.
|
|
138
|
+
*
|
|
139
|
+
* - **bound** (host strand present): provision-then-record — write the single
|
|
140
|
+
* `FormationUsage` consent row against the pre-existing strand (record-only) and
|
|
141
|
+
* return it + its membership key (a read-gating secret disclosed only here, behind the
|
|
142
|
+
* token + disclosure validation `runSession` already enforced).
|
|
143
|
+
* - **missing** (invite names a host strand absent on this responder, e.g. unconverged):
|
|
144
|
+
* reject cleanly + retryably, writing NO usage row — recording usage here would fail the
|
|
145
|
+
* deferred `StrandExists` CHECK at commit and drop the frame.
|
|
146
|
+
* - **unbound** (no binding): the responder-provisions fallback — see {@link provisionUnbound}.
|
|
147
|
+
*
|
|
148
|
+
* Defense-in-depth: known provisioning/redeem failures (notably the concurrent
|
|
149
|
+
* `(Token, UseNumber)` PK collision when two redemptions of one unbound single-use invite
|
|
150
|
+
* race) are caught and mapped to a logged, retry-suggesting rejection rather than a thrown
|
|
151
|
+
* insert + a silently-closed stream. The LOG-before-reject keeps this a deliberate
|
|
152
|
+
* internal-error→protocol-rejection conversion (AGENTS.md: don't eat exceptions silently),
|
|
153
|
+
* not control-flow-by-exception.
|
|
95
154
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const isUsed = await this.formationUsageRecorder.isTokenUsed(token);
|
|
110
|
-
if (isUsed) {
|
|
111
|
-
log('Token already used: %s', token);
|
|
112
|
-
return { mode: 'responderCreates', valid: false };
|
|
113
|
-
}
|
|
114
|
-
return { mode: 'responderCreates', valid: true };
|
|
115
|
-
},
|
|
116
|
-
validateIdentity: async (identity, sessionId) => {
|
|
117
|
-
log('validateIdentity (session: %s)', sessionId);
|
|
118
|
-
if (!this.disclosureValidator) {
|
|
119
|
-
// No validator configured - accept all identities
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
// Extract disclosure from identity bundle
|
|
123
|
-
const disclosure = identity;
|
|
124
|
-
// We need the token here - it's passed via the contact message
|
|
125
|
-
// For now, use a placeholder - this will be refined
|
|
126
|
-
const token = identity?.token ?? '';
|
|
127
|
-
return this.disclosureValidator.validateDisclosure(token, disclosure);
|
|
128
|
-
},
|
|
129
|
-
provisionStrand: async (creator, creatorPartyId, otherPartyId, sessionId) => {
|
|
130
|
-
log('provisionStrand: creator=%s, other=%s (session: %s)', creatorPartyId, otherPartyId, sessionId);
|
|
131
|
-
if (!this.strandProvisioner) {
|
|
132
|
-
// No provisioner - return a placeholder
|
|
133
|
-
const strandId = `strand-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
134
|
-
return {
|
|
135
|
-
strand: { strandId, createdBy: creator },
|
|
155
|
+
async provisionAsResponder(token, initiatorPartyId, _disclosure) {
|
|
156
|
+
const recorder = this.formationUsageRecorder;
|
|
157
|
+
const resolved = recorder?.resolveStrand
|
|
158
|
+
? await recorder.resolveStrand(token)
|
|
159
|
+
: { kind: 'unbound' };
|
|
160
|
+
try {
|
|
161
|
+
switch (resolved.kind) {
|
|
162
|
+
case 'bound': {
|
|
163
|
+
// recorder is guaranteed non-null here: only resolveStrand can yield 'bound'.
|
|
164
|
+
await recorder.recordUsage(token, initiatorPartyId, resolved.strandId);
|
|
165
|
+
return this.approve({
|
|
166
|
+
strand: { strandId: resolved.strandId, createdBy: 'responder' },
|
|
167
|
+
memberPrivateKey: resolved.memberPrivateKey ?? undefined,
|
|
136
168
|
dbConnectionInfo: { endpoint: 'local', credentialsRef: '' }
|
|
137
|
-
};
|
|
169
|
+
});
|
|
138
170
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
strand: { strandId: result.strandId, createdBy: creator },
|
|
146
|
-
dbConnectionInfo: { endpoint: 'local', credentialsRef: '' }
|
|
147
|
-
};
|
|
148
|
-
},
|
|
149
|
-
validateResponse: async (response, sessionId) => {
|
|
150
|
-
log('validateResponse (session: %s)', sessionId);
|
|
151
|
-
// Accept all responses for now
|
|
152
|
-
return true;
|
|
153
|
-
},
|
|
154
|
-
validateDatabaseResult: async (result, sessionId) => {
|
|
155
|
-
log('validateDatabaseResult (session: %s)', sessionId);
|
|
156
|
-
// Accept all database results for now
|
|
157
|
-
return true;
|
|
171
|
+
case 'missing': {
|
|
172
|
+
log('Host strand %s not yet available on this responder; rejecting token %s', resolved.strandId, token);
|
|
173
|
+
return { approved: false, reason: 'Host strand not yet available on this responder' };
|
|
174
|
+
}
|
|
175
|
+
case 'unbound':
|
|
176
|
+
return await this.provisionUnbound(token, initiatorPartyId);
|
|
158
177
|
}
|
|
159
|
-
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
log('provisionAsResponder failed for token %s: %o', token, err);
|
|
181
|
+
return { approved: false, reason: 'Formation conflict, retry' };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Responder-provisions fallback (invite binds no host strand). Precedence:
|
|
186
|
+
*
|
|
187
|
+
* 1. `recorder.provisionAndRecord` present (a real DB recorder) → atomic create-strand +
|
|
188
|
+
* record-consent, so the unbound redemption is single-use just like the bound path.
|
|
189
|
+
* Returns the new strand (+ key, null for an open responder-provisioned strand).
|
|
190
|
+
* 2. else `strandProvisioner` present → the legacy/mock contract: provision a bare strand,
|
|
191
|
+
* NO inline usage write (those callers record usage explicitly via
|
|
192
|
+
* `recordFormationComplete`, or assert transport invariants only). Threads the invite's
|
|
193
|
+
* REAL `sAppId`.
|
|
194
|
+
* 3. else → a structural placeholder (no recorder + no provisioner ⇒ no single-use semantics
|
|
195
|
+
* exist to enforce).
|
|
196
|
+
*
|
|
197
|
+
* Plan tradeoff — option (a) "record usage on the fallback" over (b) "remove the fallback":
|
|
198
|
+
* (a) closes the single-use hole with a targeted atomic create+record and leaves the
|
|
199
|
+
* `StrandProvisioner` mock-transport tests untouched; (b) would delete the provisioner
|
|
200
|
+
* surface and churn ~6 unit + ~6 integration sites for ZERO production benefit (production
|
|
201
|
+
* — `cadre-web.ts` / `cadre-phone.ts` — always publishes strand-BOUND invites and treats
|
|
202
|
+
* the responder-provisions placeholder as failure). The broader "should this fallback exist
|
|
203
|
+
* at all?" question lives in backlog `formation-initiatorcreates-cover-or-remove`.
|
|
204
|
+
*/
|
|
205
|
+
async provisionUnbound(token, initiatorPartyId) {
|
|
206
|
+
const recorder = this.formationUsageRecorder;
|
|
207
|
+
if (recorder?.provisionAndRecord) {
|
|
208
|
+
const sAppId = await this.resolveInviteSAppId(token);
|
|
209
|
+
const provisioned = await recorder.provisionAndRecord(token, initiatorPartyId, sAppId);
|
|
210
|
+
return this.approve({
|
|
211
|
+
strand: { strandId: provisioned.strandId, createdBy: 'responder' },
|
|
212
|
+
memberPrivateKey: provisioned.memberPrivateKey ?? undefined,
|
|
213
|
+
dbConnectionInfo: { endpoint: 'local', credentialsRef: '' }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (this.strandProvisioner) {
|
|
217
|
+
const sAppId = await this.resolveInviteSAppId(token);
|
|
218
|
+
const result = await this.strandProvisioner.provisionStrand(sAppId, initiatorPartyId, this.partyId);
|
|
219
|
+
return this.approve({
|
|
220
|
+
strand: { strandId: result.strandId, createdBy: 'responder' },
|
|
221
|
+
dbConnectionInfo: { endpoint: 'local', credentialsRef: '' }
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// No recorder + no provisioner — a structural placeholder the initiator can still validate.
|
|
225
|
+
const strandId = `strand-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
226
|
+
return this.approve({
|
|
227
|
+
strand: { strandId, createdBy: 'responder' },
|
|
228
|
+
dbConnectionInfo: { endpoint: 'local', credentialsRef: '' }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/** Wrap a provision result as an approving {@link ResponderProvisionOutcome}. */
|
|
232
|
+
approve(result) {
|
|
233
|
+
return { approved: true, result };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The invite's authoritative `sAppId` for the responder-provisions fallback, read back
|
|
237
|
+
* from the recorder's `isTokenValid` invitation (the real `FormationInvite.sAppId`).
|
|
238
|
+
* Empty string when no recorder is wired or the invitation omits it.
|
|
239
|
+
*/
|
|
240
|
+
async resolveInviteSAppId(token) {
|
|
241
|
+
if (!this.formationUsageRecorder)
|
|
242
|
+
return '';
|
|
243
|
+
const check = await this.formationUsageRecorder.isTokenValid(token);
|
|
244
|
+
return check.invitation?.sAppId ?? '';
|
|
245
|
+
}
|
|
246
|
+
// ── Initiator-side result validation ─────────────────────────────────────────
|
|
247
|
+
async validateResponse(invitation, disclosure, response) {
|
|
248
|
+
if (this.formationResponseValidator) {
|
|
249
|
+
return this.formationResponseValidator.validateResponse({ invitation, disclosure, response });
|
|
250
|
+
}
|
|
251
|
+
return isValidResponderCreatesResult(response);
|
|
160
252
|
}
|
|
161
253
|
}
|
|
162
254
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strand-formation-manager.js","sourceRoot":"","sources":["../src/strand-formation-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"strand-formation-manager.js","sourceRoot":"","sources":["../src/strand-formation-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native cadre-core strand-formation transport.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the non-deprecated `seed-bootstrap.ts` protocol service: a dedicated
|
|
5
|
+
* libp2p protocol id, length-prefixed JSON frames, and small single-purpose
|
|
6
|
+
* session helpers. It replaces the deprecated `@serfab/strand-proto` transport,
|
|
7
|
+
* carrying the caller's REAL invitation token + disclosure and BOTH parties'
|
|
8
|
+
* real cadre peer addresses end-to-end (no `{ partyId: sessionId }` / `cadre-*.local`
|
|
9
|
+
* placeholders), and validating the responder's result on the initiator side.
|
|
10
|
+
*
|
|
11
|
+
* Provisioning flow (`responderCreates`, 2 messages): the responder provisions (or, for
|
|
12
|
+
* provision-then-record, resolves + records consent against) the strand and returns the
|
|
13
|
+
* result on approval.
|
|
14
|
+
*
|
|
15
|
+
* Cadre-disclosure timing (see docs/strand-proto.md "Security & Privacy"): the
|
|
16
|
+
* responder reveals its own party id + cadre addresses — and, for a closed strand
|
|
17
|
+
* returned via provision-then-record, that strand's membership key — ONLY after the
|
|
18
|
+
* token and disclosure validate; a rejection discloses none of them.
|
|
19
|
+
*/
|
|
20
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
21
|
+
import type { StrandFormationDisclosure } from './types.js';
|
|
22
|
+
/** Protocol id for the native formation transport (parallel to `/sereus/seed/1.0.0`). */
|
|
23
|
+
export declare const FORMATION_PROTOCOL = "/sereus/formation/1.0.0";
|
|
24
|
+
export type FormationParty = 'initiator' | 'responder';
|
|
25
|
+
export interface FormationStrandInfo {
|
|
26
|
+
strandId: string;
|
|
27
|
+
createdBy: FormationParty;
|
|
28
|
+
}
|
|
29
|
+
export interface FormationDbConnectionInfo {
|
|
30
|
+
endpoint: string;
|
|
31
|
+
credentialsRef: string;
|
|
32
|
+
}
|
|
33
|
+
export interface FormationProvisionResult {
|
|
34
|
+
strand: FormationStrandInfo;
|
|
35
|
+
/**
|
|
36
|
+
* The strand's membership key (closed-strand read-gating secret), delivered to a
|
|
37
|
+
* validated invitee for provision-then-record formation. Disclosed only AFTER token
|
|
38
|
+
* + disclosure validation, exactly like the responder identity/cadre — a rejected or
|
|
39
|
+
* already-used token discloses neither identity nor key. Absent for open strands.
|
|
40
|
+
*/
|
|
41
|
+
memberPrivateKey?: string;
|
|
42
|
+
dbConnectionInfo: FormationDbConnectionInfo;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Outcome of the responder's provisioning hook.
|
|
46
|
+
*
|
|
47
|
+
* Distinct from a bare {@link FormationProvisionResult} so the hook can REJECT a
|
|
48
|
+
* formation AFTER token + disclosure validation — e.g. a bound invite naming a host
|
|
49
|
+
* strand this responder has not yet converged on, or a concurrent-redemption collision.
|
|
50
|
+
* Without this channel such cases threw inside provisioning, the stream closed with no
|
|
51
|
+
* result frame, and the initiator saw a read-error/timeout instead of a clean
|
|
52
|
+
* `approved: false`. A rejection still discloses NO responder identity/cadre.
|
|
53
|
+
*/
|
|
54
|
+
export type ResponderProvisionOutcome = {
|
|
55
|
+
approved: true;
|
|
56
|
+
result: FormationProvisionResult;
|
|
57
|
+
} | {
|
|
58
|
+
approved: false;
|
|
59
|
+
reason: string;
|
|
60
|
+
};
|
|
61
|
+
/** Initiator → Responder: carries the real token + disclosure + initiator cadre. */
|
|
62
|
+
export interface FormationContactMessage {
|
|
63
|
+
/** The real invitation token. */
|
|
64
|
+
token: string;
|
|
65
|
+
/** Initiator's member key (peer id). */
|
|
66
|
+
partyId: string;
|
|
67
|
+
/** The real disclosure, carried verbatim. */
|
|
68
|
+
disclosure: StrandFormationDisclosure;
|
|
69
|
+
/** Initiator's real multiaddrs. */
|
|
70
|
+
cadrePeerAddrs: string[];
|
|
71
|
+
}
|
|
72
|
+
/** Responder → Initiator: responder identity/cadre disclosed only after validation. */
|
|
73
|
+
export interface FormationResultMessage {
|
|
74
|
+
approved: boolean;
|
|
75
|
+
/** Present iff `approved === false`. */
|
|
76
|
+
reason?: string;
|
|
77
|
+
/** Responder's real party id (disclosed only after validation). */
|
|
78
|
+
partyId?: string;
|
|
79
|
+
/** Responder's real multiaddrs (omitted on rejection). */
|
|
80
|
+
cadrePeerAddrs?: string[];
|
|
81
|
+
/** The provisioned strand/db result (always present on approval). */
|
|
82
|
+
provisionResult?: FormationProvisionResult;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Structural check on a `responderCreates` result: the responder must have approved,
|
|
86
|
+
* disclosed a non-empty real identity + cadre (not the deprecated `cadre-*.local`
|
|
87
|
+
* placeholders), and returned a strand vouched for by the responder party with a real id.
|
|
88
|
+
*
|
|
89
|
+
* `createdBy: 'responder'` means "the responder party vouches for / returns this strand."
|
|
90
|
+
* Under provision-then-record that strand was minted authority-signed earlier (not created
|
|
91
|
+
* in-session), but it is still the responder party returning its own strand, so the marker
|
|
92
|
+
* — and this structural validator — stay unchanged.
|
|
93
|
+
*
|
|
94
|
+
* The behavioral floor for the initiator: a responder that returns an arbitrary/empty
|
|
95
|
+
* `strandId` or omits its disclosed identity is rejected, not silently accepted.
|
|
96
|
+
*/
|
|
97
|
+
export declare function isValidResponderCreatesResult(response: FormationResultMessage): boolean;
|
|
98
|
+
export interface FormationListenerOptions {
|
|
99
|
+
/** Validate the invitation token; returns whether it is valid. */
|
|
100
|
+
validateToken(token: string): Promise<{
|
|
101
|
+
valid: boolean;
|
|
102
|
+
}>;
|
|
103
|
+
/** Validate the initiator's disclosure with the REAL token + disclosure. */
|
|
104
|
+
validateDisclosure(token: string, disclosure: StrandFormationDisclosure): Promise<boolean>;
|
|
105
|
+
/**
|
|
106
|
+
* Provision (or, for provision-then-record, resolve + record consent against) the
|
|
107
|
+
* strand (`responderCreates`) for the given initiator. The REAL token is threaded
|
|
108
|
+
* in so the hook can map it to the bound host strand and its membership key.
|
|
109
|
+
*
|
|
110
|
+
* Returns a {@link ResponderProvisionOutcome}: the hook may REJECT post-validation
|
|
111
|
+
* (e.g. an unconverged host strand) and the listener turns that into a clean,
|
|
112
|
+
* non-disclosing `approved: false` reply rather than dropping the result frame.
|
|
113
|
+
*/
|
|
114
|
+
provisionStrand(token: string, initiatorPartyId: string, disclosure: StrandFormationDisclosure): Promise<ResponderProvisionOutcome>;
|
|
115
|
+
/** Responder identity, disclosed only AFTER token + disclosure validation passes. */
|
|
116
|
+
getResponderIdentity(): {
|
|
117
|
+
partyId: string;
|
|
118
|
+
cadrePeerAddrs: string[];
|
|
119
|
+
};
|
|
120
|
+
sessionTimeoutMs?: number;
|
|
121
|
+
stepTimeoutMs?: number;
|
|
122
|
+
maxConcurrentSessions?: number;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Responder side of the native formation protocol. Registers a libp2p handler and
|
|
126
|
+
* drives each inbound stream through token + disclosure validation, provisioning,
|
|
127
|
+
* and result delivery — enforcing the cadre-disclosure timing rule (responder cadre
|
|
128
|
+
* is revealed only after validation; rejections disclose nothing).
|
|
129
|
+
*/
|
|
130
|
+
export declare class FormationListener {
|
|
131
|
+
private readonly options;
|
|
132
|
+
private readonly sessionTimeoutMs;
|
|
133
|
+
private readonly stepTimeoutMs;
|
|
134
|
+
private readonly maxConcurrentSessions;
|
|
135
|
+
private readonly registered;
|
|
136
|
+
private activeSessions;
|
|
137
|
+
private sessionCounter;
|
|
138
|
+
constructor(options: FormationListenerOptions);
|
|
139
|
+
/** Number of in-flight inbound sessions. */
|
|
140
|
+
get activeCount(): number;
|
|
141
|
+
register(node: Libp2p, protocolId?: string): void;
|
|
142
|
+
unregister(node: Libp2p, protocolId?: string): void;
|
|
143
|
+
private handleStream;
|
|
144
|
+
private runSession;
|
|
145
|
+
}
|
|
146
|
+
export interface FormationDialOptions {
|
|
147
|
+
/** The contact message to send (real token, partyId, disclosure, initiator cadre). */
|
|
148
|
+
contact: FormationContactMessage;
|
|
149
|
+
/** Responder multiaddrs to dial. */
|
|
150
|
+
responderAddrs: string[];
|
|
151
|
+
/** Validate the responder's result; a false return aborts the formation. */
|
|
152
|
+
validateResponse(response: FormationResultMessage): Promise<boolean>;
|
|
153
|
+
sessionTimeoutMs?: number;
|
|
154
|
+
stepTimeoutMs?: number;
|
|
155
|
+
protocolId?: string;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Initiator side of the native formation protocol. Dials the responder, sends the
|
|
159
|
+
* contact (carrying the real disclosure/token/cadre), validates the responder's
|
|
160
|
+
* result, and returns the strand the responder provisioned.
|
|
161
|
+
*/
|
|
162
|
+
export declare function dialFormation(node: Libp2p, options: FormationDialOptions): Promise<FormationProvisionResult>;
|