@serfab/cadre-core 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cadre-invite-protocol.d.ts +35 -13
- package/dist/cadre-invite-protocol.js +90 -25
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-node.d.ts +586 -255
- package/dist/cadre-node.js +1176 -564
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +66 -9
- package/dist/control-cohort.js +67 -9
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +451 -193
- package/dist/control-database.js +701 -359
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +6 -0
- package/dist/control-protocol-guard.js +14 -2
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +48 -31
- package/dist/control-schema-version.js +67 -45
- package/dist/control-schema-version.js.map +1 -1
- package/dist/control-schema.d.ts +8 -8
- package/dist/control-schema.js +16 -16
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +42 -7
- package/dist/control-write-retry.js +87 -19
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +16 -13
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +26 -9
- package/dist/joined-strand-store.js +63 -24
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/member-redemption.d.ts +15 -9
- package/dist/member-redemption.js +13 -8
- package/dist/member-redemption.js.map +1 -1
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-grant-protocol.d.ts +11 -7
- package/dist/owner-grant-protocol.js +31 -17
- package/dist/owner-grant-protocol.js.map +1 -1
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +89 -20
- package/dist/peer-join-backfill.js +265 -108
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +47 -40
- package/dist/seed-bootstrap.js +66 -61
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-database.d.ts +23 -2
- package/dist/strand-database.js +26 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -89
- package/dist/strand-first-sync-gate.js +288 -122
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +26 -17
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +30 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +40 -28
- package/dist/strand-membership-writer.js +212 -153
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.js +5 -2
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +90 -49
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-invite-protocol.ts +119 -31
- package/src/cadre-node.ts +1282 -584
- package/src/control-cohort.ts +86 -9
- package/src/control-database.ts +848 -403
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +16 -2
- package/src/control-schema-version.ts +101 -51
- package/src/control-schema.ts +16 -16
- package/src/control-write-retry.ts +91 -19
- package/src/delegate-admission.ts +351 -351
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +30 -17
- package/src/joined-strand-store.ts +69 -24
- package/src/link-budget.ts +9 -7
- package/src/member-redemption.ts +20 -13
- package/src/membership-view.ts +144 -0
- package/src/owner-grant-protocol.ts +33 -18
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-join-backfill.ts +971 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/seed-bootstrap.ts +72 -67
- package/src/strand-database.ts +352 -322
- package/src/strand-first-sync-gate.ts +334 -134
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +33 -25
- package/src/strand-membership-writer.ts +2093 -2019
- package/src/strand-move.ts +5 -2
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +89 -51
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type ControlDatabase
|
|
34
34
|
} from './control-database.js';
|
|
35
35
|
import { verifyCadreInviteRow, verifyCadreInviteRedemption } from './peer-authorization.js';
|
|
36
|
+
import { verifyPeerRecordSignature } from './peer-record.js';
|
|
36
37
|
import { ed25519PublicKeyB64FromPeerId } from './ed25519-key.js';
|
|
37
38
|
import { CADRE_INVITATION_PREFIX, CADRE_JOIN_REQUEST_PREFIX, OWNER_GRANT_PREFIX, OWNER_KEY_EXPORT_STEM } from './cadre-join-request.js';
|
|
38
39
|
import {
|
|
@@ -54,7 +55,7 @@ import {
|
|
|
54
55
|
type RedemptionAddressOutcome,
|
|
55
56
|
type RedemptionRejection
|
|
56
57
|
} from './member-redemption.js';
|
|
57
|
-
import type { CadreInviteRow, CadreJoinProgress, SeedPeer } from './types.js';
|
|
58
|
+
import type { CadreInviteRow, CadreJoinProgress, PeerAddressRecord, SeedPeer } from './types.js';
|
|
58
59
|
|
|
59
60
|
const log = debug('sereus:cadre:invite-proto');
|
|
60
61
|
|
|
@@ -168,17 +169,28 @@ function isWellFormedCadreInvitation(value: unknown): value is CadreInvitation {
|
|
|
168
169
|
|
|
169
170
|
// ── Wire messages ────────────────────────────────────────────────────────────
|
|
170
171
|
|
|
172
|
+
/**
|
|
173
|
+
* The device's own address record as a request carries it: a {@link PeerAddressRecord} without
|
|
174
|
+
* the peer id, which the connection gives, and the key, which is the request's `peerKey`.
|
|
175
|
+
*/
|
|
176
|
+
export type RedeemSelfRecord = Pick<PeerAddressRecord, 'addrs' | 'updatedAt' | 'profile' | 'sig'>;
|
|
177
|
+
|
|
171
178
|
/** Device → member: one frame, the whole redemption. */
|
|
172
179
|
export interface CadreInviteRedeemRequest {
|
|
173
|
-
v:
|
|
180
|
+
v: 2;
|
|
174
181
|
/** The cadre the device believes it is joining; a machine serving another party refuses by name. */
|
|
175
182
|
partyId: string;
|
|
176
183
|
/** The owner-signed row from the bundle. */
|
|
177
184
|
invite: CadreInviteRow;
|
|
178
185
|
/** The device's ed25519 public key, base64url; must be the key behind the connecting peer id. */
|
|
179
186
|
peerKey: string;
|
|
180
|
-
/**
|
|
181
|
-
|
|
187
|
+
/**
|
|
188
|
+
* The device's own address record for its `CadrePeer` row, self-signed for the invitation's
|
|
189
|
+
* party as `CadreNode.registerSelf` signs it (`signPeerRecord`); `addrs` is empty for a phone.
|
|
190
|
+
* The member stores `addrs` and, when the signature verifies over them, the rest of the
|
|
191
|
+
* record too ({@link admittedRecord}), so the device's first publish is its admission.
|
|
192
|
+
*/
|
|
193
|
+
record: RedeemSelfRecord;
|
|
182
194
|
/** Single-use nonce the device minted (`generateStampId`); both signatures cover it. */
|
|
183
195
|
usageStampId: string;
|
|
184
196
|
/** By `invitePrivateKey` over the `'redeem'` digest. */
|
|
@@ -336,19 +348,62 @@ export function isWellFormedCadreInviteRow(value: unknown): value is CadreInvite
|
|
|
336
348
|
&& isBoundedString(row.issuerSig);
|
|
337
349
|
}
|
|
338
350
|
|
|
351
|
+
/**
|
|
352
|
+
* The record's other fields are left to {@link admittedRecord}: a bad value there costs the
|
|
353
|
+
* device its signature on the row, not its admission.
|
|
354
|
+
*/
|
|
339
355
|
function isWellFormedRedeemRequest(value: unknown): value is CadreInviteRedeemRequest {
|
|
340
356
|
if (typeof value !== 'object' || value === null) return false;
|
|
341
|
-
const { v, partyId, invite, peerKey,
|
|
342
|
-
return v ===
|
|
357
|
+
const { v, partyId, invite, peerKey, record, usageStampId, inviteSig, peerSig } = value as Record<string, unknown>;
|
|
358
|
+
return v === 2
|
|
343
359
|
&& isBoundedString(partyId)
|
|
344
360
|
&& isWellFormedCadreInviteRow(invite)
|
|
345
361
|
&& isEd25519KeyB64(peerKey)
|
|
346
|
-
&& Array.isArray(
|
|
362
|
+
&& typeof record === 'object' && record !== null && Array.isArray((record as Record<string, unknown>).addrs)
|
|
347
363
|
&& isBoundedString(usageStampId)
|
|
348
364
|
&& isBoundedString(inviteSig)
|
|
349
365
|
&& isBoundedString(peerSig);
|
|
350
366
|
}
|
|
351
367
|
|
|
368
|
+
/** What the member writes on the device's `CadrePeer` row from the request's record. */
|
|
369
|
+
interface AdmittedRecord {
|
|
370
|
+
/** The record's addresses, sanitized ({@link sanitizeAddrs}): stored whether or not the rest is. */
|
|
371
|
+
addrs: string[];
|
|
372
|
+
/** The record's freshness, profile and signature, or null when they cannot be stored as the device's own. */
|
|
373
|
+
signed: Pick<PeerAddressRecord, 'updatedAt' | 'profile' | 'sig'> | null;
|
|
374
|
+
/** Why `signed` is null; absent when it is not. */
|
|
375
|
+
unsignedBecause?: string;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Judge the device's record (`request.record`) for its admission row. The signature covers the
|
|
380
|
+
* exact address list, so it is kept only when sanitizing dropped nothing, the profile is one the
|
|
381
|
+
* schema's `KnownProfile` check admits, the stamp and signature are in their stored forms, and
|
|
382
|
+
* the signature verifies for `partyId` under the device's key. Anything less admits the device
|
|
383
|
+
* with its addresses and no signature, as an owner-written row is, and the device's next
|
|
384
|
+
* publish fills it in. No clock check: a stamp in the future or the past is one the device's own
|
|
385
|
+
* self-publish could write too.
|
|
386
|
+
*/
|
|
387
|
+
function admittedRecord(partyId: string, remotePeerId: string, request: Pick<CadreInviteRedeemRequest, 'peerKey' | 'record'>): AdmittedRecord {
|
|
388
|
+
const { addrs: offered, updatedAt, profile, sig } = request.record as Partial<Record<keyof RedeemSelfRecord, unknown>>;
|
|
389
|
+
const addrs = sanitizeAddrs(offered);
|
|
390
|
+
const unsigned = (because: string): AdmittedRecord => ({ addrs, signed: null, unsignedBecause: because });
|
|
391
|
+
// Sanitizing only drops entries, so an unchanged length is an unchanged list.
|
|
392
|
+
if (!Array.isArray(offered) || addrs.length !== offered.length) {
|
|
393
|
+
return unsigned('sanitizing changed the signed address list');
|
|
394
|
+
}
|
|
395
|
+
if (profile !== null && profile !== 'transaction' && profile !== 'storage') {
|
|
396
|
+
return unsigned('unknown profile');
|
|
397
|
+
}
|
|
398
|
+
if (typeof updatedAt !== 'number' || !Number.isSafeInteger(updatedAt) || updatedAt <= 0 || !isBoundedString(sig)) {
|
|
399
|
+
return unsigned('malformed stamp or signature');
|
|
400
|
+
}
|
|
401
|
+
if (!verifyPeerRecordSignature(partyId, { peerId: remotePeerId, publicKey: request.peerKey, addrs, updatedAt, profile, sig })) {
|
|
402
|
+
return unsigned('signature does not verify');
|
|
403
|
+
}
|
|
404
|
+
return { addrs, signed: { updatedAt, profile, sig } };
|
|
405
|
+
}
|
|
406
|
+
|
|
352
407
|
/**
|
|
353
408
|
* Keep only parsable, distinct multiaddr strings, capped at {@link MAX_ADDRS}, order kept.
|
|
354
409
|
* Applied on both sides: the member bounds what it stores on the device's row, the device
|
|
@@ -411,22 +466,26 @@ export interface RedeemSigner {
|
|
|
411
466
|
* (invitation key, `usageStampId`, device key) for the invitation's party. The same bytes
|
|
412
467
|
* the schema verifies at the member (`cadreInviteRedeemMessage` / `cadreInviteConsentMessage`),
|
|
413
468
|
* whose own `party_id()` must therefore be the party the bundle names.
|
|
469
|
+
*
|
|
470
|
+
* @param record - the device's address record, already signed for the invitation's party over
|
|
471
|
+
* sanitized addresses ({@link sanitizeAddrs}); copied in unchanged, since the signature
|
|
472
|
+
* covers the exact list.
|
|
414
473
|
*/
|
|
415
474
|
export function signRedeemRequest(
|
|
416
475
|
invitation: CadreInvitation,
|
|
417
476
|
signer: RedeemSigner,
|
|
418
477
|
usageStampId: string,
|
|
419
|
-
|
|
478
|
+
record: RedeemSelfRecord
|
|
420
479
|
): CadreInviteRedeemRequest {
|
|
421
480
|
const fields = { inviteKey: invitation.invite.key, usageStampId, peerKey: signer.peerKey };
|
|
422
481
|
const signB64 = (message: Uint8Array, privateKey: string): string =>
|
|
423
482
|
sign(message, privateKey, 'ed25519', 'bytes', 'base64url', 'base64url') as string;
|
|
424
483
|
return {
|
|
425
|
-
v:
|
|
484
|
+
v: 2,
|
|
426
485
|
partyId: invitation.partyId,
|
|
427
486
|
invite: invitation.invite,
|
|
428
487
|
peerKey: signer.peerKey,
|
|
429
|
-
|
|
488
|
+
record: { addrs: [...record.addrs], updatedAt: record.updatedAt, profile: record.profile, sig: record.sig },
|
|
430
489
|
usageStampId,
|
|
431
490
|
inviteSig: signB64(cadreInviteRedeemMessage(invitation.partyId, fields), invitation.invitePrivateKey),
|
|
432
491
|
peerSig: signB64(cadreInviteConsentMessage(invitation.partyId, fields), signer.peerPrivateKey)
|
|
@@ -466,7 +525,7 @@ export function verifyRedeemReply(
|
|
|
466
525
|
// ── Member side (handler) ────────────────────────────────────────────────────
|
|
467
526
|
|
|
468
527
|
/** The control-database surface a redemption needs; a `ControlDatabase` satisfies it. */
|
|
469
|
-
export type CadreInviteStore = Pick<ControlDatabase, 'seatCadreInvite' | 'isCadreInviteLive' | 'redeemCadreInvite' | 'querySeedPeers'>;
|
|
528
|
+
export type CadreInviteStore = Pick<ControlDatabase, 'queryCadreInvite' | 'seatCadreInvite' | 'isCadreInviteLive' | 'redeemCadreInvite' | 'querySeedPeers'>;
|
|
470
529
|
|
|
471
530
|
export interface CadreInviteHandlerOptions extends MemberRedemptionHandlerOptions {
|
|
472
531
|
/** This member's peer id: the reply's peer list marks it preferred, since the device just reached it. */
|
|
@@ -485,11 +544,11 @@ const REDEEM_CONSTRAINT_CODES: ReadonlyArray<ConstraintRefusalRow<CadreInviteRej
|
|
|
485
544
|
* Member side of the invitation protocol (`MemberRedemptionHandler` has the shape): after the
|
|
486
545
|
* request's shape, it checks the party, the connecting identity (an ed25519 peer id is an
|
|
487
546
|
* identity multihash of the very key, and this is the only layer that can check the pair),
|
|
488
|
-
* both signatures and a targeted invitation's device, then seats the row from the bundle
|
|
489
|
-
* redeems. An issuer this member does not know as an owner is the
|
|
490
|
-
* the device tries the next address, which may hold the issuer's row.
|
|
547
|
+
* both signatures and a targeted invitation's device, then seats the row from the bundle when
|
|
548
|
+
* this member holds none and redeems. An issuer this member does not know as an owner is the
|
|
549
|
+
* one retryable refusal left: the device tries the next address, which may hold the issuer's row.
|
|
491
550
|
*/
|
|
492
|
-
export class CadreInviteHandler extends MemberRedemptionHandler<CadreInviteRedeemRequest, CadreInviteRedeemReply> {
|
|
551
|
+
export class CadreInviteHandler extends MemberRedemptionHandler<CadreInviteRedeemRequest, CadreInviteRedeemReply, CadreInviteRow> {
|
|
493
552
|
private readonly peerId: string;
|
|
494
553
|
private readonly store: CadreInviteStore;
|
|
495
554
|
protected readonly busyRefusal = rejection('busy', 'Too many concurrent redemptions');
|
|
@@ -523,27 +582,41 @@ export class CadreInviteHandler extends MemberRedemptionHandler<CadreInviteRedee
|
|
|
523
582
|
return null;
|
|
524
583
|
}
|
|
525
584
|
|
|
526
|
-
protected
|
|
527
|
-
return this.store.
|
|
585
|
+
protected readHeld(request: CadreInviteRedeemRequest): Promise<CadreInviteRow | null> {
|
|
586
|
+
return this.store.queryCadreInvite(request.invite.key);
|
|
528
587
|
}
|
|
529
588
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
589
|
+
protected isLive(request: CadreInviteRedeemRequest, held: CadreInviteRow | null): Promise<boolean> {
|
|
590
|
+
return this.store.isCadreInviteLive(request.invite, held);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* The two writes — the seat (its own transaction), only when this member holds no row, and
|
|
595
|
+
* the redemption — each answered by name when it fails. The held row is carried into the
|
|
596
|
+
* redemption so it is not read again in the write lock; after a fresh seat nothing is carried
|
|
597
|
+
* and the redemption reads the row it seated, since the bundle's copy is the requester's.
|
|
598
|
+
*/
|
|
599
|
+
protected async redeemAdmitted(request: CadreInviteRedeemRequest, remotePeerId: string, held: CadreInviteRow | null): Promise<CadreInviteRedeemReply> {
|
|
600
|
+
if (held === null) {
|
|
601
|
+
const refused = await this.seat(request, remotePeerId);
|
|
602
|
+
if (refused) return refused;
|
|
603
|
+
}
|
|
604
|
+
// Built before the write: the `CadrePeer` membership view is normally warm then (the
|
|
605
|
+
// connection gate read it when the device connected), and the redemption's commit drops it.
|
|
606
|
+
// The list lacks the device's own row, which the device drops from it anyway.
|
|
607
|
+
const peers = await this.peersBestEffort();
|
|
608
|
+
const record = admittedRecord(this.partyId, remotePeerId, request);
|
|
609
|
+
if (record.signed === null) {
|
|
610
|
+
log('admitting %s without its own record signature: %s', remotePeerId, record.unsignedBecause);
|
|
540
611
|
}
|
|
541
612
|
try {
|
|
542
613
|
const result = await this.store.redeemCadreInvite({
|
|
543
614
|
inviteKey: request.invite.key,
|
|
615
|
+
...(held ? { held } : {}),
|
|
544
616
|
peerId: remotePeerId,
|
|
545
617
|
peerKey: request.peerKey,
|
|
546
|
-
multiaddrs:
|
|
618
|
+
multiaddrs: record.addrs,
|
|
619
|
+
...(record.signed ? { record: record.signed } : {}),
|
|
547
620
|
usageStampId: request.usageStampId,
|
|
548
621
|
inviteSig: request.inviteSig,
|
|
549
622
|
peerSig: request.peerSig
|
|
@@ -556,9 +629,24 @@ export class CadreInviteHandler extends MemberRedemptionHandler<CadreInviteRedee
|
|
|
556
629
|
log('redeeming invitation %s for %s failed: %o', request.invite.key, remotePeerId, error);
|
|
557
630
|
return constraintRefusal(error, REDEEM_CONSTRAINT_CODES) ?? writeFailureRefusal(error);
|
|
558
631
|
}
|
|
559
|
-
// The row as held:
|
|
560
|
-
// is the primary key and every column is under the issuer's
|
|
561
|
-
|
|
632
|
+
// The row as held: read before the write, or the bundle's copy the seat inserted verbatim
|
|
633
|
+
// or found already held (its key is the primary key and every column is under the issuer's
|
|
634
|
+
// signature).
|
|
635
|
+
return { accepted: true, partyId: this.partyId, invite: held ?? request.invite, peers };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Seat the bundle's copy of the row; a refusal answered by name, or null once it is held. */
|
|
639
|
+
private async seat(request: CadreInviteRedeemRequest, remotePeerId: string): Promise<CadreInviteRejection | null> {
|
|
640
|
+
try {
|
|
641
|
+
await this.store.seatCadreInvite(request.invite);
|
|
642
|
+
return null;
|
|
643
|
+
} catch (error) {
|
|
644
|
+
if (error instanceof CadreInviteIssuerUnknownError) {
|
|
645
|
+
return rejection('issuer-unknown', 'The invitation\'s issuer is not known as an owner here');
|
|
646
|
+
}
|
|
647
|
+
log('seating invitation %s from %s failed: %o', request.invite.key, remotePeerId, error);
|
|
648
|
+
return constraintRefusal(error, REDEEM_CONSTRAINT_CODES) ?? writeFailureRefusal(error);
|
|
649
|
+
}
|
|
562
650
|
}
|
|
563
651
|
|
|
564
652
|
/** Dial hints are an optimization; a failed read costs the device its hints, not its membership. */
|