@serfab/cadre-core 1.8.0 → 1.10.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/dist/cadre-node.d.ts +301 -121
- package/dist/cadre-node.js +588 -244
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +1 -1
- package/dist/control-cohort.js +1 -1
- package/dist/control-database.d.ts +105 -14
- package/dist/control-database.js +242 -73
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +6 -6
- package/dist/control-formation-recorder.js +7 -8
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +90 -3
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +21 -15
- package/dist/control-write-retry.js +21 -15
- package/dist/control-write-retry.js.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.js +16 -13
- package/dist/index.js.map +1 -1
- package/dist/invitation-bootstrap.d.ts +32 -0
- package/dist/invitation-bootstrap.js +30 -0
- package/dist/invitation-bootstrap.js.map +1 -0
- package/dist/link-budget.d.ts +204 -119
- package/dist/link-budget.js +208 -120
- package/dist/link-budget.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +3 -3
- package/dist/membership-connection-gater.js +3 -3
- package/dist/node-local-snapshot.d.ts +6 -4
- package/dist/node-local-snapshot.js +7 -3
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +18 -14
- package/dist/peer-dial.js +18 -14
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +3 -12
- package/dist/peer-join-backfill.js +2 -3
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +186 -0
- package/dist/pending-join-runner.js +649 -0
- package/dist/pending-join-runner.js.map +1 -0
- package/dist/push-fanout.d.ts +1 -1
- package/dist/push-fanout.js +1 -1
- package/dist/seed-bootstrap.d.ts +2 -2
- package/dist/seed-bootstrap.js +1 -1
- package/dist/strand-addr-protocol.js +2 -2
- package/dist/strand-formation-deadlines.d.ts +5 -5
- package/dist/strand-formation-deadlines.js +5 -5
- package/dist/strand-formation-manager.d.ts +18 -4
- package/dist/strand-formation-manager.js +48 -33
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +71 -25
- package/dist/strand-formation-protocol.js +225 -51
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-formation-rejection.d.ts +94 -0
- package/dist/strand-formation-rejection.js +77 -0
- package/dist/strand-formation-rejection.js.map +1 -0
- package/dist/strand-fret-addrs.d.ts +54 -0
- package/dist/strand-fret-addrs.js +108 -0
- package/dist/strand-fret-addrs.js.map +1 -0
- package/dist/strand-instance-manager.d.ts +9 -38
- package/dist/strand-instance-manager.js +17 -74
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-network-state-file.d.ts +12 -0
- package/dist/strand-network-state-file.js +38 -0
- package/dist/strand-network-state-file.js.map +1 -0
- package/dist/strand-network-state.d.ts +111 -0
- package/dist/strand-network-state.js +191 -0
- package/dist/strand-network-state.js.map +1 -0
- package/dist/strand-solicitation.d.ts +11 -3
- package/dist/strand-solicitation.js +9 -0
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/types.d.ts +123 -40
- package/dist/types.js +5 -9
- package/dist/types.js.map +1 -1
- package/package.json +25 -24
- package/src/cadre-node.ts +637 -251
- package/src/control-authorization.ts +117 -116
- package/src/control-cohort.ts +1 -1
- package/src/control-database.ts +303 -82
- package/src/control-formation-recorder.ts +8 -9
- package/src/control-schema.ts +90 -3
- package/src/control-write-retry.ts +21 -15
- package/src/index.ts +36 -56
- package/src/invitation-bootstrap.ts +40 -0
- package/src/link-budget.ts +221 -120
- package/src/membership-connection-gater.ts +3 -3
- package/src/node-local-snapshot.ts +12 -7
- package/src/peer-dial.ts +18 -14
- package/src/peer-join-backfill.ts +5 -6
- package/src/pending-join-runner.ts +735 -0
- package/src/push-fanout.ts +1 -1
- package/src/seed-bootstrap.ts +2 -2
- package/src/strand-addr-protocol.ts +2 -2
- package/src/strand-formation-deadlines.ts +5 -5
- package/src/strand-formation-manager.ts +62 -37
- package/src/strand-formation-protocol.ts +1036 -803
- package/src/strand-formation-rejection.ts +129 -0
- package/src/strand-fret-addrs.ts +145 -0
- package/src/strand-instance-manager.ts +24 -97
- package/src/strand-network-state-file.ts +40 -0
- package/src/strand-network-state.ts +243 -0
- package/src/strand-solicitation.ts +17 -4
- package/src/types.ts +119 -40
- package/dist/strand-peer-book-file.d.ts +0 -12
- package/dist/strand-peer-book-file.js +0 -37
- package/dist/strand-peer-book-file.js.map +0 -1
- package/dist/strand-peer-book-protocol.d.ts +0 -168
- package/dist/strand-peer-book-protocol.js +0 -356
- package/dist/strand-peer-book-protocol.js.map +0 -1
- package/dist/strand-peer-book-swap.d.ts +0 -104
- package/dist/strand-peer-book-swap.js +0 -284
- package/dist/strand-peer-book-swap.js.map +0 -1
- package/dist/strand-peer-book.d.ts +0 -159
- package/dist/strand-peer-book.js +0 -361
- package/dist/strand-peer-book.js.map +0 -1
- package/dist/strand-peer-observer.d.ts +0 -88
- package/dist/strand-peer-observer.js +0 -180
- package/dist/strand-peer-observer.js.map +0 -1
- package/src/strand-peer-book-file.ts +0 -39
- package/src/strand-peer-book-protocol.ts +0 -466
- package/src/strand-peer-book-swap.ts +0 -338
- package/src/strand-peer-book.ts +0 -475
- package/src/strand-peer-observer.ts +0 -235
package/src/push-fanout.ts
CHANGED
|
@@ -200,7 +200,7 @@ export class PushFanoutService {
|
|
|
200
200
|
* through to FCM/APNs.
|
|
201
201
|
*
|
|
202
202
|
* NOTE: the platform push waits for the whole direct dial to fail, up to
|
|
203
|
-
* `DEFAULT_WAKE_DIAL_BUDGET_MS` (
|
|
203
|
+
* `DEFAULT_WAKE_DIAL_BUDGET_MS` (57 s at the default declared link). A suspended
|
|
204
204
|
* phone typically fails its relay address fast and spends the rest on its direct
|
|
205
205
|
* one. If that delay shows up, start the platform push in parallel with the dial,
|
|
206
206
|
* or after its first attempt.
|
package/src/seed-bootstrap.ts
CHANGED
|
@@ -209,7 +209,7 @@ export interface SeedBootstrapConfig {
|
|
|
209
209
|
* not-yet-trusted node during onboarding, so this is the more exposed
|
|
210
210
|
* direction than the receiver knobs above.
|
|
211
211
|
*
|
|
212
|
-
* Defaults to `relayedRequestBudgetMs(linkRoundTripMs)` (`link-budget.ts`;
|
|
212
|
+
* Defaults to `relayedRequestBudgetMs(linkRoundTripMs)` (`link-budget.ts`; 28.5 s at the
|
|
213
213
|
* default declaration): one dial that may need a relay, then one request and its answer.
|
|
214
214
|
* Delivery does not set `runOnLimitedConnection`, so it does not use a limited relayed
|
|
215
215
|
* connection today; the relayed-dial count is the upper bound on the dial it can use, the
|
|
@@ -1138,7 +1138,7 @@ export class SeedBootstrapService {
|
|
|
1138
1138
|
* stream, then dial the seed's owners.
|
|
1139
1139
|
*
|
|
1140
1140
|
* The ack and the close come BEFORE the owner dials: an unreachable owner can hold
|
|
1141
|
-
* a dial for `dialBudget.totalMs` (
|
|
1141
|
+
* a dial for `dialBudget.totalMs` (86 s at the default declared link), which is not
|
|
1142
1142
|
* link time and does not belong inside the sender's delivery deadline. The close is
|
|
1143
1143
|
* what releases the sender, which reads the ack to end-of-stream. The stream stays
|
|
1144
1144
|
* counted in {@link activeStreams} through the dials, so {@link maxConcurrentSeeds}
|
|
@@ -387,7 +387,7 @@ export async function collectStrandAddrs(
|
|
|
387
387
|
* per-target lines scattered through a concurrent fan-out do not reassemble into
|
|
388
388
|
* "this sibling was unreachable".
|
|
389
389
|
*
|
|
390
|
-
* NOTE: cost is (targets × `timeoutMs`,
|
|
390
|
+
* NOTE: cost is (targets × `timeoutMs`, 28.5 s each at the default declared link) with
|
|
391
391
|
* no whole-sibling budget, the same shape `dialWake` bounds with
|
|
392
392
|
* `DEFAULT_WAKE_DIAL_BUDGET_MS`. Siblings are asked concurrently, so a collection costs
|
|
393
393
|
* its slowest sibling, not the sum. Fine today — `dialTargets` yields the peerId plus
|
|
@@ -434,7 +434,7 @@ async function dialOneSibling(
|
|
|
434
434
|
|
|
435
435
|
/**
|
|
436
436
|
* Deadline for ONE attempt to ask a sibling — dial, request, response — unless the caller set
|
|
437
|
-
* its own: `relayedRequestBudgetMs` at the declared link (
|
|
437
|
+
* its own: `relayedRequestBudgetMs` at the declared link (28.5 s at the default).
|
|
438
438
|
*
|
|
439
439
|
* An attempt is either a dial by peer id, which normally reuses an open control connection
|
|
440
440
|
* (`CIRCUIT_REQUEST_ROUND_TRIPS`, 2), or the fallback to the sibling's control addresses, a
|
|
@@ -62,14 +62,14 @@ export interface FormationDeadlines {
|
|
|
62
62
|
* cohort read at that link (`cohortReadDeadlineMs`, 2L: one request and its answer over an
|
|
63
63
|
* open circuit) and `K` one commit (`commitBudgetMs`, 20L, measured):
|
|
64
64
|
*
|
|
65
|
-
* dialMs = a relayed dial plus one protocol negotiation (
|
|
65
|
+
* dialMs = a relayed dial plus one protocol negotiation (6L + 4 000)
|
|
66
66
|
* awaitContactMs = one request over the open stream (2L)
|
|
67
67
|
* validationMs = 2R (4L)
|
|
68
68
|
* provisionWorkMs = approval hook (10 000, flat) + 2R + K (10 000 + 24L)
|
|
69
69
|
* provisionGraceMs = R + K (22L)
|
|
70
70
|
* responseTravelMarginMs = L + validationMs (5L)
|
|
71
71
|
* initiatorAwaitResponseMs = work + grace + margin (10 000 + 51L)
|
|
72
|
-
* sessionMs = dialMs + initiatorAwaitResponseMs (
|
|
72
|
+
* sessionMs = dialMs + initiatorAwaitResponseMs (14 000 + 57L)
|
|
73
73
|
*
|
|
74
74
|
* Why each term:
|
|
75
75
|
*
|
|
@@ -120,10 +120,10 @@ export interface FormationDeadlines {
|
|
|
120
120
|
* **What this costs.** At the default declaration (3 500 ms) a joiner whose responder
|
|
121
121
|
* accepts the contact and then hangs waits up to 188.5 s, about three minutes, before it is
|
|
122
122
|
* told, where the fixed ladder this replaced told it after 15 s; the whole session is
|
|
123
|
-
* bounded at
|
|
123
|
+
* bounded at 213.5 s. A responder that is unreachable still fails at the dial, 25 s. A host
|
|
124
124
|
* that knows its party is on a fast link lowers `linkRoundTripMs` and gets roughly the old
|
|
125
|
-
* numbers back (at L = 100: dial
|
|
126
|
-
*
|
|
125
|
+
* numbers back (at L = 100: dial 4.6 s, provisioning 14.6 s, await-response 15.1 s, session
|
|
126
|
+
* 19.7 s). Before this derivation, the fixed 5 s dial could not open a relayed connection
|
|
127
127
|
* above a 1.25-second round trip, and the fixed 2 s grace could not contain one commit at the
|
|
128
128
|
* supported link, so a joiner could be told 'timed out' over an invite that was in fact
|
|
129
129
|
* spent.
|
|
@@ -28,12 +28,14 @@ import type {
|
|
|
28
28
|
import {
|
|
29
29
|
FormationListener,
|
|
30
30
|
INVALID_TOKEN_REASON,
|
|
31
|
-
|
|
31
|
+
dialFormationByMachine,
|
|
32
32
|
isValidResponderCreatesResult,
|
|
33
33
|
isWellFormedMembershipInvite,
|
|
34
34
|
type FormationContactMessage,
|
|
35
35
|
type FormationProvisionResult,
|
|
36
|
+
type FormationRejection,
|
|
36
37
|
type FormationResultMessage,
|
|
38
|
+
type FormationTokenCheck,
|
|
37
39
|
type ResponderProvisionOutcome
|
|
38
40
|
} from './strand-formation-protocol.js';
|
|
39
41
|
import { formationDeadlines } from './strand-formation-deadlines.js';
|
|
@@ -84,17 +86,18 @@ export const MEMBERSHIP_INVITE_UNAVAILABLE_REASON = 'Strand membership invitatio
|
|
|
84
86
|
export const HOST_STRAND_MUST_BE_RECREATED_REASON = 'Host strand must be recreated';
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
|
-
*
|
|
89
|
+
* The rejection a would-be joiner is told for each approval-failure category
|
|
88
90
|
* (see {@link FormationApprovalFailure}). Distinct per category on purpose: mapping any
|
|
89
|
-
* of these onto the generic '
|
|
90
|
-
*
|
|
91
|
+
* of these onto the generic `'conflict'` would tell a joiner to retry what retrying can
|
|
92
|
+
* never fix (e.g. a non-enrolled validation key). The three inviter-setup faults share
|
|
93
|
+
* one code; the reason text still names which one for the operator.
|
|
91
94
|
*/
|
|
92
|
-
const
|
|
93
|
-
refused: 'Formation approval refused',
|
|
94
|
-
unavailable: 'Formation approval unavailable, retry',
|
|
95
|
-
malformed: 'Formation approval invalid',
|
|
96
|
-
unenrolled: 'Formation approval key is not enrolled',
|
|
97
|
-
misconfigured: 'Formation approval misconfigured'
|
|
95
|
+
const APPROVAL_REJECTIONS: Record<FormationApprovalFailure, FormationRejection> = {
|
|
96
|
+
refused: { approved: false, code: 'approval-refused', reason: 'Formation approval refused' },
|
|
97
|
+
unavailable: { approved: false, code: 'approval-unavailable', reason: 'Formation approval unavailable, retry' },
|
|
98
|
+
malformed: { approved: false, code: 'approval-invalid', reason: 'Formation approval invalid' },
|
|
99
|
+
unenrolled: { approved: false, code: 'approval-invalid', reason: 'Formation approval key is not enrolled' },
|
|
100
|
+
misconfigured: { approved: false, code: 'approval-invalid', reason: 'Formation approval misconfigured' }
|
|
98
101
|
};
|
|
99
102
|
|
|
100
103
|
/**
|
|
@@ -138,6 +141,13 @@ export interface StrandFormationManagerConfig {
|
|
|
138
141
|
protocolId?: string;
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
/**
|
|
145
|
+
* A node's own cadre addresses, sent on every formation. A function is read at each
|
|
146
|
+
* formation, for a node whose addresses change after the manager is built — a
|
|
147
|
+
* `CadreNode` installs its responder before a relay reservation gives it any.
|
|
148
|
+
*/
|
|
149
|
+
export type CadrePeerAddrsSource = string[] | (() => string[]);
|
|
150
|
+
|
|
141
151
|
/**
|
|
142
152
|
* Options for creating a StrandFormationManager
|
|
143
153
|
*/
|
|
@@ -153,7 +163,7 @@ export interface StrandFormationManagerOptions {
|
|
|
153
163
|
/** This party's ID for identification */
|
|
154
164
|
partyId: string;
|
|
155
165
|
/** This party's cadre peer addresses */
|
|
156
|
-
cadrePeerAddrs?:
|
|
166
|
+
cadrePeerAddrs?: CadrePeerAddrsSource;
|
|
157
167
|
/**
|
|
158
168
|
* This node's live STRAND-network multiaddrs for a strand it is running (responder
|
|
159
169
|
* side). Wired by `CadreNode` to its own per-strand address lookup; the manager has no
|
|
@@ -213,7 +223,7 @@ export class StrandFormationManager {
|
|
|
213
223
|
private readonly strandProvisioner?: StrandProvisioner;
|
|
214
224
|
private readonly formationResponseValidator?: FormationResponseValidator;
|
|
215
225
|
private readonly partyId: string;
|
|
216
|
-
private readonly cadrePeerAddrs:
|
|
226
|
+
private readonly cadrePeerAddrs: CadrePeerAddrsSource;
|
|
217
227
|
private readonly resolveStrandAddrs?: (strandId: string) => string[];
|
|
218
228
|
private readonly issueMembershipInvite?: MembershipInviteIssuer;
|
|
219
229
|
private readonly config: StrandFormationManagerConfig;
|
|
@@ -237,7 +247,7 @@ export class StrandFormationManager {
|
|
|
237
247
|
validateDisclosure: (token, disclosure) => this.validateDisclosure(token, disclosure),
|
|
238
248
|
provisionStrand: (contact, signal) =>
|
|
239
249
|
this.provisionAsResponder(contact, signal),
|
|
240
|
-
getResponderIdentity: () => ({ partyId: this.partyId, cadrePeerAddrs: this.
|
|
250
|
+
getResponderIdentity: () => ({ partyId: this.partyId, cadrePeerAddrs: this.currentCadrePeerAddrs() }),
|
|
241
251
|
// Forwarded only when wired, so `FormationListenerOptions.resolveStrandAddrs`
|
|
242
252
|
// stays genuinely absent (and the listener short-circuits) for an unwired manager.
|
|
243
253
|
...(this.resolveStrandAddrs && { resolveStrandAddrs: this.resolveStrandAddrs }),
|
|
@@ -251,6 +261,10 @@ export class StrandFormationManager {
|
|
|
251
261
|
log('StrandFormationManager created for party: %s', this.partyId);
|
|
252
262
|
}
|
|
253
263
|
|
|
264
|
+
private currentCadrePeerAddrs(): string[] {
|
|
265
|
+
return typeof this.cadrePeerAddrs === 'function' ? this.cadrePeerAddrs() : this.cadrePeerAddrs;
|
|
266
|
+
}
|
|
267
|
+
|
|
254
268
|
/**
|
|
255
269
|
* Register this manager as a protocol handler on a libp2p node.
|
|
256
270
|
* Call this on the control network node to handle incoming formation requests.
|
|
@@ -283,8 +297,10 @@ export class StrandFormationManager {
|
|
|
283
297
|
* Builds a contact message carrying the real token + disclosure + this party's
|
|
284
298
|
* real cadre addresses + the joiner's consent (minted and signed by the
|
|
285
299
|
* solicitation layer — this manager only places it on the contact), dials the
|
|
286
|
-
*
|
|
287
|
-
*
|
|
300
|
+
* inviting party's machines the invitation names, one at a time, over the native
|
|
301
|
+
* protocol ({@link dialFormationByMachine}), and validates the responder's result
|
|
302
|
+
* before returning. Every machine gets the same contact, so at most one of them can
|
|
303
|
+
* record this redemption.
|
|
288
304
|
*/
|
|
289
305
|
async formStrand(
|
|
290
306
|
invitation: OpenInvitation,
|
|
@@ -301,12 +317,12 @@ export class StrandFormationManager {
|
|
|
301
317
|
usageStampId: consent.usageStampId,
|
|
302
318
|
peerSignature: consent.peerSignature,
|
|
303
319
|
disclosure,
|
|
304
|
-
cadrePeerAddrs: this.
|
|
320
|
+
cadrePeerAddrs: this.currentCadrePeerAddrs()
|
|
305
321
|
};
|
|
306
322
|
|
|
307
323
|
this.dialerSessions++;
|
|
308
324
|
try {
|
|
309
|
-
const dialed = await
|
|
325
|
+
const dialed = await dialFormationByMachine(node, {
|
|
310
326
|
contact,
|
|
311
327
|
responderAddrs: invitation.bootstrap,
|
|
312
328
|
validateResponse: (response) => this.validateResponse(invitation, disclosure, response),
|
|
@@ -383,7 +399,12 @@ export class StrandFormationManager {
|
|
|
383
399
|
|
|
384
400
|
// ── Responder-side hooks ─────────────────────────────────────────────────────
|
|
385
401
|
|
|
386
|
-
|
|
402
|
+
/**
|
|
403
|
+
* An expired or used-up invitation is `'token-spent'` (final); one with no row here is
|
|
404
|
+
* `'token-unknown'` (retryable — the row may not have replicated to this machine yet), and
|
|
405
|
+
* so is one from a recorder that does not say why.
|
|
406
|
+
*/
|
|
407
|
+
private async validateToken(token: string): Promise<FormationTokenCheck> {
|
|
387
408
|
if (!this.formationUsageRecorder) {
|
|
388
409
|
// No recorder configured — accept all tokens.
|
|
389
410
|
return { valid: true };
|
|
@@ -391,13 +412,13 @@ export class StrandFormationManager {
|
|
|
391
412
|
|
|
392
413
|
const tokenCheck = await this.formationUsageRecorder.isTokenValid(token);
|
|
393
414
|
if (!tokenCheck.valid) {
|
|
394
|
-
log('Token invalid: %s', token);
|
|
395
|
-
return { valid: false };
|
|
415
|
+
log('Token invalid (%s): %s', tokenCheck.reason ?? 'unknown', token);
|
|
416
|
+
return { valid: false, code: tokenCheck.reason === 'expired' ? 'token-spent' : 'token-unknown' };
|
|
396
417
|
}
|
|
397
418
|
|
|
398
419
|
if (await this.formationUsageRecorder.isTokenUsed(token)) {
|
|
399
420
|
log('Token already used: %s', token);
|
|
400
|
-
return { valid: false };
|
|
421
|
+
return { valid: false, code: 'token-spent' };
|
|
401
422
|
}
|
|
402
423
|
|
|
403
424
|
return { valid: true };
|
|
@@ -438,7 +459,7 @@ export class StrandFormationManager {
|
|
|
438
459
|
* ({@link InvitationExhaustedError}, raised by `ControlDatabase` when the count of recorded
|
|
439
460
|
* uses has reached the invite's seat budget — which is how the loser of a same-node race
|
|
440
461
|
* surfaces, the local write queue having serialized the two writes) is reported as
|
|
441
|
-
* `'
|
|
462
|
+
* `'token-spent'`, because retrying it can never succeed. Concurrent redemptions never
|
|
442
463
|
* contend for a shared row key — each writes under its own `UsageStampId` — so there is no
|
|
443
464
|
* key collision to surface here at all. The LOG-before-reject keeps this a deliberate
|
|
444
465
|
* internal-error→protocol-rejection conversion (AGENTS.md: don't eat exceptions silently),
|
|
@@ -456,7 +477,7 @@ export class StrandFormationManager {
|
|
|
456
477
|
const disclosureText = canonicalJson(contact.disclosure);
|
|
457
478
|
if (uint8ArrayFromString(disclosureText, 'utf8').byteLength > MAX_DISCLOSURE_BYTES) {
|
|
458
479
|
log('Disclosure over %d bytes; rejecting token %s', MAX_DISCLOSURE_BYTES, token);
|
|
459
|
-
return { approved: false, reason: 'Disclosure too large' };
|
|
480
|
+
return { approved: false, code: 'disclosure-too-large', reason: 'Disclosure too large' };
|
|
460
481
|
}
|
|
461
482
|
|
|
462
483
|
const recorder = this.formationUsageRecorder;
|
|
@@ -487,7 +508,7 @@ export class StrandFormationManager {
|
|
|
487
508
|
// here can atomically un-issue a strand-DB row.
|
|
488
509
|
const issued = await this.issueBoundMembershipInvite(token, resolved.strandId, signal);
|
|
489
510
|
if (!issued.ok) {
|
|
490
|
-
return
|
|
511
|
+
return issued.rejection;
|
|
491
512
|
}
|
|
492
513
|
await authorized.record();
|
|
493
514
|
return this.approve({
|
|
@@ -501,7 +522,7 @@ export class StrandFormationManager {
|
|
|
501
522
|
}
|
|
502
523
|
case 'missing': {
|
|
503
524
|
log('Host strand %s not yet available on this responder; rejecting token %s', resolved.strandId, token);
|
|
504
|
-
return { approved: false, reason: 'Host strand not yet available on this responder' };
|
|
525
|
+
return { approved: false, code: 'host-strand-unavailable', reason: 'Host strand not yet available on this responder' };
|
|
505
526
|
}
|
|
506
527
|
case 'unbound':
|
|
507
528
|
return await this.provisionUnbound(contact, disclosureText, signal);
|
|
@@ -514,29 +535,27 @@ export class StrandFormationManager {
|
|
|
514
535
|
}
|
|
515
536
|
if (err instanceof FormationApprovalError) {
|
|
516
537
|
log('approval failed (%s) for token %s: %o', err.failure, token, err);
|
|
517
|
-
return
|
|
538
|
+
return APPROVAL_REJECTIONS[err.failure];
|
|
518
539
|
}
|
|
519
540
|
if (err instanceof InvitationExhaustedError) {
|
|
520
|
-
// Shares `INVALID_TOKEN_REASON` with the up-front `validateToken`
|
|
521
|
-
// of the race that exposed the spent invite sees exactly what a
|
|
522
|
-
// sees.
|
|
523
|
-
// operator
|
|
524
|
-
// NOTE: if a joining client ever has to tell "never valid" from "used up" WITHOUT node
|
|
525
|
-
// logs, that is a new protocol reason string, not a local change here.
|
|
541
|
+
// Shares `INVALID_TOKEN_REASON` and `'token-spent'` with the up-front `validateToken`
|
|
542
|
+
// rejection so the loser of the race that exposed the spent invite sees exactly what a
|
|
543
|
+
// non-racing latecomer sees. How many uses were recorded against the total is for the
|
|
544
|
+
// operator, in this log line, not for the joiner.
|
|
526
545
|
log(
|
|
527
546
|
'invitation exhausted for token %s: %d of %d use(s) already recorded',
|
|
528
547
|
err.token,
|
|
529
548
|
err.usesRecorded,
|
|
530
549
|
err.totalUses
|
|
531
550
|
);
|
|
532
|
-
return { approved: false, reason: INVALID_TOKEN_REASON };
|
|
551
|
+
return { approved: false, code: 'token-spent', reason: INVALID_TOKEN_REASON };
|
|
533
552
|
}
|
|
534
553
|
// An approval is never discarded to a lost key race: each redemption writes under its
|
|
535
554
|
// own `UsageStampId`, so no other writer can take its row key. Reaching this catch-all
|
|
536
555
|
// means the failure was never retryable at the database layer to begin with (or the
|
|
537
556
|
// transient-cluster retry inside `ControlDatabase.lockedWithRetry` ran out).
|
|
538
557
|
log('provisionAsResponder failed for token %s: %o', token, err);
|
|
539
|
-
return { approved: false, reason: 'Formation conflict, retry' };
|
|
558
|
+
return { approved: false, code: 'conflict', reason: 'Formation conflict, retry' };
|
|
540
559
|
}
|
|
541
560
|
}
|
|
542
561
|
|
|
@@ -579,7 +598,7 @@ export class StrandFormationManager {
|
|
|
579
598
|
token: string,
|
|
580
599
|
strandId: string,
|
|
581
600
|
signal?: AbortSignal
|
|
582
|
-
): Promise<{ ok: true; invite?: StrandMembershipInvite } | { ok: false;
|
|
601
|
+
): Promise<{ ok: true; invite?: StrandMembershipInvite } | { ok: false; rejection: FormationRejection }> {
|
|
583
602
|
if (!this.issueMembershipInvite) {
|
|
584
603
|
return { ok: true };
|
|
585
604
|
}
|
|
@@ -589,10 +608,16 @@ export class StrandFormationManager {
|
|
|
589
608
|
} catch (err) {
|
|
590
609
|
if (err instanceof PreSplitStrandIdentityError) {
|
|
591
610
|
log('host strand %s is pre-split (token %s); rejecting — it must be recreated: %o', strandId, token, err);
|
|
592
|
-
return {
|
|
611
|
+
return {
|
|
612
|
+
ok: false,
|
|
613
|
+
rejection: { approved: false, code: 'host-strand-must-be-recreated', reason: HOST_STRAND_MUST_BE_RECREATED_REASON }
|
|
614
|
+
};
|
|
593
615
|
}
|
|
594
616
|
log('membership-invite issue for strand %s failed (token %s); rejecting retryably: %o', strandId, token, err);
|
|
595
|
-
return {
|
|
617
|
+
return {
|
|
618
|
+
ok: false,
|
|
619
|
+
rejection: { approved: false, code: 'host-strand-unavailable', reason: MEMBERSHIP_INVITE_UNAVAILABLE_REASON }
|
|
620
|
+
};
|
|
596
621
|
}
|
|
597
622
|
}
|
|
598
623
|
|