@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/control-schema.ts
CHANGED
|
@@ -368,9 +368,15 @@ declare schema CadreControl {
|
|
|
368
368
|
-- can pollute.
|
|
369
369
|
VouchUsage text null, -- the CadreInviteUsage.UsageStampId that admitted this row; null on an
|
|
370
370
|
-- owner-vouched row. A row carries exactly one proof: a signature or a usage.
|
|
371
|
+
Profile text null, -- the machine's own node profile (cadre-core NodeProfile): 'transaction' is an
|
|
372
|
+
-- app (phone, browser), 'storage' an always-on node; null until it self-publishes.
|
|
373
|
+
-- Self-declared and covered by Sig. No insert rule constrains it: an owner could
|
|
374
|
+
-- only mislabel a machine it already has full authority over, and that machine's
|
|
375
|
+
-- next self-publish corrects it.
|
|
371
376
|
constraint NotRevoked check on insert (
|
|
372
377
|
not exists (select 1 from Revocation R where R.TableName = 'CadrePeer' and R.StampId = new.StampId)
|
|
373
378
|
),
|
|
379
|
+
constraint KnownProfile check (new.Profile is null or new.Profile in ('transaction', 'storage')),
|
|
374
380
|
constraint RevocationRecorded check on delete (
|
|
375
381
|
exists (select 1 from Revocation R where R.TableName = 'CadrePeer' and R.StampId = old.StampId and R.RowKey = old.PeerId)
|
|
376
382
|
),
|
|
@@ -414,10 +420,16 @@ declare schema CadreControl {
|
|
|
414
420
|
exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and not exists (select 1 from Revocation R where R.TableName = 'OwnerKey' and R.StampId = A.StampId) and verify(digest('CadreControl.CadrePeer', 'remove', party_id(), old.PeerId, old.StampId), context.Signature, A.Key, 'ed25519'))
|
|
415
421
|
or exists (select 1 from committed.Revocation R where R.TableName = 'CadrePeer' and R.RowKey = old.PeerId and R.StampId = old.StampId)
|
|
416
422
|
),
|
|
417
|
-
-- A peer re-publishes its own addrs and freshness under its own key (cadre-core
|
|
423
|
+
-- A peer re-publishes its own addrs, profile and freshness under its own key (cadre-core
|
|
418
424
|
-- peer-record.ts); everything else is immutable and UpdatedAt must increase. Or an owner
|
|
419
425
|
-- re-vouches the row over its current stamp, which also clears VouchUsage: the row then
|
|
420
|
-
-- stands on the signature and its invitation is no longer consulted for it.
|
|
426
|
+
-- stands on the signature and its invitation is no longer consulted for it. The owner
|
|
427
|
+
-- branch keeps what the machine itself declares (PublicKey, Multiaddr, Sig, Profile): the
|
|
428
|
+
-- row's stored VouchSig is a valid re-vouch signature, so any machine holding the row can
|
|
429
|
+
-- take this branch. NOTE: it may still bump UpdatedAt (reauthorizeCadrePeer's re-broadcast
|
|
430
|
+
-- needs it), which on a self-published row leaves Sig unverifiable until the machine's
|
|
431
|
+
-- next heartbeat re-signs past it; if that window ever matters, pin UpdatedAt here when
|
|
432
|
+
-- old.Sig is not null. StampId never
|
|
421
433
|
-- changes on update: an update that rotated it would leave the old stamp free and
|
|
422
434
|
-- untombstoned, so retiring a stamp is delete plus tombstone. The proof columns are
|
|
423
435
|
-- compared null-safe: VouchSig and VouchUsage are null on one of the two row kinds, and a
|
|
@@ -432,7 +444,7 @@ declare schema CadreControl {
|
|
|
432
444
|
and coalesce(new.VouchUsage, '') = coalesce(old.VouchUsage, '')
|
|
433
445
|
and new.UpdatedAt > coalesce(old.UpdatedAt, 0)
|
|
434
446
|
and verify(
|
|
435
|
-
digest('CadreControl.CadrePeer', 'publish', party_id(), new.PeerId, new.Multiaddr, cast(new.UpdatedAt as text)),
|
|
447
|
+
digest('CadreControl.CadrePeer', 'publish', party_id(), new.PeerId, new.Multiaddr, cast(new.UpdatedAt as text), coalesce(new.Profile, '')),
|
|
436
448
|
new.Sig, new.PublicKey, 'ed25519')
|
|
437
449
|
)
|
|
438
450
|
or (
|
|
@@ -441,6 +453,10 @@ declare schema CadreControl {
|
|
|
441
453
|
and coalesce(new.VouchOwner, '') = context.OwnerKey
|
|
442
454
|
and coalesce(new.VouchSig, '') = context.Signature
|
|
443
455
|
and new.VouchUsage is null
|
|
456
|
+
and coalesce(new.PublicKey, '') = coalesce(old.PublicKey, '')
|
|
457
|
+
and coalesce(new.Multiaddr, '') = coalesce(old.Multiaddr, '')
|
|
458
|
+
and coalesce(new.Sig, '') = coalesce(old.Sig, '')
|
|
459
|
+
and coalesce(new.Profile, '') = coalesce(old.Profile, '')
|
|
444
460
|
)
|
|
445
461
|
)
|
|
446
462
|
) with context (OwnerKey text null, Signature text);
|
package/src/ed25519-key.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
2
2
|
import { getPublicKey } from '@optimystic/quereus-plugin-crypto';
|
|
3
|
-
import { peerIdFromString } from '@libp2p/peer-id';
|
|
3
|
+
import { peerIdFromPublicKey, peerIdFromString } from '@libp2p/peer-id';
|
|
4
|
+
import { publicKeyFromRaw } from '@libp2p/crypto/keys';
|
|
4
5
|
import type { PrivateKey } from '@libp2p/interface';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -95,6 +96,24 @@ export function ed25519PublicKeyB64FromPeerId(peerId: string): string | null {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
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
|
+
|
|
98
117
|
/** Longest rejected key value echoed back in an error — a real key is 43 base64url chars. */
|
|
99
118
|
const REJECTED_VALUE_ECHO_LIMIT = 64;
|
|
100
119
|
|
|
@@ -122,15 +141,6 @@ function describeRejected(value: string): string {
|
|
|
122
141
|
* are a valid point on the Ed25519 curve — a well-formed-but-off-curve key still fails
|
|
123
142
|
* signature verification later exactly like any other wrong key.
|
|
124
143
|
*
|
|
125
|
-
* **This rule is restated once outside this package**, in `validateOwnerKey`
|
|
126
|
-
* (`packages/cadre-provider/src/server/owner-key-validation.ts`): cadre-provider declares no
|
|
127
|
-
* `workspace:` dependencies and validates `POST /containers` pins over `uint8arrays`
|
|
128
|
-
* directly. Change the rule here and change it there — the copies are kept in step by
|
|
129
|
-
* hand, since neither package can import the other's. Each side pins its own copy to the
|
|
130
|
-
* same accept/reject table (`test/ed25519-key.spec.ts` here,
|
|
131
|
-
* `create-container-owner-keys.test.ts` there), so a change fails the suite of the
|
|
132
|
-
* package it was made in and lands the editor on this pointer.
|
|
133
|
-
*
|
|
134
144
|
* @param value - Candidate base64url-encoded Ed25519 public key.
|
|
135
145
|
* @param label - Human-readable name of the field, used in error messages.
|
|
136
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
|
+
}
|