@serfab/cadre-core 1.2.0 → 1.4.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 +5 -3
- package/dist/cadre-node.d.ts +32 -19
- package/dist/cadre-node.js +121 -31
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +4 -4
- package/dist/control-formation-recorder.js +6 -6
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +18 -5
- package/dist/membership-connection-gater.js +50 -10
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/storage-scope.d.ts +50 -12
- package/dist/storage-scope.js +85 -12
- package/dist/storage-scope.js.map +1 -1
- package/dist/strand-formation-manager.js +2 -1
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-id.d.ts +35 -0
- package/dist/strand-id.js +46 -0
- package/dist/strand-id.js.map +1 -0
- package/dist/strand-instance-manager.js +20 -2
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-network-config.d.ts +3 -3
- package/dist/strand-network-config.js +3 -3
- package/dist/strand-solicitation.js +2 -1
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +45 -0
- package/dist/strand-watcher.js +74 -13
- package/dist/strand-watcher.js.map +1 -1
- package/dist/types.d.ts +118 -15
- package/dist/types.js +59 -0
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/cadre-node.ts +127 -31
- package/src/control-formation-recorder.ts +6 -6
- package/src/index.ts +563 -554
- package/src/membership-connection-gater.ts +56 -11
- package/src/storage-scope.ts +163 -78
- package/src/strand-formation-manager.ts +647 -646
- package/src/strand-id.ts +48 -0
- package/src/strand-instance-manager.ts +21 -2
- package/src/strand-network-config.ts +250 -250
- package/src/strand-solicitation.ts +500 -499
- package/src/strand-watcher.ts +388 -325
- package/src/types.ts +121 -15
|
@@ -88,9 +88,10 @@
|
|
|
88
88
|
* annex the party's relay capacity.
|
|
89
89
|
* - An `'admit-for-relay'` connection that is NOT reserving is dropped: it has
|
|
90
90
|
* {@link RELAY_ADMISSION_RESERVE_DEADLINE_MS} to get a reservation ADMITTED
|
|
91
|
-
* at that hook, after which the underlying connection
|
|
92
|
-
*
|
|
93
|
-
* conversation" to "in it briefly,
|
|
91
|
+
* at that hook, after which the gate CLOSES the underlying connection so
|
|
92
|
+
* both ends let go of the socket. The guarantee above thus weakens on
|
|
93
|
+
* relay-enabled nodes from "never in the conversation" to "in it briefly,
|
|
94
|
+
* and can speak nothing".
|
|
94
95
|
*
|
|
95
96
|
* The deadline clears on reservation ADMISSION, not on the reservation's own
|
|
96
97
|
* success: this gate cannot observe the server-side `reserve()` outcome, so a
|
|
@@ -158,6 +159,7 @@ import debug from 'debug';
|
|
|
158
159
|
import type { ConnectionGater, PeerId, MultiaddrConnection } from '@libp2p/interface';
|
|
159
160
|
import { SEED_PROTOCOL } from './seed-bootstrap.js';
|
|
160
161
|
import { FORMATION_PROTOCOL } from './strand-formation-protocol.js';
|
|
162
|
+
import { withDeadline } from './control-stream.js';
|
|
161
163
|
|
|
162
164
|
const log = debug('sereus:cadre:connection-gater');
|
|
163
165
|
|
|
@@ -197,7 +199,7 @@ export const ADMISSION_DECISION_TIMEOUT_MS = 2_000;
|
|
|
197
199
|
/**
|
|
198
200
|
* How long an `'admit-for-relay'` connection may exist without a relay
|
|
199
201
|
* reservation being ADMITTED at the `denyInboundRelayReservation` hook, after
|
|
200
|
-
* which the gate
|
|
202
|
+
* which the gate closes the underlying connection. A reserving client asks for
|
|
201
203
|
* its slot immediately after the connection upgrades (`relay-reservation.ts`
|
|
202
204
|
* dials and requests in one drive; a strand node's configured circuit listener
|
|
203
205
|
* does the same from inside its own `listen()`), so a connection idle past this
|
|
@@ -205,6 +207,19 @@ export const ADMISSION_DECISION_TIMEOUT_MS = 2_000;
|
|
|
205
207
|
*/
|
|
206
208
|
export const RELAY_ADMISSION_RESERVE_DEADLINE_MS = 5_000;
|
|
207
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Bound on the graceful close of an expired relay-only connection. It exists
|
|
212
|
+
* because `AbstractMultiaddrConnection.close()` awaits an `idle`/`drain` event
|
|
213
|
+
* when the connection still has unsent bytes, and that wait has no timeout of
|
|
214
|
+
* its own — an unsignalled one never ends. This gate writes nothing to a
|
|
215
|
+
* stranger, so the wait is not reachable today; the bound is here so a timer
|
|
216
|
+
* callback can never hold an unending await.
|
|
217
|
+
*
|
|
218
|
+
* Applied through {@link withDeadline}, not `AbortSignal.timeout`: the latter is
|
|
219
|
+
* not reliably present on React Native/Hermes, which loads this same module.
|
|
220
|
+
*/
|
|
221
|
+
export const RELAY_ADMISSION_CLOSE_TIMEOUT_MS = 2_000;
|
|
222
|
+
|
|
208
223
|
/**
|
|
209
224
|
* Default cap on concurrent relay reservations held by peers the membership
|
|
210
225
|
* check could not place (see {@link UnauthorizedReservationBudget}). Small on
|
|
@@ -351,7 +366,7 @@ export class UnauthorizedReservationBudget {
|
|
|
351
366
|
*
|
|
352
367
|
* An `'admit-for-relay'` verdict admits the connection and arms a
|
|
353
368
|
* `reserveDeadlineMs` timer against it; the timer is disarmed when a
|
|
354
|
-
* reservation for that peer is ADMITTED at the reservation hook, and
|
|
369
|
+
* reservation for that peer is ADMITTED at the reservation hook, and closes the
|
|
355
370
|
* underlying `MultiaddrConnection` when it fires first.
|
|
356
371
|
*
|
|
357
372
|
* NOTE: `base` is spread, so a gater passed as a CLASS INSTANCE would lose its
|
|
@@ -470,7 +485,7 @@ export async function decideWithinDeadline<T>(
|
|
|
470
485
|
}
|
|
471
486
|
}
|
|
472
487
|
|
|
473
|
-
/** One armed not-reserving deadline: the connection it will
|
|
488
|
+
/** One armed not-reserving deadline: the connection it will close, and its timer. */
|
|
474
489
|
interface PendingReserveDeadline {
|
|
475
490
|
maConn: MultiaddrConnection;
|
|
476
491
|
timer: ReturnType<typeof setTimeout>;
|
|
@@ -479,11 +494,11 @@ interface PendingReserveDeadline {
|
|
|
479
494
|
/**
|
|
480
495
|
* The not-reserving deadlines for `'admit-for-relay'` connections, keyed by
|
|
481
496
|
* remote peerId (a Set per peer — one peer can hold several in-flight
|
|
482
|
-
* connections). `arm` starts a timer that
|
|
497
|
+
* connections). `arm` starts a timer that closes the connection; `disarm`
|
|
483
498
|
* (called when a reservation for that peer is admitted) cancels every pending
|
|
484
499
|
* timer for the peer. Timers are unref'd so an armed deadline never holds a
|
|
485
500
|
* process open, and a timer that fires against an already-closed connection is
|
|
486
|
-
* a
|
|
501
|
+
* a no-op (`close()` returns at once on any status but `open`).
|
|
487
502
|
*
|
|
488
503
|
* Disarming is final for the connections it cancelled: a peer that reserved
|
|
489
504
|
* once and then lets its reservation lapse keeps a mute connection (the
|
|
@@ -528,11 +543,41 @@ class PendingReserveDeadlines {
|
|
|
528
543
|
if (entries?.size === 0) {
|
|
529
544
|
this.byPeer.delete(remotePeerId);
|
|
530
545
|
}
|
|
531
|
-
log('Relay-only admission expired for %s — no reservation admitted within %dms,
|
|
546
|
+
log('Relay-only admission expired for %s — no reservation admitted within %dms, closing the connection', remotePeerId, this.deadlineMs);
|
|
547
|
+
void this.drop(remotePeerId, entry.maConn);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* End an expired relay-only connection: a bounded graceful close, with
|
|
552
|
+
* `abort()` only as the escalation. The order cannot be reversed — `abort()`
|
|
553
|
+
* marks the connection `aborted`, and `close()` returns immediately on any
|
|
554
|
+
* status that is not `open`, so aborting first would make the close a silent
|
|
555
|
+
* no-op. Closing a connection the peer already dropped returns at once
|
|
556
|
+
* without throwing, so the fallback stays unreached in the ordinary case.
|
|
557
|
+
*/
|
|
558
|
+
private async drop(remotePeerId: string, maConn: MultiaddrConnection): Promise<void> {
|
|
559
|
+
try {
|
|
560
|
+
await withDeadline(
|
|
561
|
+
RELAY_ADMISSION_CLOSE_TIMEOUT_MS,
|
|
562
|
+
`relay-only close for ${remotePeerId}`,
|
|
563
|
+
(signal) => maConn.close({ signal })
|
|
564
|
+
);
|
|
565
|
+
return;
|
|
566
|
+
} catch (error) {
|
|
567
|
+
log('Closing the expired relay-only connection from %s failed — aborting: %o', remotePeerId, error);
|
|
568
|
+
}
|
|
569
|
+
// NOTE: this fallback cannot actually free a WebSocket. `@libp2p/websockets`'
|
|
570
|
+
// `sendReset()` calls `websocket.close(1006)`, and 1006 is a reserved code
|
|
571
|
+
// RFC 6455 forbids an endpoint from sending, so `ws` throws and
|
|
572
|
+
// `AbstractMessageStream.abort()` swallows it — the local end flips to
|
|
573
|
+
// `aborted` while the socket stays up. Measured against
|
|
574
|
+
// @libp2p/websockets@10.1.3 (see tickets/blocked/report-libp2p-websockets-abort-close-code.md).
|
|
575
|
+
// `MultiaddrConnection` exposes no way down to the raw socket, so this is as
|
|
576
|
+
// far as this layer can go. Revisit — drop the fallback, or go back to a
|
|
577
|
+
// plain `abort()` — once that transport sends a legal reset code.
|
|
532
578
|
try {
|
|
533
|
-
|
|
579
|
+
maConn.abort(new Error(`relay-only admission expired: no relay reservation admitted within ${this.deadlineMs}ms`));
|
|
534
580
|
} catch (error) {
|
|
535
|
-
// Aborting a connection that already closed on its own is not a failure.
|
|
536
581
|
log('Aborting the expired relay-only connection from %s threw: %o', remotePeerId, error);
|
|
537
582
|
}
|
|
538
583
|
}
|
package/src/storage-scope.ts
CHANGED
|
@@ -1,78 +1,163 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* storage-scope.ts — the scope keys `CadreNodeConfig.storage.provider` is called with.
|
|
3
|
-
*
|
|
4
|
-
* A provider is a factory cadre-core invokes once per **scope**: once for the control
|
|
5
|
-
* database and once for each strand. A strand's scope key is its strand id; the control
|
|
6
|
-
* database's scope key is what this module mints.
|
|
7
|
-
*
|
|
8
|
-
* WHY THE CONTROL SCOPE CARRIES THE PARTY ID. The control database holds the party's
|
|
9
|
-
* own records — its strands, owner keys, peers, invitations, revocations. Two parties
|
|
10
|
-
* on one device must therefore not share one control store, or a node started for
|
|
11
|
-
* party B reads party A's rows as if party A had written them for B. Every other
|
|
12
|
-
* party-scoped store in this codebase already knows this: `PersistentTrustedOwnerStore`,
|
|
13
|
-
* `PersistentBootstrapPeerStore` and `PersistentEnrolledMachineStore` each take a
|
|
14
|
-
* `partyId` and fail closed on a mismatch. Putting the party id in the key here means
|
|
15
|
-
* no embedder has to know it belongs there, and none can get it wrong.
|
|
16
|
-
*
|
|
17
|
-
* WHY IT IS ENCODED. A party id is arbitrary text — nothing in cadre-core validates
|
|
18
|
-
* its shape, and the React Native reference app lets a user type one into Settings.
|
|
19
|
-
* Scope keys reach real namespaces unescaped: cadre-cli builds `${config.path}/${scope}`,
|
|
20
|
-
* React Native a LevelDB filename, the browser an IndexedDB database name. A party id
|
|
21
|
-
* containing `/` or `..` would escape the CLI's storage directory. Base64url keeps every
|
|
22
|
-
* key inside `[A-Za-z0-9._-]` — the charset embedders already rely on, and the same
|
|
23
|
-
* encoding the React Native app's `secureStoreKeySegment` uses on a party id for the
|
|
24
|
-
* same reason.
|
|
25
|
-
*
|
|
26
|
-
* THE STRAND ARM OF THAT CHARSET RULE IS
|
|
27
|
-
* `StrandRow.Id`, and
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* provider.
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* storage-scope.ts — the scope keys `CadreNodeConfig.storage.provider` is called with.
|
|
3
|
+
*
|
|
4
|
+
* A provider is a factory cadre-core invokes once per **scope**: once for the control
|
|
5
|
+
* database and once for each strand. A strand's scope key is its strand id; the control
|
|
6
|
+
* database's scope key is what this module mints.
|
|
7
|
+
*
|
|
8
|
+
* WHY THE CONTROL SCOPE CARRIES THE PARTY ID. The control database holds the party's
|
|
9
|
+
* own records — its strands, owner keys, peers, invitations, revocations. Two parties
|
|
10
|
+
* on one device must therefore not share one control store, or a node started for
|
|
11
|
+
* party B reads party A's rows as if party A had written them for B. Every other
|
|
12
|
+
* party-scoped store in this codebase already knows this: `PersistentTrustedOwnerStore`,
|
|
13
|
+
* `PersistentBootstrapPeerStore` and `PersistentEnrolledMachineStore` each take a
|
|
14
|
+
* `partyId` and fail closed on a mismatch. Putting the party id in the key here means
|
|
15
|
+
* no embedder has to know it belongs there, and none can get it wrong.
|
|
16
|
+
*
|
|
17
|
+
* WHY IT IS ENCODED. A party id is arbitrary text — nothing in cadre-core validates
|
|
18
|
+
* its shape, and the React Native reference app lets a user type one into Settings.
|
|
19
|
+
* Scope keys reach real namespaces unescaped: cadre-cli builds `${config.path}/${scope}`,
|
|
20
|
+
* React Native a LevelDB filename, the browser an IndexedDB database name. A party id
|
|
21
|
+
* containing `/` or `..` would escape the CLI's storage directory. Base64url keeps every
|
|
22
|
+
* key inside `[A-Za-z0-9._-]` — the charset embedders already rely on, and the same
|
|
23
|
+
* encoding the React Native app's `secureStoreKeySegment` uses on a party id for the
|
|
24
|
+
* same reason.
|
|
25
|
+
*
|
|
26
|
+
* THE STRAND ARM OF THAT CHARSET RULE IS ENFORCED HERE. A strand's scope key is
|
|
27
|
+
* `StrandRow.Id`, and a strand row replicated into the control database by another
|
|
28
|
+
* node in the party carries whatever id THAT node wrote. `assertStrandScopeKey`
|
|
29
|
+
* (below) is the check that makes the charset true of strand keys too; it runs
|
|
30
|
+
* unconditionally at the top of `StrandInstanceManager.startStrand`, so no strand id
|
|
31
|
+
* reaches an embedder's provider — or a libp2p protocol prefix — unvalidated. Ids
|
|
32
|
+
* cadre-core mints come from `strand-id.ts`, which is built on the same predicate.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Prefix marking a scope key as a control database rather than a strand.
|
|
39
|
+
*
|
|
40
|
+
* Not exported: `controlStorageScope` mints these keys and `isControlStorageScope`
|
|
41
|
+
* recognizes them, which is the whole surface a caller needs.
|
|
42
|
+
*/
|
|
43
|
+
const CONTROL_SCOPE_PREFIX = 'control-';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The storage scope key for a party's control database.
|
|
47
|
+
*
|
|
48
|
+
* Returns `control-` followed by the base64url encoding of `partyId`'s UTF-8 bytes,
|
|
49
|
+
* so the whole key stays within `[A-Za-z0-9._-]` and is safe to use directly as a
|
|
50
|
+
* file name, directory name or database name. See the module comment for why the
|
|
51
|
+
* encoding is load-bearing rather than decorative.
|
|
52
|
+
*
|
|
53
|
+
* To read a party id back off a device — from, say, a LevelDB file named
|
|
54
|
+
* `sereus-control-MTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1` — in a browser or
|
|
55
|
+
* Node console, with no dependency on this package:
|
|
56
|
+
* ```js
|
|
57
|
+
* const b64 = key.slice('control-'.length).replace(/-/g, '+').replace(/_/g, '/');
|
|
58
|
+
* new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)))
|
|
59
|
+
* ```
|
|
60
|
+
* The `TextDecoder` step is not optional: `atob` alone yields one character per BYTE,
|
|
61
|
+
* so a party id with any non-ASCII character in it decodes to mojibake.
|
|
62
|
+
*/
|
|
63
|
+
export function controlStorageScope(partyId: string): string {
|
|
64
|
+
return CONTROL_SCOPE_PREFIX + uint8ArrayToString(uint8ArrayFromString(partyId, 'utf8'), 'base64url');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether a scope key names a control database rather than a strand.
|
|
69
|
+
*
|
|
70
|
+
* A prefix test, and a sound one: a strand scope key can never begin `control-`,
|
|
71
|
+
* because {@link isValidStrandScopeKey} rejects that prefix and
|
|
72
|
+
* {@link assertStrandScopeKey} runs on every strand launch.
|
|
73
|
+
*/
|
|
74
|
+
export function isControlStorageScope(scope: string): boolean {
|
|
75
|
+
return scope.startsWith(CONTROL_SCOPE_PREFIX);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The longest strand scope key accepted.
|
|
80
|
+
*
|
|
81
|
+
* The longest id cadre-core mints is 39 characters (`strand-` plus the 32 hex
|
|
82
|
+
* characters of `randomBytes(128, 'hex')` — 128 BITS, so 16 bytes), so this leaves
|
|
83
|
+
* ample room while keeping a key clear of the 255-BYTE limit every mainstream
|
|
84
|
+
* filesystem puts on one path component. The charset is ASCII, so characters and
|
|
85
|
+
* bytes are the same count here.
|
|
86
|
+
*/
|
|
87
|
+
const MAX_STRAND_SCOPE_KEY_LENGTH = 128;
|
|
88
|
+
|
|
89
|
+
/** The charset every scope key stays within — see the module comment. */
|
|
90
|
+
const SCOPE_KEY_CHARSET = /^[A-Za-z0-9._-]+$/;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Whether a strand id is usable as a storage scope key and as a libp2p network name.
|
|
94
|
+
*
|
|
95
|
+
* A strand id is not minted locally in the general case — a strand row replicates in
|
|
96
|
+
* from another node in the party carrying whatever id THAT node wrote — and both
|
|
97
|
+
* places it lands turn it straight into a name: the embedder's storage provider
|
|
98
|
+
* (a directory under cadre-cli's storage path, a LevelDB filename, an IndexedDB
|
|
99
|
+
* database name) and the strand node's protocol prefix `/optimystic/strand-<id>`.
|
|
100
|
+
*
|
|
101
|
+
* Valid means all of:
|
|
102
|
+
* - non-empty, and at most {@link MAX_STRAND_SCOPE_KEY_LENGTH} characters;
|
|
103
|
+
* - within `[A-Za-z0-9._-]`, so no separator, drive letter, NUL or non-ASCII text;
|
|
104
|
+
* - not `.` or `..`, which the charset admits but every filesystem reads as a
|
|
105
|
+
* directory rather than a name;
|
|
106
|
+
* - not `control-`-prefixed, which would let a strand's store masquerade as a
|
|
107
|
+
* party's control database (see {@link isControlStorageScope}).
|
|
108
|
+
*/
|
|
109
|
+
export function isValidStrandScopeKey(strandId: string): boolean {
|
|
110
|
+
return strandId.length > 0
|
|
111
|
+
&& strandId.length <= MAX_STRAND_SCOPE_KEY_LENGTH
|
|
112
|
+
&& SCOPE_KEY_CHARSET.test(strandId)
|
|
113
|
+
&& strandId !== '.'
|
|
114
|
+
&& strandId !== '..'
|
|
115
|
+
&& !isControlStorageScope(strandId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Thrown when a strand id cannot be used as a storage scope key or network name.
|
|
120
|
+
*
|
|
121
|
+
* NOT retryable: the id is a property of the strand row, so every later attempt on
|
|
122
|
+
* the same row fails identically. Callers that poll — `CadreNode.handleStrandAdded`
|
|
123
|
+
* via `StrandWatcher` — suppress the strand rather than scheduling a retry.
|
|
124
|
+
*/
|
|
125
|
+
export class InvalidStrandIdError extends Error {
|
|
126
|
+
readonly strandId: string;
|
|
127
|
+
|
|
128
|
+
constructor(strandId: string) {
|
|
129
|
+
super(
|
|
130
|
+
`Strand id ${JSON.stringify(strandId)} cannot be used as a storage scope key: ` +
|
|
131
|
+
`a strand id must be 1-${MAX_STRAND_SCOPE_KEY_LENGTH} characters within [A-Za-z0-9._-], ` +
|
|
132
|
+
"must not be '.' or '..', and must not begin 'control-'. " +
|
|
133
|
+
'This strand was created by a node that does not mint conforming ids; it cannot be ' +
|
|
134
|
+
'started here, because the id becomes a file, directory or database name.'
|
|
135
|
+
);
|
|
136
|
+
this.name = 'InvalidStrandIdError';
|
|
137
|
+
this.strandId = strandId;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** {@link isValidStrandScopeKey} as an assertion, throwing {@link InvalidStrandIdError}. */
|
|
142
|
+
export function assertStrandScopeKey(strandId: string): void {
|
|
143
|
+
if (!isValidStrandScopeKey(strandId)) {
|
|
144
|
+
throw new InvalidStrandIdError(strandId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The charset half of the rule on its own, for the CONTROL key.
|
|
150
|
+
*
|
|
151
|
+
* {@link assertStrandScopeKey} cannot serve here: it rejects the `control-` prefix by
|
|
152
|
+
* design. Asserted at the seam that calls the provider (`CadreNode.resolveControlStorage`)
|
|
153
|
+
* rather than inside {@link controlStorageScope}, so the guarantee belongs to the call
|
|
154
|
+
* that hands a key out and not to one particular way of minting one.
|
|
155
|
+
*/
|
|
156
|
+
export function assertScopeKeyCharset(scope: string): void {
|
|
157
|
+
if (!SCOPE_KEY_CHARSET.test(scope)) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Storage scope key ${JSON.stringify(scope)} leaves the [A-Za-z0-9._-] charset ` +
|
|
160
|
+
'embedders rely on to use it directly as a file, directory or database name.'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|