@serfab/cadre-core 1.14.0 → 1.16.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 +293 -274
- package/dist/cadre-invite-protocol.d.ts +64 -29
- package/dist/cadre-invite-protocol.js +207 -38
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +42 -0
- package/dist/cadre-join-request.js +144 -0
- package/dist/cadre-join-request.js.map +1 -0
- package/dist/cadre-node.d.ts +375 -79
- package/dist/cadre-node.js +758 -186
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-database.d.ts +92 -12
- package/dist/control-database.js +323 -142
- package/dist/control-database.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +19 -3
- package/dist/control-schema.js.map +1 -1
- package/dist/ed25519-key.d.ts +7 -9
- package/dist/ed25519-key.js +20 -10
- package/dist/ed25519-key.js.map +1 -1
- package/dist/fetch-impl.d.ts +12 -0
- package/dist/fetch-impl.js +23 -0
- package/dist/fetch-impl.js.map +1 -0
- package/dist/file-durable-slot.js +2 -1
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/file-safe-name.d.ts +25 -0
- package/dist/file-safe-name.js +29 -0
- package/dist/file-safe-name.js.map +1 -0
- package/dist/formation-approval.js +5 -11
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +0 -19
- package/dist/fs-atomic.js +3 -24
- package/dist/fs-atomic.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.js +31 -6
- package/dist/index.js.map +1 -1
- package/dist/invitation-bootstrap.d.ts +50 -12
- package/dist/invitation-bootstrap.js +76 -13
- package/dist/invitation-bootstrap.js.map +1 -1
- package/dist/joined-strand-store.d.ts +7 -0
- package/dist/joined-strand-store.js +9 -0
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +2 -1
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +9 -4
- package/dist/link-budget.js +9 -4
- package/dist/link-budget.js.map +1 -1
- package/dist/machine-removal.d.ts +78 -0
- package/dist/machine-removal.js +166 -0
- package/dist/machine-removal.js.map +1 -0
- package/dist/membership-connection-gater.d.ts +6 -5
- package/dist/membership-connection-gater.js +6 -5
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-claim-payload.d.ts +55 -0
- package/dist/node-claim-payload.js +108 -4
- package/dist/node-claim-payload.js.map +1 -1
- package/dist/node-default-transports.d.ts +27 -0
- package/dist/node-default-transports.js +35 -0
- package/dist/node-default-transports.js.map +1 -0
- package/dist/node-transports.d.ts +30 -0
- package/dist/node-transports.js +40 -0
- package/dist/node-transports.js.map +1 -0
- package/dist/peer-dial.d.ts +124 -27
- package/dist/peer-dial.js +254 -30
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.js +6 -0
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/peer-record.d.ts +6 -45
- package/dist/peer-record.js +8 -50
- package/dist/peer-record.js.map +1 -1
- package/dist/primary-lan-address.d.ts +8 -0
- package/dist/primary-lan-address.js +46 -0
- package/dist/primary-lan-address.js.map +1 -0
- package/dist/provider/client.d.ts +131 -0
- package/dist/provider/client.js +226 -0
- package/dist/provider/client.js.map +1 -0
- package/dist/provider/descriptor.d.ts +41 -0
- package/dist/provider/descriptor.js +103 -0
- package/dist/provider/descriptor.js.map +1 -0
- package/dist/provider/index.d.ts +9 -0
- package/dist/provider/index.js +10 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/provider/oauth.d.ts +125 -0
- package/dist/provider/oauth.js +199 -0
- package/dist/provider/oauth.js.map +1 -0
- package/dist/provider/rent-node.d.ts +101 -0
- package/dist/provider/rent-node.js +220 -0
- package/dist/provider/rent-node.js.map +1 -0
- package/dist/relay-addrs.d.ts +9 -4
- package/dist/relay-addrs.js +9 -4
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +51 -16
- package/dist/relay-reservation.js +107 -24
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +22 -5
- package/dist/seed-bootstrap.js +48 -25
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/strand-commit-durability.d.ts +54 -0
- package/dist/strand-commit-durability.js +74 -0
- package/dist/strand-commit-durability.js.map +1 -0
- package/dist/strand-database.d.ts +23 -8
- package/dist/strand-database.js +41 -13
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +7 -1
- package/dist/strand-first-sync-gate.js +7 -1
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-protocol.js +4 -1
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +19 -1
- package/dist/strand-fret-addrs.js +27 -9
- package/dist/strand-fret-addrs.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +81 -19
- package/dist/strand-instance-manager.js +216 -105
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-move.d.ts +124 -0
- package/dist/strand-move.js +438 -0
- package/dist/strand-move.js.map +1 -0
- package/dist/strand-network-config.js +7 -6
- package/dist/strand-network-config.js.map +1 -1
- package/dist/strand-peer-redial.d.ts +113 -0
- package/dist/strand-peer-redial.js +236 -0
- package/dist/strand-peer-redial.js.map +1 -0
- package/dist/strand-wake-protocol.js +10 -7
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +212 -4
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +18 -10
- package/src/cadre-invite-protocol.ts +1052 -864
- package/src/cadre-join-request.ts +167 -0
- package/src/cadre-node.ts +10003 -9381
- package/src/control-database.ts +4663 -4451
- package/src/control-schema.ts +19 -3
- package/src/ed25519-key.ts +20 -10
- package/src/fetch-impl.ts +23 -0
- package/src/file-durable-slot.ts +64 -63
- package/src/file-safe-name.ts +32 -0
- package/src/formation-approval.ts +604 -610
- package/src/fs-atomic.ts +92 -117
- package/src/index.ts +71 -4
- package/src/invitation-bootstrap.ts +91 -13
- package/src/joined-strand-store.ts +508 -498
- package/src/key-store-file.ts +142 -141
- package/src/link-budget.ts +9 -4
- package/src/machine-removal.ts +216 -0
- package/src/membership-connection-gater.ts +638 -637
- package/src/node-claim-payload.ts +138 -4
- package/src/node-default-transports.ts +38 -0
- package/src/node-transports.ts +49 -0
- package/src/peer-dial.ts +604 -336
- package/src/peer-join-backfill.ts +6 -0
- package/src/peer-record.ts +159 -202
- package/src/primary-lan-address.ts +47 -0
- package/src/provider/client.ts +326 -0
- package/src/provider/descriptor.ts +129 -0
- package/src/provider/index.ts +59 -0
- package/src/provider/oauth.ts +285 -0
- package/src/provider/rent-node.ts +275 -0
- package/src/relay-addrs.ts +9 -4
- package/src/relay-reservation.ts +1211 -1121
- package/src/seed-bootstrap.ts +1502 -1471
- package/src/strand-commit-durability.ts +116 -0
- package/src/strand-database.ts +46 -14
- package/src/strand-first-sync-gate.ts +7 -1
- package/src/strand-formation-protocol.ts +1039 -1036
- package/src/strand-fret-addrs.ts +179 -145
- package/src/strand-instance-manager.ts +2026 -1883
- package/src/strand-move.ts +585 -0
- package/src/strand-network-config.ts +7 -6
- package/src/strand-peer-redial.ts +307 -0
- package/src/strand-wake-protocol.ts +10 -7
- package/src/types.ts +220 -4
package/src/fs-atomic.ts
CHANGED
|
@@ -1,117 +1,92 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Node-only filesystem helpers: atomic file write
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// file so a retry leaves no `.tmp` debris. The destination is only ever
|
|
94
|
-
// swapped by the atomic rename, so the previous bytes stay intact.
|
|
95
|
-
await rm(tmpPath, { force: true }).catch(() => {});
|
|
96
|
-
throw error;
|
|
97
|
-
}
|
|
98
|
-
await syncDirBestEffort(dir);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Best-effort fsync of a directory so a fresh file's directory entry is
|
|
103
|
-
* durable across power loss. Opening a directory for fsync is not portable
|
|
104
|
-
* (unsupported on Windows and some filesystems), and rename atomicity does not
|
|
105
|
-
* depend on it — so a failure here is logged and swallowed, never surfaced.
|
|
106
|
-
*/
|
|
107
|
-
async function syncDirBestEffort(dir: string): Promise<void> {
|
|
108
|
-
let dirHandle: FileHandle | undefined;
|
|
109
|
-
try {
|
|
110
|
-
dirHandle = await open(dir, 'r');
|
|
111
|
-
await dirHandle.sync();
|
|
112
|
-
} catch (error) {
|
|
113
|
-
log('directory fsync skipped for %s: %o', dir, error);
|
|
114
|
-
} finally {
|
|
115
|
-
await dirHandle?.close().catch(() => {});
|
|
116
|
-
}
|
|
117
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Node-only filesystem helpers: atomic file write and the ENOENT check, shared
|
|
3
|
+
* by `key-store-file.ts` and `file-durable-slot.ts`. The filename-safe encoding
|
|
4
|
+
* they also use is cross-platform and lives in `file-safe-name.ts`.
|
|
5
|
+
*
|
|
6
|
+
* This module imports `node:fs/promises`, so — like its consumers — it is kept
|
|
7
|
+
* OUT of the package's cross-platform default entry and is not itself an
|
|
8
|
+
* exported subpath; it is only reachable through the Node-only store modules.
|
|
9
|
+
*/
|
|
10
|
+
import debug from 'debug';
|
|
11
|
+
import { mkdir, rm, rename, open } from 'node:fs/promises';
|
|
12
|
+
import type { FileHandle } from 'node:fs/promises';
|
|
13
|
+
|
|
14
|
+
const log = debug('sereus:cadre:fs-atomic');
|
|
15
|
+
|
|
16
|
+
/** Best-effort POSIX permissions (ignored on Windows / unsupported FSes). */
|
|
17
|
+
export const DIR_MODE = 0o700;
|
|
18
|
+
export const FILE_MODE = 0o600;
|
|
19
|
+
|
|
20
|
+
/** Whether an unknown error is a Node "file/dir not found" (ENOENT). */
|
|
21
|
+
export function isNotFound(error: unknown): boolean {
|
|
22
|
+
return typeof error === 'object' && error !== null
|
|
23
|
+
&& (error as { code?: unknown }).code === 'ENOENT';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Crash-atomic file write: a concurrent reader sees either the complete
|
|
28
|
+
* previous bytes or the complete new bytes, never a torn file. `dir` is created
|
|
29
|
+
* (0700) if absent; the data is written to `tmpPath` (created exclusively,
|
|
30
|
+
* 0600), fsync'd, then atomically renamed onto `finalPath`. A failure at any
|
|
31
|
+
* point removes the temp file and leaves the previous file untouched.
|
|
32
|
+
*
|
|
33
|
+
* `tmpPath` must be a sibling of `finalPath` (same directory) so the rename
|
|
34
|
+
* stays within one filesystem and is therefore atomic, and should carry a
|
|
35
|
+
* random component so concurrent writers (and crash-orphaned leftovers) are
|
|
36
|
+
* collide-free.
|
|
37
|
+
*
|
|
38
|
+
* Durability bar: the temp file is fsync'd before the rename so a power loss
|
|
39
|
+
* cannot surface a file whose bytes never reached the platter, and `dir` is
|
|
40
|
+
* fsync'd afterwards (best-effort — see {@link syncDirBestEffort}) so the
|
|
41
|
+
* rename itself survives a crash. On POSIX `rename(2)` replaces atomically;
|
|
42
|
+
* Node maps to `MoveFileEx(MOVEFILE_REPLACE_EXISTING)` on Windows, which is
|
|
43
|
+
* also atomic — though it can fail with `EPERM`/`EBUSY` if another process
|
|
44
|
+
* holds the destination open. That cross-process case is out of scope for the
|
|
45
|
+
* single-flight in-process callers here; such a failure surfaces to the caller
|
|
46
|
+
* with the previous file left intact.
|
|
47
|
+
*/
|
|
48
|
+
export async function writeFileAtomically(
|
|
49
|
+
dir: string,
|
|
50
|
+
tmpPath: string,
|
|
51
|
+
finalPath: string,
|
|
52
|
+
data: Uint8Array
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
await mkdir(dir, { recursive: true, mode: DIR_MODE });
|
|
55
|
+
// 'wx' creates the temp file exclusively; a name collision (astronomically
|
|
56
|
+
// unlikely given the caller's random component) fails here before we own anything.
|
|
57
|
+
const handle = await open(tmpPath, 'wx', FILE_MODE);
|
|
58
|
+
try {
|
|
59
|
+
try {
|
|
60
|
+
await handle.writeFile(data);
|
|
61
|
+
await handle.sync(); // land bytes on disk before the rename exposes them
|
|
62
|
+
} finally {
|
|
63
|
+
await handle.close().catch(() => {});
|
|
64
|
+
}
|
|
65
|
+
await rename(tmpPath, finalPath);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Any failure after creation (write, sync, or rename): drop the temp
|
|
68
|
+
// file so a retry leaves no `.tmp` debris. The destination is only ever
|
|
69
|
+
// swapped by the atomic rename, so the previous bytes stay intact.
|
|
70
|
+
await rm(tmpPath, { force: true }).catch(() => {});
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
await syncDirBestEffort(dir);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Best-effort fsync of a directory so a fresh file's directory entry is
|
|
78
|
+
* durable across power loss. Opening a directory for fsync is not portable
|
|
79
|
+
* (unsupported on Windows and some filesystems), and rename atomicity does not
|
|
80
|
+
* depend on it — so a failure here is logged and swallowed, never surfaced.
|
|
81
|
+
*/
|
|
82
|
+
async function syncDirBestEffort(dir: string): Promise<void> {
|
|
83
|
+
let dirHandle: FileHandle | undefined;
|
|
84
|
+
try {
|
|
85
|
+
dirHandle = await open(dir, 'r');
|
|
86
|
+
await dirHandle.sync();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
log('directory fsync skipped for %s: %o', dir, error);
|
|
89
|
+
} finally {
|
|
90
|
+
await dirHandle?.close().catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,8 +7,12 @@ export { canonicalJson } from './canonical-json.js';
|
|
|
7
7
|
// Main CadreNode class
|
|
8
8
|
export { CadreNode, OWNER_INVITATION_DEFAULT_TTL_MS, CADRE_INVITATION_DEFAULT_TTL_MS } from './cadre-node.js';
|
|
9
9
|
|
|
10
|
+
// Removing a machine from a cadre: the by-name refusal `CadreNode.removeMachine` throws
|
|
11
|
+
// (the listing, plan and result types are in types.ts)
|
|
12
|
+
export { MachineRemovalRefusedError } from './machine-removal.js';
|
|
13
|
+
|
|
10
14
|
// Control database
|
|
11
|
-
export { ControlDatabase, MissingHostStrandError, InvitationExhaustedError, CadreInviteIssuerUnknownError, buildAuthorizationMessage, registerPartyIdFunction, formationVouchMessage, formationConsentMessage, cadreInviteAddMessage, cadreInviteRedeemMessage, cadreInviteConsentMessage, projectSeedPeers, REAPABLE_TABLES, type ReapableTable, type ControlDatabaseConfig, type ControlTable, type RevocableTable, type FormationUsageResult, type MembershipChangeListener, type GuardedDeleteListener, type ControlWriteAbandonedListener, type RevokedRowRef } from './control-database.js';
|
|
15
|
+
export { ControlDatabase, parseNullableStoredDatetimeMs, MissingHostStrandError, InvitationExhaustedError, CadreInviteIssuerUnknownError, buildAuthorizationMessage, registerPartyIdFunction, formationVouchMessage, formationConsentMessage, cadreInviteAddMessage, cadreInviteRedeemMessage, cadreInviteConsentMessage, projectSeedPeers, REAPABLE_TABLES, type ReapableTable, type ControlDatabaseConfig, type ControlTable, type RevocableTable, type FormationUsageResult, type FormationInviteRow, type MembershipChangeListener, type GuardedDeleteListener, type ControlWriteAbandonedListener, type RevokedRowRef, type MachineRemovalWrites } from './control-database.js';
|
|
12
16
|
|
|
13
17
|
// Bounded retry for transient control-write failures (classifier + loop behind
|
|
14
18
|
// ControlDatabase.lockedWithRetry; exported so the integration package can drive the
|
|
@@ -63,6 +67,10 @@ export {
|
|
|
63
67
|
type KeyId
|
|
64
68
|
} from './key-store.js';
|
|
65
69
|
|
|
70
|
+
// The filename-safe form of an id that every Node-only per-party file store names its file
|
|
71
|
+
// with (`<name>.<encoded party>.json`), for an embedder keeping its own file beside theirs.
|
|
72
|
+
export { encodeFileSafeComponent } from './file-safe-name.js';
|
|
73
|
+
|
|
66
74
|
// The KeyStore-backed identity rule, shared with embedding apps that need the
|
|
67
75
|
// node key BEFORE `CadreNode` exists.
|
|
68
76
|
export { loadOrCreateIdentityKey } from './identity-key.js';
|
|
@@ -155,6 +163,7 @@ export {
|
|
|
155
163
|
|
|
156
164
|
// Strand database
|
|
157
165
|
export { StrandDatabase, type StrandDatabaseConfig } from './strand-database.js';
|
|
166
|
+
export type { StrandCommitDurability, StrandCommitHeld } from './strand-commit-durability.js';
|
|
158
167
|
|
|
159
168
|
// Strand management
|
|
160
169
|
export {
|
|
@@ -428,7 +437,6 @@ export {
|
|
|
428
437
|
isPeerRecordFresh,
|
|
429
438
|
isSignalingAddr,
|
|
430
439
|
orderSignalingFirst,
|
|
431
|
-
withTrailingPeerId,
|
|
432
440
|
currentMemberTrustPolicy,
|
|
433
441
|
DEFAULT_PEER_RECORD_MAX_AGE_MS,
|
|
434
442
|
DEFAULT_PEER_RECORD_HEARTBEAT_MS
|
|
@@ -481,8 +489,12 @@ export {
|
|
|
481
489
|
} from './control-cohort.js';
|
|
482
490
|
|
|
483
491
|
// Dialing one peer from several candidate addresses, each under its own time
|
|
484
|
-
// limit, so an address that never answers cannot starve the rest.
|
|
492
|
+
// limit, so an address that never answers cannot starve the rest. Each address is
|
|
493
|
+
// bound to the peer expected and dialed outside libp2p's dial queue (`dialAddr`),
|
|
494
|
+
// so another dial of the same peer in flight cannot capture the attempt.
|
|
485
495
|
export {
|
|
496
|
+
dialAddr,
|
|
497
|
+
withTrailingPeerId,
|
|
486
498
|
dialPeerAddrs,
|
|
487
499
|
SelfRelayOnlyError,
|
|
488
500
|
PeerUnreachableError,
|
|
@@ -550,10 +562,56 @@ export {
|
|
|
550
562
|
export {
|
|
551
563
|
encodeNodeClaimPayload,
|
|
552
564
|
decodeNodeClaimPayload,
|
|
565
|
+
selectNodeClaimAddresses,
|
|
566
|
+
withClaimDestination,
|
|
567
|
+
destinationPeerIdOf,
|
|
568
|
+
classifyDialAddress,
|
|
553
569
|
NODE_CLAIM_PAYLOAD_PREFIX,
|
|
554
|
-
type NodeClaimPayload
|
|
570
|
+
type NodeClaimPayload,
|
|
571
|
+
type NodeClaimAddressOptions,
|
|
572
|
+
type DialAddressKind
|
|
555
573
|
} from './node-claim-payload.js';
|
|
556
574
|
|
|
575
|
+
// Moving a party's strands into another party of the same owner, so an app's strands follow
|
|
576
|
+
// the app into the user's main cadre: the bundle one node exports from the party it leaves,
|
|
577
|
+
// its adoption by a node of the party it joined, and the check that the party it left holds
|
|
578
|
+
// nothing the joined one lacks before it is dissolved.
|
|
579
|
+
export {
|
|
580
|
+
exportStrandBundle,
|
|
581
|
+
adoptStrandBundle,
|
|
582
|
+
readStrandHoldings,
|
|
583
|
+
unheldBundleContent,
|
|
584
|
+
StrandMoveNotOwnerError,
|
|
585
|
+
StrandMoveSamePartyError,
|
|
586
|
+
type StrandHoldings,
|
|
587
|
+
type StrandBundle,
|
|
588
|
+
type StrandBundleEntry,
|
|
589
|
+
type FormationInviteEntry,
|
|
590
|
+
type StrandMoveReport,
|
|
591
|
+
type StrandMoveFailure
|
|
592
|
+
} from './strand-move.js';
|
|
593
|
+
|
|
594
|
+
// The join request: what an app that wants to join a cadre copies to the app that manages it,
|
|
595
|
+
// which mints an invitation targeted at the request's peer id. Both paste-text prefixes live here.
|
|
596
|
+
export {
|
|
597
|
+
encodeCadreJoinRequest,
|
|
598
|
+
decodeCadreJoinRequest,
|
|
599
|
+
requireCadreJoinReply,
|
|
600
|
+
CADRE_JOIN_REQUEST_PREFIX,
|
|
601
|
+
CADRE_INVITATION_PREFIX,
|
|
602
|
+
CADRE_JOIN_REQUEST_MAX_APP_NAME,
|
|
603
|
+
type CadreJoinReply,
|
|
604
|
+
type CadreJoinRequest
|
|
605
|
+
} from './cadre-join-request.js';
|
|
606
|
+
|
|
607
|
+
// Renting a node from a hosting provider: the provider's descriptor, the app's side of the
|
|
608
|
+
// OAuth 2 + PKCE sign-in, the typed client for its container API, and `rentNode` /
|
|
609
|
+
// `finishRental`, which add the rented container to this node's cadre by invitation or by
|
|
610
|
+
// claim. Global `fetch` and `crypto.getRandomValues` only, so the whole set loads on React
|
|
611
|
+
// Native and in the browser (`docs/architecture.md` → "Renting a node from an app"). The
|
|
612
|
+
// barrel is the curated list; it is not repeated here.
|
|
613
|
+
export * from './provider/index.js';
|
|
614
|
+
|
|
557
615
|
// Schema Verification
|
|
558
616
|
export {
|
|
559
617
|
signSchema,
|
|
@@ -599,6 +657,7 @@ export { formationDeadlines, type FormationDeadlines } from './strand-formation-
|
|
|
599
657
|
export {
|
|
600
658
|
CadreInviteHandler,
|
|
601
659
|
redeemAtMembers,
|
|
660
|
+
redeemInPasses,
|
|
602
661
|
signRedeemRequest,
|
|
603
662
|
verifyRedeemReply,
|
|
604
663
|
encodeCadreInvitation,
|
|
@@ -620,9 +679,17 @@ export {
|
|
|
620
679
|
type CadreInviteHandlerOptions,
|
|
621
680
|
type CadreInviteStore,
|
|
622
681
|
type RedeemAtMembersOptions,
|
|
682
|
+
type RedeemInPassesOptions,
|
|
623
683
|
type RedeemSigner,
|
|
624
684
|
type CadreInviteRedemption
|
|
625
685
|
} from './cadre-invite-protocol.js';
|
|
686
|
+
// Which machines, and how many addresses of each, an invitation names; how far a cadre invitation reaches
|
|
687
|
+
export {
|
|
688
|
+
selectCadreInvitationMembers,
|
|
689
|
+
cadreInvitationReach,
|
|
690
|
+
CADRE_INVITATION_MAX_ADDRS,
|
|
691
|
+
CADRE_INVITATION_ADDRS_PER_SIBLING
|
|
692
|
+
} from './invitation-bootstrap.js';
|
|
626
693
|
// The pending-join retry loop behind CadreNode.requestJoin (PendingJoinStatus is in types.ts)
|
|
627
694
|
export { PENDING_JOIN_POLL_MS, PENDING_JOIN_MAX_BACKOFF_MS, MAX_PENDING_JOIN_MS } from './pending-join-runner.js';
|
|
628
695
|
|
|
@@ -1,23 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which of the inviting party's
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Which of the inviting party's machines an invitation names, so a joiner can still form or
|
|
3
|
+
* join while the machine that minted it is offline: an open strand invitation
|
|
4
|
+
* (`CadreNode.createOpenInvitation`, which puts the minting machine's own addresses first
|
|
5
|
+
* itself) and a cadre invitation ({@link selectCadreInvitationMembers}). Also how far a cadre
|
|
6
|
+
* invitation reaches once its minting machine is gone ({@link cadreInvitationReach}).
|
|
5
7
|
*/
|
|
6
8
|
|
|
9
|
+
import { classifyDialAddress, destinationPeerIdOf, type DialAddressKind } from './node-claim-payload.js';
|
|
10
|
+
import type { CadreInvitationReach } from './types.js';
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* At most this many of the party's other machines are named, each with at most
|
|
9
14
|
* {@link INVITATION_ADDRS_PER_SIBLING} addresses: 12 extra addresses of roughly 100–200
|
|
10
15
|
* characters, base64url-encoded into an invitation shared as text.
|
|
11
16
|
*
|
|
12
|
-
* NOTE:
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
+
* NOTE: a cadre invitation, which may be carried in a QR code, is capped harder
|
|
18
|
+
* ({@link CADRE_INVITATION_MAX_ADDRS}); an open strand invitation keeps these caps. If open
|
|
19
|
+
* invitations are ever carried in a QR code too, cap them the same way.
|
|
20
|
+
* NOTE: for an open strand invitation the per-machine cap keeps the record's order (relay first,
|
|
21
|
+
* then direct addresses as the machine published them, loopback included); a cadre invitation
|
|
22
|
+
* ranks them first. If an open invitation's sibling is seen to have its reachable address cut,
|
|
23
|
+
* rank the same way.
|
|
17
24
|
*/
|
|
18
25
|
export const INVITATION_SIBLING_MACHINES = 3;
|
|
19
26
|
export const INVITATION_ADDRS_PER_SIBLING = 4;
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Most addresses a cadre invitation names in total, so its text fits one QR code. Measured
|
|
30
|
+
* with three owner keys: 2,206 characters encoded at 6 relayed addresses of about 155
|
|
31
|
+
* characters (the worst case, since ranking puts relayed addresses first), 1,972 with relayed
|
|
32
|
+
* and direct WebSocket addresses alternating, against 2,331 bytes in the largest QR code at
|
|
33
|
+
* error-correction level M (2,953 at L). Each further address costs about 170 characters, each
|
|
34
|
+
* further owner key about 60.
|
|
35
|
+
*/
|
|
36
|
+
export const CADRE_INVITATION_MAX_ADDRS = 6;
|
|
37
|
+
|
|
38
|
+
/** Most addresses a cadre invitation names for each of the party's other machines. */
|
|
39
|
+
export const CADRE_INVITATION_ADDRS_PER_SIBLING = 2;
|
|
40
|
+
|
|
21
41
|
/** One other machine of the party whose signed address record resolved. */
|
|
22
42
|
export interface InvitationSibling {
|
|
23
43
|
peerId: string;
|
|
@@ -28,13 +48,71 @@ export interface InvitationSibling {
|
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
/**
|
|
31
|
-
* The addresses of the siblings an invitation names
|
|
32
|
-
* now come first, since a live connection is the best evidence that
|
|
33
|
-
* shows only that it was up within the freshness window. Within each
|
|
51
|
+
* The addresses of the siblings an invitation names, at most `perSibling` each. Machines this
|
|
52
|
+
* node is connected to right now come first, since a live connection is the best evidence that
|
|
53
|
+
* a machine is up; a record shows only that it was up within the freshness window. Within each
|
|
54
|
+
* half, newest record first.
|
|
34
55
|
*/
|
|
35
|
-
export function selectInvitationSiblingAddrs(
|
|
56
|
+
export function selectInvitationSiblingAddrs(
|
|
57
|
+
siblings: InvitationSibling[],
|
|
58
|
+
connected: ReadonlySet<string>,
|
|
59
|
+
perSibling: number = INVITATION_ADDRS_PER_SIBLING
|
|
60
|
+
): string[] {
|
|
36
61
|
return [...siblings]
|
|
37
62
|
.sort((a, b) => Number(connected.has(b.peerId)) - Number(connected.has(a.peerId)) || b.updatedAt - a.updatedAt)
|
|
38
63
|
.slice(0, INVITATION_SIBLING_MACHINES)
|
|
39
|
-
.flatMap((sibling) => sibling.addrs.slice(0,
|
|
64
|
+
.flatMap((sibling) => sibling.addrs.slice(0, perSibling));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The `members` of a cadre invitation: the minting machine's own addresses first (at most
|
|
69
|
+
* {@link INVITATION_ADDRS_PER_SIBLING}), then the other machines' in
|
|
70
|
+
* {@link selectInvitationSiblingAddrs} order (at most {@link CADRE_INVITATION_ADDRS_PER_SIBLING}
|
|
71
|
+
* each), the whole list cut to {@link CADRE_INVITATION_MAX_ADDRS}. Each machine's addresses are
|
|
72
|
+
* ranked for a phone before they are cut ({@link rankForPhone}): libp2p reports a machine
|
|
73
|
+
* listening on every interface with its loopback and private addresses ahead of its relayed one.
|
|
74
|
+
*
|
|
75
|
+
* NOTE: accepted tradeoff — with two or more other machines, only the best two addresses of
|
|
76
|
+
* each survive, connected machines first, so a reachable address of a sibling can be cut. Size
|
|
77
|
+
* was weighed over completeness, so the text fits a QR code; revisit if redemptions are seen
|
|
78
|
+
* failing for want of a sibling address the record held.
|
|
79
|
+
*/
|
|
80
|
+
export function selectCadreInvitationMembers(
|
|
81
|
+
own: readonly string[],
|
|
82
|
+
siblings: InvitationSibling[],
|
|
83
|
+
connected: ReadonlySet<string>
|
|
84
|
+
): string[] {
|
|
85
|
+
const ranked = siblings.map((sibling) => ({ ...sibling, addrs: rankForPhone(sibling.addrs) }));
|
|
86
|
+
return [
|
|
87
|
+
...rankForPhone(own).slice(0, INVITATION_ADDRS_PER_SIBLING),
|
|
88
|
+
...selectInvitationSiblingAddrs(ranked, connected, CADRE_INVITATION_ADDRS_PER_SIBLING),
|
|
89
|
+
].slice(0, CADRE_INVITATION_MAX_ADDRS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Order in which a phone is likeliest to reach an address: from anywhere, on its network, on that machine, not at all. */
|
|
93
|
+
const PHONE_DIAL_RANK: Record<DialAddressKind, number> = { relay: 0, name: 0, public: 0, private: 1, loopback: 2 };
|
|
94
|
+
|
|
95
|
+
/** `addrs` best first for a phone dialing them ({@link PHONE_DIAL_RANK}), ties in their given order; one a phone cannot dial (plain TCP) last. */
|
|
96
|
+
function rankForPhone(addrs: readonly string[]): string[] {
|
|
97
|
+
const rankOf = (addr: string): number => {
|
|
98
|
+
const kind = classifyDialAddress(addr);
|
|
99
|
+
return kind === undefined ? 3 : PHONE_DIAL_RANK[kind];
|
|
100
|
+
};
|
|
101
|
+
return addrs.map((addr) => ({ addr, rank: rankOf(addr) })).sort((a, b) => a.rank - b.rank).map(({ addr }) => addr);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Where a phone could redeem an invitation naming `members` from, judged on the machines other
|
|
106
|
+
* than `issuerPeerId`: in the app flows the minting app is the one closed or in the background
|
|
107
|
+
* when the other app redeems, and two apps on one phone cannot reach each other. An address
|
|
108
|
+
* names its machine by its last `/p2p/`.
|
|
109
|
+
*/
|
|
110
|
+
export function cadreInvitationReach(members: readonly string[], issuerPeerId: string): CadreInvitationReach {
|
|
111
|
+
const others = members.flatMap((addr) => {
|
|
112
|
+
const kind = classifyDialAddress(addr);
|
|
113
|
+
// Classified, so it parses and `destinationPeerIdOf` cannot throw.
|
|
114
|
+
return kind === undefined || destinationPeerIdOf(addr) === issuerPeerId ? [] : [kind];
|
|
115
|
+
});
|
|
116
|
+
if (others.some((kind) => kind === 'relay' || kind === 'name' || kind === 'public')) return 'anywhere';
|
|
117
|
+
return others.length > 0 ? 'local-network' : 'issuer-only';
|
|
40
118
|
}
|