@serfab/cadre-core 1.13.0 → 1.15.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 +383 -0
- package/dist/cadre-invite-protocol.js +774 -0
- package/dist/cadre-invite-protocol.js.map +1 -0
- 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 +849 -210
- package/dist/cadre-node.js +1574 -374
- package/dist/cadre-node.js.map +1 -1
- package/dist/claim-proof.d.ts +45 -0
- package/dist/claim-proof.js +132 -0
- package/dist/claim-proof.js.map +1 -0
- package/dist/control-authorization.d.ts +80 -19
- package/dist/control-authorization.js +55 -13
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.js +1 -1
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +605 -108
- package/dist/control-database.js +1299 -280
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +142 -0
- package/dist/control-protocol-guard.js +293 -0
- package/dist/control-protocol-guard.js.map +1 -0
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +817 -1067
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +4 -0
- package/dist/control-stream.js +22 -7
- package/dist/control-stream.js.map +1 -1
- package/dist/delegate-admission.d.ts +1 -1
- package/dist/delegate-admission.js +1 -1
- package/dist/device-token.d.ts +11 -9
- package/dist/device-token.js +14 -12
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +22 -9
- package/dist/ed25519-key.js +47 -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 +22 -10
- package/dist/index.js +54 -11
- 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 +25 -33
- package/dist/link-budget.js +25 -35
- 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 +191 -151
- package/dist/membership-connection-gater.js +236 -173
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-claim-payload.d.ts +74 -0
- package/dist/node-claim-payload.js +224 -0
- package/dist/node-claim-payload.js.map +1 -0
- 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/owner-anchor-sync.d.ts +22 -0
- package/dist/owner-anchor-sync.js +143 -0
- package/dist/owner-anchor-sync.js.map +1 -0
- package/dist/peer-authorization.d.ts +113 -18
- package/dist/peer-authorization.js +225 -29
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -27
- package/dist/peer-dial.js +276 -31
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +26 -2
- package/dist/peer-join-backfill.js +60 -12
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/peer-record.d.ts +17 -52
- package/dist/peer-record.js +20 -58
- package/dist/peer-record.js.map +1 -1
- package/dist/pending-join-runner.d.ts +66 -58
- package/dist/pending-join-runner.js +136 -145
- package/dist/pending-join-runner.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/push-fanout.d.ts +8 -2
- package/dist/push-fanout.js +8 -2
- package/dist/push-fanout.js.map +1 -1
- 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 +165 -123
- package/dist/seed-bootstrap.js +313 -275
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +98 -37
- package/dist/seed-trust-policy.js +188 -2
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +24 -26
- package/dist/strand-addr-protocol.js +19 -26
- package/dist/strand-addr-protocol.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 +5 -2
- 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-revocation-enforcer.d.ts +11 -11
- package/dist/strand-revocation-enforcer.js +11 -11
- package/dist/strand-wake-protocol.d.ts +13 -16
- package/dist/strand-wake-protocol.js +23 -27
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/trusted-owner-store.d.ts +35 -5
- package/dist/trusted-owner-store.js +50 -8
- package/dist/trusted-owner-store.js.map +1 -1
- package/dist/types.d.ts +512 -89
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +19 -10
- package/src/cadre-invite-protocol.ts +1052 -0
- package/src/cadre-join-request.ts +167 -0
- package/src/cadre-node.ts +1703 -386
- package/src/claim-proof.ts +147 -0
- package/src/control-authorization.ts +203 -117
- package/src/control-cohort.ts +1 -1
- package/src/control-database.ts +4663 -3453
- package/src/control-protocol-guard.ts +348 -0
- package/src/control-schema.ts +828 -1078
- package/src/control-stream.ts +26 -7
- package/src/delegate-admission.ts +1 -1
- package/src/device-token.ts +14 -10
- package/src/ed25519-key.ts +47 -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 +789 -648
- 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 +25 -36
- package/src/machine-removal.ts +216 -0
- package/src/membership-connection-gater.ts +259 -191
- package/src/node-claim-payload.ts +259 -0
- package/src/node-default-transports.ts +38 -0
- package/src/node-transports.ts +49 -0
- package/src/owner-anchor-sync.ts +171 -0
- package/src/peer-authorization.ts +258 -29
- package/src/peer-dial.ts +604 -315
- package/src/peer-join-backfill.ts +62 -11
- package/src/peer-record.ts +159 -197
- package/src/pending-join-runner.ts +729 -735
- 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/push-fanout.ts +8 -2
- package/src/relay-addrs.ts +9 -4
- package/src/relay-reservation.ts +1211 -1121
- package/src/seed-bootstrap.ts +1502 -1448
- package/src/seed-trust-policy.ts +353 -140
- package/src/strand-addr-protocol.ts +26 -35
- 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-revocation-enforcer.ts +11 -11
- package/src/strand-wake-protocol.ts +23 -30
- package/src/trusted-owner-store.ts +242 -171
- package/src/types.ts +2687 -2242
package/src/control-stream.ts
CHANGED
|
@@ -35,6 +35,10 @@ export interface ControlStream extends AsyncIterable<Uint8Array> {
|
|
|
35
35
|
send(data: Uint8Array): boolean;
|
|
36
36
|
close(): Promise<void>;
|
|
37
37
|
abort(err: Error): void;
|
|
38
|
+
/** Whether the remote has half-closed its write end (`'closed'`); absent on test doubles. */
|
|
39
|
+
readonly remoteWriteStatus?: string;
|
|
40
|
+
/** Bytes received and not yet read; absent on test doubles. */
|
|
41
|
+
readonly readBufferLength?: number;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/** Write a JSON object as a single 4-byte big-endian length-prefixed frame. */
|
|
@@ -206,16 +210,31 @@ export function readStreamToEnd(
|
|
|
206
210
|
return Promise.race([readLoop, timeout]).finally(() => clearTimeout(timer));
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
/**
|
|
213
|
+
/**
|
|
214
|
+
* Accumulate every chunk to EOF, rejecting once the running total exceeds `maxBytes`.
|
|
215
|
+
*
|
|
216
|
+
* A remote that half-closed before this read began has already sent everything it will,
|
|
217
|
+
* so the read ends once those buffered bytes are in. Its own end-of-stream cannot be
|
|
218
|
+
* waited for: libp2p 3.3.11's stream iterator ends on the one-shot `remoteCloseWrite`
|
|
219
|
+
* event, and an iterator attached after it fired never ends. That happens to any handler
|
|
220
|
+
* that awaits before reading, as the control-protocol guard's live membership check makes
|
|
221
|
+
* the wake and strand-addr handlers do.
|
|
222
|
+
*/
|
|
210
223
|
async function collect(stream: ControlStream, maxBytes: number, label: string): Promise<Uint8Array> {
|
|
211
224
|
const chunks: Uint8Array[] = [];
|
|
212
225
|
let total = 0;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
226
|
+
const alreadySent = stream.remoteWriteStatus === 'closed' ? (stream.readBufferLength ?? 0) : undefined;
|
|
227
|
+
if (alreadySent !== 0) {
|
|
228
|
+
for await (const chunk of stream) {
|
|
229
|
+
const bytes = toBytes(chunk);
|
|
230
|
+
chunks.push(bytes);
|
|
231
|
+
total += bytes.length;
|
|
232
|
+
if (total > maxBytes) {
|
|
233
|
+
throw new Error(`${label} message too large: ${total} bytes exceeds max ${maxBytes}`);
|
|
234
|
+
}
|
|
235
|
+
if (alreadySent !== undefined && total >= alreadySent) {
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
219
238
|
}
|
|
220
239
|
}
|
|
221
240
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* granted delegate is admitted outright, never drawing on the small
|
|
22
22
|
* unauthorized-reservation budget that unplaced peers share
|
|
23
23
|
* (`membership-connection-gater.ts` → "The relay-reservation seam") and never
|
|
24
|
-
*
|
|
24
|
+
* put on the provisional connection deadline. The fail-closed per-stream gate
|
|
25
25
|
* (`authorizeInboundControlStream`) never honors a grant, so a delegate gets
|
|
26
26
|
* the connection and its reservation (all a circuit-relay `hop` needs) and
|
|
27
27
|
* nothing more.
|
package/src/device-token.ts
CHANGED
|
@@ -9,15 +9,16 @@
|
|
|
9
9
|
* encoding, so no field split is ambiguous even with an opaque platform token. It
|
|
10
10
|
* is intentionally NOT JSON-canonicalized (deterministic across node/browser/RN,
|
|
11
11
|
* no key ordering) and is reconstructable inside the `DeviceToken.AuthorizedUpdate`
|
|
12
|
-
* SQL constraint from the row's own columns
|
|
12
|
+
* SQL constraint from the row's own columns and the node-local `party_id()`:
|
|
13
13
|
*
|
|
14
|
-
* digest('CadreControl.DeviceToken', 'publish',
|
|
14
|
+
* digest('CadreControl.DeviceToken', 'publish', party_id(),
|
|
15
15
|
* new.PeerId, new.Platform, new.Token, cast(new.UpdatedAt as text))
|
|
16
16
|
*
|
|
17
17
|
* Keep {@link deviceTokenSignedPayload} and that constraint byte-for-byte in sync.
|
|
18
18
|
* The `'publish'` action tag marks this as a peer's SELF-signed record (signed with
|
|
19
19
|
* the peer's own key, not an owner key) and keeps it disjoint from every
|
|
20
|
-
* owner-signed digest
|
|
20
|
+
* owner-signed digest; the party id keeps a record published into one party's table
|
|
21
|
+
* from verifying in another's (peer-record.ts says why a node uses its own party).
|
|
21
22
|
*
|
|
22
23
|
* Unlike a peer-address record, a device-token record carries NO public key: the
|
|
23
24
|
* signature is verified against the `CadrePeer.PublicKey` bound to the same PeerId
|
|
@@ -34,10 +35,11 @@ import type { DeviceTokenRecord, PushPlatform } from './types.js';
|
|
|
34
35
|
* `DeviceToken.AuthorizedUpdate` constraint exactly; both sides take the default
|
|
35
36
|
* base64url output of a single `digest(...)`, which round-trips cleanly.
|
|
36
37
|
*/
|
|
37
|
-
export function deviceTokenSignedPayload(record: Omit<DeviceTokenRecord, 'sig'>): string {
|
|
38
|
+
export function deviceTokenSignedPayload(partyId: string, record: Omit<DeviceTokenRecord, 'sig'>): string {
|
|
38
39
|
const fields = controlAuthorizationFields(
|
|
39
40
|
'CadreControl.DeviceToken',
|
|
40
41
|
'publish',
|
|
42
|
+
partyId,
|
|
41
43
|
[record.peerId, record.platform, record.token, String(record.updatedAt)]
|
|
42
44
|
);
|
|
43
45
|
return digest(fields, 'sha256', 'base64url') as string;
|
|
@@ -47,14 +49,16 @@ export function deviceTokenSignedPayload(record: Omit<DeviceTokenRecord, 'sig'>)
|
|
|
47
49
|
* Sign a device-token record with the ed25519 private key behind its `peerId`.
|
|
48
50
|
* Returns a fully-populated {@link DeviceTokenRecord}.
|
|
49
51
|
*
|
|
52
|
+
* @param partyId - the party whose `DeviceToken` table the record is published into
|
|
50
53
|
* @param fields - the record fields to sign
|
|
51
54
|
* @param privateKeyB64 - base64url ed25519 seed (see `ed25519KeyPairFromLibp2p`)
|
|
52
55
|
*/
|
|
53
56
|
export function signDeviceTokenRecord(
|
|
57
|
+
partyId: string,
|
|
54
58
|
fields: Omit<DeviceTokenRecord, 'sig'>,
|
|
55
59
|
privateKeyB64: string
|
|
56
60
|
): DeviceTokenRecord {
|
|
57
|
-
const payloadDigest = deviceTokenSignedPayload(fields);
|
|
61
|
+
const payloadDigest = deviceTokenSignedPayload(partyId, fields);
|
|
58
62
|
const sig = sign(
|
|
59
63
|
payloadDigest,
|
|
60
64
|
privateKeyB64,
|
|
@@ -68,15 +72,15 @@ export function signDeviceTokenRecord(
|
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* Verify a record's self-signature against the `CadrePeer.PublicKey` (base64url)
|
|
71
|
-
* bound to its `peerId`. Reconstructs the signed bytes from the record
|
|
72
|
-
* {@link signDeviceTokenRecord} produced them. Returns false on a missing
|
|
73
|
-
* any verification failure.
|
|
75
|
+
* bound to its `peerId`, for `partyId`. Reconstructs the signed bytes from the record
|
|
76
|
+
* exactly as {@link signDeviceTokenRecord} produced them. Returns false on a missing
|
|
77
|
+
* key/sig or any verification failure.
|
|
74
78
|
*/
|
|
75
|
-
export function verifyDeviceTokenSignature(record: DeviceTokenRecord, publicKeyB64: string): boolean {
|
|
79
|
+
export function verifyDeviceTokenSignature(partyId: string, record: DeviceTokenRecord, publicKeyB64: string): boolean {
|
|
76
80
|
if (!publicKeyB64 || !record.sig) {
|
|
77
81
|
return false;
|
|
78
82
|
}
|
|
79
|
-
const payloadDigest = deviceTokenSignedPayload(record);
|
|
83
|
+
const payloadDigest = deviceTokenSignedPayload(partyId, record);
|
|
80
84
|
return verify(
|
|
81
85
|
payloadDigest,
|
|
82
86
|
record.sig,
|
package/src/ed25519-key.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
2
2
|
import { getPublicKey } from '@optimystic/quereus-plugin-crypto';
|
|
3
|
+
import { peerIdFromPublicKey, peerIdFromString } from '@libp2p/peer-id';
|
|
4
|
+
import { publicKeyFromRaw } from '@libp2p/crypto/keys';
|
|
3
5
|
import type { PrivateKey } from '@libp2p/interface';
|
|
4
6
|
|
|
5
7
|
/**
|
|
@@ -68,12 +70,56 @@ export function ed25519PublicKeyFromPrivate(privateKeyB64: string): string {
|
|
|
68
70
|
return getPublicKey(privateKeyB64, 'ed25519', 'base64url', 'base64url') as string;
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Derive the base64url ed25519 public key embedded in an Ed25519 libp2p PeerId.
|
|
75
|
+
*
|
|
76
|
+
* An Ed25519 PeerId is an identity multihash of the public key, so
|
|
77
|
+
* `peerIdFromString(id).publicKey.raw` is the 32-byte ed25519 key whose
|
|
78
|
+
* base64url form matches the `OwnerKey.Key` representation (and
|
|
79
|
+
* `ed25519KeyPairFromLibp2p().publicKeyB64`). Returns null for a non-Ed25519
|
|
80
|
+
* id, a missing embedded key, or any parse failure — callers treat null as
|
|
81
|
+
* "not an owner" rather than throwing.
|
|
82
|
+
*
|
|
83
|
+
* Lives here, beside the other key bridges, so the lightweight verifiers in
|
|
84
|
+
* `peer-authorization.ts` can check a stored `PeerKey` against a row's `PeerId`
|
|
85
|
+
* without importing the seed-bootstrap runtime.
|
|
86
|
+
*/
|
|
87
|
+
export function ed25519PublicKeyB64FromPeerId(peerId: string): string | null {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = peerIdFromString(peerId);
|
|
90
|
+
if (parsed.type !== 'Ed25519' || !parsed.publicKey) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return uint8ArrayToString(parsed.publicKey.raw, 'base64url');
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The inverse of {@link ed25519PublicKeyB64FromPeerId}: the libp2p peer id whose identity
|
|
101
|
+
* is this ed25519 public key (base64url), or null when the value does not decode to a
|
|
102
|
+
* 32-byte key. How an `OwnerKey` row is matched to the machine it names
|
|
103
|
+
* (`machine-removal.ts`), since a machine's owner key is the key behind its peer id.
|
|
104
|
+
*/
|
|
105
|
+
export function peerIdFromEd25519PublicKeyB64(publicKeyB64: string): string | null {
|
|
106
|
+
try {
|
|
107
|
+
const raw = uint8ArrayFromString(publicKeyB64, 'base64url');
|
|
108
|
+
if (raw.length !== 32) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return peerIdFromPublicKey(publicKeyFromRaw(raw)).toString();
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
71
117
|
/** Longest rejected key value echoed back in an error — a real key is 43 base64url chars. */
|
|
72
118
|
const REJECTED_VALUE_ECHO_LIMIT = 64;
|
|
73
119
|
|
|
74
120
|
/**
|
|
75
121
|
* Render a rejected value for an error message, capped: owner keys now reach this check
|
|
76
|
-
* from remote-supplied fields (a
|
|
122
|
+
* from remote-supplied fields (a cadre invitation's `ownerKeys`, a donation request's
|
|
77
123
|
* `ownerKeys`), so an unbounded echo would let a peer turn one junk string into a
|
|
78
124
|
* megabyte of log line.
|
|
79
125
|
*/
|
|
@@ -95,15 +141,6 @@ function describeRejected(value: string): string {
|
|
|
95
141
|
* are a valid point on the Ed25519 curve — a well-formed-but-off-curve key still fails
|
|
96
142
|
* signature verification later exactly like any other wrong key.
|
|
97
143
|
*
|
|
98
|
-
* **This rule is restated once outside this package**, in `validateOwnerKey`
|
|
99
|
-
* (`packages/cadre-provider/src/server/owner-key-validation.ts`): cadre-provider declares no
|
|
100
|
-
* `workspace:` dependencies and validates `POST /containers` pins over `uint8arrays`
|
|
101
|
-
* directly. Change the rule here and change it there — the copies are kept in step by
|
|
102
|
-
* hand, since neither package can import the other's. Each side pins its own copy to the
|
|
103
|
-
* same accept/reject table (`test/ed25519-key.spec.ts` here,
|
|
104
|
-
* `create-container-owner-keys.test.ts` there), so a change fails the suite of the
|
|
105
|
-
* package it was made in and lands the editor on this pointer.
|
|
106
|
-
*
|
|
107
144
|
* @param value - Candidate base64url-encoded Ed25519 public key.
|
|
108
145
|
* @param label - Human-readable name of the field, used in error messages.
|
|
109
146
|
* @returns The trimmed value, so callers write the same bytes they validated.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `fetch` a cross-platform HTTP caller in this package uses: the one injected for tests
|
|
3
|
+
* and embedders, else the runtime's global. Shared by the formation-approval client and the
|
|
4
|
+
* provider client, so both resolve it the same way.
|
|
5
|
+
*
|
|
6
|
+
* Bound to `globalThis` because an unbound global `fetch` is an illegal invocation in browsers
|
|
7
|
+
* (and in any runtime whose `fetch` is a branded method rather than a plain function).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The injected `fetch`, else the bound global; undefined when the runtime has none. */
|
|
11
|
+
export function resolveFetch(fetchImpl?: typeof fetch): typeof fetch | undefined {
|
|
12
|
+
if (fetchImpl) return fetchImpl;
|
|
13
|
+
return typeof globalThis.fetch === 'function' ? globalThis.fetch.bind(globalThis) : undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** {@link resolveFetch}, throwing when the runtime has no `fetch` and none was injected. */
|
|
17
|
+
export function requireFetch(fetchImpl?: typeof fetch): typeof fetch {
|
|
18
|
+
const resolved = resolveFetch(fetchImpl);
|
|
19
|
+
if (!resolved) {
|
|
20
|
+
throw new Error('No fetch implementation: globalThis.fetch is missing and none was supplied');
|
|
21
|
+
}
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
package/src/file-durable-slot.ts
CHANGED
|
@@ -1,63 +1,64 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Node-only {@link DurableSlot} backend: one JSON file per party under `dir`,
|
|
3
|
-
* at `<dir>/<name>.<encoded partyId>.json` (0600, atomically replaced on every
|
|
4
|
-
* save), so one directory can hold the same node-local record for several
|
|
5
|
-
* parties without cross-party leakage.
|
|
6
|
-
*
|
|
7
|
-
* This is the whole platform-specific part of the file-backed node-local
|
|
8
|
-
* stores (`trusted-owner-store-file.ts`, `bootstrap-peer-store-file.ts`,
|
|
9
|
-
* `enrolled-machine-store-file.ts`); the load policy, envelope and write chain
|
|
10
|
-
* are cross-platform — in `node-local-snapshot.ts` for the first two, and in
|
|
11
|
-
* `enrolled-machine-store.ts` for the third, which deliberately does not share
|
|
12
|
-
* that machinery. Like its three importers, this module imports
|
|
13
|
-
* `node:fs/promises`, so it is kept OUT of the package's cross-platform
|
|
14
|
-
* default entry and is not itself an exported subpath.
|
|
15
|
-
*/
|
|
16
|
-
import { readFile } from 'node:fs/promises';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
import { randomBytes } from 'node:crypto';
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
private readonly
|
|
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
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Node-only {@link DurableSlot} backend: one JSON file per party under `dir`,
|
|
3
|
+
* at `<dir>/<name>.<encoded partyId>.json` (0600, atomically replaced on every
|
|
4
|
+
* save), so one directory can hold the same node-local record for several
|
|
5
|
+
* parties without cross-party leakage.
|
|
6
|
+
*
|
|
7
|
+
* This is the whole platform-specific part of the file-backed node-local
|
|
8
|
+
* stores (`trusted-owner-store-file.ts`, `bootstrap-peer-store-file.ts`,
|
|
9
|
+
* `enrolled-machine-store-file.ts`); the load policy, envelope and write chain
|
|
10
|
+
* are cross-platform — in `node-local-snapshot.ts` for the first two, and in
|
|
11
|
+
* `enrolled-machine-store.ts` for the third, which deliberately does not share
|
|
12
|
+
* that machinery. Like its three importers, this module imports
|
|
13
|
+
* `node:fs/promises`, so it is kept OUT of the package's cross-platform
|
|
14
|
+
* default entry and is not itself an exported subpath.
|
|
15
|
+
*/
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
import { isNotFound, writeFileAtomically } from './fs-atomic.js';
|
|
20
|
+
import { encodeFileSafeComponent } from './file-safe-name.js';
|
|
21
|
+
import type { DurableSlot } from './node-local-snapshot.js';
|
|
22
|
+
|
|
23
|
+
export class FileDurableSlot implements DurableSlot {
|
|
24
|
+
/** Filename-safe form of the party id, shared by the record and temp paths. */
|
|
25
|
+
private readonly encodedParty: string;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly dir: string,
|
|
29
|
+
private readonly name: string,
|
|
30
|
+
partyId: string
|
|
31
|
+
) {
|
|
32
|
+
this.encodedParty = encodeFileSafeComponent(partyId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The record's bytes as text. An absent file (ENOENT — including an absent
|
|
37
|
+
* directory) is `undefined`, a cold start. Every other read failure
|
|
38
|
+
* (EACCES, EISDIR, EIO, …) THROWS, per {@link DurableSlot.load}: the caller
|
|
39
|
+
* must not mistake an unreadable file for an empty one and snapshot-write
|
|
40
|
+
* over it.
|
|
41
|
+
*/
|
|
42
|
+
async load(): Promise<string | undefined> {
|
|
43
|
+
try {
|
|
44
|
+
return (await readFile(this.filePath())).toString('utf8');
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (isNotFound(error)) return undefined;
|
|
47
|
+
throw new Error(`FileDurableSlot: ${this.filePath()} is present but unreadable`, { cause: error });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async save(text: string): Promise<void> {
|
|
52
|
+
await writeFileAtomically(this.dir, this.tempPath(), this.filePath(), new TextEncoder().encode(text));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private filePath(): string {
|
|
56
|
+
return join(this.dir, `${this.name}.${this.encodedParty}.json`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Sibling temp path for the atomic replace (random component: collide-free). */
|
|
60
|
+
private tempPath(): string {
|
|
61
|
+
const unique = randomBytes(6).toString('hex');
|
|
62
|
+
return join(this.dir, `${this.name}.${this.encodedParty}.${unique}.tmp`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The filename-safe encoding of an id, shared by every Node-only file store that keys a file on
|
|
3
|
+
* a party or key id (`<name>.<encoded id>.json`). A pure string function, so it lives outside
|
|
4
|
+
* `fs-atomic.ts` and is exported from the cross-platform entry: an embedder that keeps its own
|
|
5
|
+
* per-party file beside cadre-core's (cadre-cli's removal marker) names it the same way.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Percent-encode a string into a filesystem-safe filename component. Beyond
|
|
10
|
+
* `encodeURIComponent` (which already escapes `/`, spaces, and unicode), this
|
|
11
|
+
* also escapes the unreserved characters `encodeURIComponent` leaves intact but
|
|
12
|
+
* that are unsafe in filenames on some platforms (notably `*` on Windows) — and
|
|
13
|
+
* `.`, so a file suffix appended by the caller is the only literal dot, making
|
|
14
|
+
* suffix-stripping unambiguous — and every uppercase letter, because Windows
|
|
15
|
+
* (NTFS) and macOS (APFS/HFS+ by default) ignore case in file names. Reversed by
|
|
16
|
+
* `decodeURIComponent`.
|
|
17
|
+
*
|
|
18
|
+
* The result contains only `a-z0-9-_` and `%XX` escapes (uppercase hex). Since a
|
|
19
|
+
* literal `%` is itself escaped, every `%` starts an escape, so ignoring case
|
|
20
|
+
* can change only escape hex digits — which decode case-insensitively — and two
|
|
21
|
+
* different inputs never yield names equal ignoring case. The escapes keep
|
|
22
|
+
* `encodeURIComponent`'s uppercase hex so a name built from an id with no
|
|
23
|
+
* capital letters (the identity slot `cadre%2Fidentity.key`, a lowercase party
|
|
24
|
+
* id) is the same as before uppercase letters were escaped.
|
|
25
|
+
*/
|
|
26
|
+
export function encodeFileSafeComponent(component: string): string {
|
|
27
|
+
// NOTE: a file left from before uppercase letters were escaped (`trusted-owners.Alice.json`) has, ignoring case, the new name of the all-lowercase id (`alice`), so on Windows/macOS a lookup for that id opens it; the per-party records' envelope party-id check rejects it (cold start), and `FileKeyStore` needs two parties in one key directory whose ids fold to the same lowercase string. The release note says to delete old files; if a store ever reads another id's old file in practice, rename or remove old-format files when the store opens.
|
|
28
|
+
return encodeURIComponent(component).replace(
|
|
29
|
+
/(%[0-9A-F]{2})|[!'()*~.A-Z]/g,
|
|
30
|
+
(c, escape: string | undefined) => escape ?? `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
|
31
|
+
);
|
|
32
|
+
}
|