@le-space/aleph-bootstrap 0.9.3 → 0.9.5
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 +37 -0
- package/index.d.ts +57 -2
- package/index.js +32 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,9 @@ It is designed for two complementary jobs:
|
|
|
12
12
|
|
|
13
13
|
- `discoverAlephBootstrapMultiaddrs(options)`
|
|
14
14
|
- `createLibp2pAlephBootstrap(options)`
|
|
15
|
+
- `filterRelayBootstrapPostsByProfile(posts, profile?)`
|
|
16
|
+
- `unsupportedRelayBootstrapReason(post)`
|
|
17
|
+
- `filterRelayBootstrapPostsByRegistration(posts, registrationId?)`
|
|
15
18
|
- `filterPublicMultiaddrs(addrs, options?)`
|
|
16
19
|
- `createRelayBootstrapPost(options)`
|
|
17
20
|
- `signRelayBootstrapAuthorization(args)`
|
|
@@ -45,6 +48,40 @@ By default, discovery will:
|
|
|
45
48
|
- verify dual-key records when they are present
|
|
46
49
|
- ignore malformed or invalid dual-key records
|
|
47
50
|
|
|
51
|
+
## Scoping discovery
|
|
52
|
+
|
|
53
|
+
The channel is shared. Several relay implementations register in it, and so
|
|
54
|
+
does every throwaway relay an E2E run starts — under the same profile as the
|
|
55
|
+
production one, with a registration that outlives the machine it describes,
|
|
56
|
+
because guests self-publish with generated keys and no remaining key can
|
|
57
|
+
FORGET the post.
|
|
58
|
+
|
|
59
|
+
Two scopes, and most consumers want both:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const list = await discoverAlephBootstrapMultiaddrs({
|
|
63
|
+
profile: 'orbitdb-relay', // not uc-go-peer's relays
|
|
64
|
+
registrationId: 'relay:orbitdb-relay:orbitdb-relay' // and not the E2E ones
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`profile` keeps out relays a consumer cannot use at all — an orbitdb app that
|
|
69
|
+
dials a `uc-go-peer` relay never gets a shared circuit and sits at
|
|
70
|
+
`candidates: 0`. `registrationId` keeps out its own corpses: measured
|
|
71
|
+
downstream, a browser probe wave against an unscoped list spent its outbound
|
|
72
|
+
stream budget on dead addresses and wrote off the one healthy relay along with
|
|
73
|
+
them.
|
|
74
|
+
|
|
75
|
+
A record this package cannot use — a legacy v1 post, or a type it did not ask
|
|
76
|
+
for — is **skipped, not fatal**. One such record on a public, append-only
|
|
77
|
+
channel would otherwise blind discovery for every consumer, permanently, and
|
|
78
|
+
nobody can FORGET somebody else's post. Pass `onUnsupportedPost` to see what
|
|
79
|
+
was skipped and why.
|
|
80
|
+
|
|
81
|
+
Use `registrationId` for anything that bakes addresses into a build or dials
|
|
82
|
+
them on start. Omit both to see the whole channel, which is what a dashboard
|
|
83
|
+
wants.
|
|
84
|
+
|
|
48
85
|
If a consumer wants to require the stronger model:
|
|
49
86
|
|
|
50
87
|
```ts
|
package/index.d.ts
CHANGED
|
@@ -103,6 +103,24 @@ interface DiscoverAlephBootstrapOptions {
|
|
|
103
103
|
* When omitted, posts of every profile are returned (previous behaviour).
|
|
104
104
|
*/
|
|
105
105
|
profile?: string | readonly string[];
|
|
106
|
+
/**
|
|
107
|
+
* Restrict discovery further, to the registrations published under this id
|
|
108
|
+
* (e.g. `"relay:orbitdb-relay:orbitdb-relay"`). Profile is not fine enough:
|
|
109
|
+
* an E2E run's throwaway relays register under the same profile as the
|
|
110
|
+
* production one, and a registration outlives the machine it describes. Use
|
|
111
|
+
* this for anything that bakes addresses into a build or dials them on
|
|
112
|
+
* start; omit it to see the whole channel.
|
|
113
|
+
*/
|
|
114
|
+
registrationId?: string | readonly string[];
|
|
115
|
+
/**
|
|
116
|
+
* Called for each record discovery had to skip. Optional, and the default of
|
|
117
|
+
* doing nothing is deliberate — a skipped record is not an error for the
|
|
118
|
+
* consumer — but a consumer whose relay went missing wants to see this.
|
|
119
|
+
*/
|
|
120
|
+
onUnsupportedPost?: (skipped: {
|
|
121
|
+
hash: string | null | undefined;
|
|
122
|
+
reason: string;
|
|
123
|
+
}) => void;
|
|
106
124
|
fetch?: typeof fetch;
|
|
107
125
|
}
|
|
108
126
|
interface FilterPublicMultiaddrsOptions {
|
|
@@ -211,7 +229,24 @@ declare function verifyRelayBootstrapDualKeyContent(content: RelayBootstrapConte
|
|
|
211
229
|
now?: number;
|
|
212
230
|
}): Promise<RelayBootstrapVerificationResult>;
|
|
213
231
|
declare function fetchAlephBootstrapPosts(options?: DiscoverAlephBootstrapOptions): Promise<RelayBootstrapPostRecord[]>;
|
|
214
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Why this post cannot be used, or `null` if it can.
|
|
234
|
+
*
|
|
235
|
+
* This used to throw, and one record of the wrong type took the whole page
|
|
236
|
+
* with it. The channel is public and append-only: anybody with an ETH key can
|
|
237
|
+
* publish into it, nobody can FORGET somebody else's post, and every consumer
|
|
238
|
+
* polls the same page. So a single legacy record — not ours, not removable —
|
|
239
|
+
* would blind discovery for everyone, permanently.
|
|
240
|
+
*
|
|
241
|
+
* The Rust client learned this the same way and changed the same shape: after
|
|
242
|
+
* a production incident on 2026-08-30, `get_messages_iterator` was page-strict
|
|
243
|
+
* and one message the validator rejected failed every poll, leaving the
|
|
244
|
+
* scheduler unable to see new v-programs at all (aleph-im/aleph-rs#386). The
|
|
245
|
+
* answer there was to yield per message and carry on; the answer here is to
|
|
246
|
+
* skip the record and say which one it was.
|
|
247
|
+
*/
|
|
248
|
+
declare function unsupportedRelayBootstrapReason(post: RelayBootstrapPostRecord): string | null;
|
|
249
|
+
declare function selectCurrentRelayBootstrapPosts(posts: readonly RelayBootstrapPostRecord[], options?: Pick<DiscoverAlephBootstrapOptions, "maxAgeMs" | "onUnsupportedPost"> & {
|
|
215
250
|
now?: number;
|
|
216
251
|
}): RelayBootstrapPostRecord[];
|
|
217
252
|
/**
|
|
@@ -219,10 +254,30 @@ declare function selectCurrentRelayBootstrapPosts(posts: readonly RelayBootstrap
|
|
|
219
254
|
* A missing/empty filter is a no-op so existing callers are unaffected.
|
|
220
255
|
*/
|
|
221
256
|
declare function filterRelayBootstrapPostsByProfile(posts: readonly RelayBootstrapPostRecord[], profile?: string | readonly string[]): RelayBootstrapPostRecord[];
|
|
257
|
+
/**
|
|
258
|
+
* Keep only the registrations a consumer published for itself.
|
|
259
|
+
*
|
|
260
|
+
* Profile is not fine enough. Every ephemeral relay an E2E run starts
|
|
261
|
+
* registers under the same profile as the production one - as
|
|
262
|
+
* `relay:<profile>:<name>-e2e-*` - and a registration outlives the machine it
|
|
263
|
+
* describes, because guests self-publish with generated keys and no remaining
|
|
264
|
+
* key can FORGET the post. So the channel accumulates registrations that are
|
|
265
|
+
* indistinguishable from the real one by profile alone, and were alive when
|
|
266
|
+
* they were written.
|
|
267
|
+
*
|
|
268
|
+
* Measured downstream: a browser probe wave against that list exhausted its
|
|
269
|
+
* outbound stream budget on dead addresses and wrote off the one healthy relay
|
|
270
|
+
* along with the corpses, leaving both browsers with nothing to dial.
|
|
271
|
+
*
|
|
272
|
+
* Omitting the scope returns every registration, which is the previous
|
|
273
|
+
* behaviour and the right default for a consumer that wants to see the channel
|
|
274
|
+
* rather than its own corner of it.
|
|
275
|
+
*/
|
|
276
|
+
declare function filterRelayBootstrapPostsByRegistration(posts: readonly RelayBootstrapPostRecord[], registrationId?: string | readonly string[]): RelayBootstrapPostRecord[];
|
|
222
277
|
declare function discoverAlephBootstrapMultiaddrs(options?: DiscoverAlephBootstrapOptions): Promise<string[]>;
|
|
223
278
|
declare function createLibp2pAlephBootstrap(options?: DiscoverAlephBootstrapOptions & {
|
|
224
279
|
timeout?: number;
|
|
225
280
|
tagName?: string;
|
|
226
281
|
}): Promise<ReturnType<typeof bootstrap>>;
|
|
227
282
|
|
|
228
|
-
export { type CreateRelayBootstrapPostOptions, DEFAULT_ALEPH_API_HOST, DEFAULT_ALEPH_BOOTSTRAP_CHANNEL, DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE, DEFAULT_ALEPH_BOOTSTRAP_REF, DEFAULT_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT, DEFAULT_BOOTSTRAP_MAX_AGE_MS, DEFAULT_BOOTSTRAP_MAX_PAGES, DEFAULT_BOOTSTRAP_PAGINATION, type DiscoverAlephBootstrapOptions, type FilterPublicMultiaddrsOptions, RELAY_BOOTSTRAP_SIGNATURE_SCHEME, type RelayBootstrapAuthorizationPayload, type RelayBootstrapAuthorizationRecord, type RelayBootstrapContent, type RelayBootstrapPostContent, type RelayBootstrapPostRecord, type RelayBootstrapProofPayload, type RelayBootstrapProofRecord, type RelayBootstrapProofSigner, type RelayBootstrapTrustMode, type RelayBootstrapVerificationResult, buildRelayBootstrapPostContent, createLibp2pAlephBootstrap, createRelayBootstrapPost, dedupeMultiaddrs, discoverAlephBootstrapMultiaddrs, fetchAlephBootstrapPosts, filterPublicMultiaddrs, filterRelayBootstrapPostsByProfile, isBrowserDialableMultiaddr, isPublicMultiaddr, relayBootstrapMultiaddrsHash, relayBootstrapTrustMode, selectCompactRelayBootstrapMultiaddrs, selectCurrentRelayBootstrapPosts, signRelayBootstrapAuthorization, signRelayBootstrapProof, verifyRelayBootstrapAuthorization, verifyRelayBootstrapDualKeyContent, verifyRelayBootstrapProof };
|
|
283
|
+
export { type CreateRelayBootstrapPostOptions, DEFAULT_ALEPH_API_HOST, DEFAULT_ALEPH_BOOTSTRAP_CHANNEL, DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE, DEFAULT_ALEPH_BOOTSTRAP_REF, DEFAULT_BOOTSTRAP_COMPACT_MULTIADDR_LIMIT, DEFAULT_BOOTSTRAP_MAX_AGE_MS, DEFAULT_BOOTSTRAP_MAX_PAGES, DEFAULT_BOOTSTRAP_PAGINATION, type DiscoverAlephBootstrapOptions, type FilterPublicMultiaddrsOptions, RELAY_BOOTSTRAP_SIGNATURE_SCHEME, type RelayBootstrapAuthorizationPayload, type RelayBootstrapAuthorizationRecord, type RelayBootstrapContent, type RelayBootstrapPostContent, type RelayBootstrapPostRecord, type RelayBootstrapProofPayload, type RelayBootstrapProofRecord, type RelayBootstrapProofSigner, type RelayBootstrapTrustMode, type RelayBootstrapVerificationResult, buildRelayBootstrapPostContent, createLibp2pAlephBootstrap, createRelayBootstrapPost, dedupeMultiaddrs, discoverAlephBootstrapMultiaddrs, fetchAlephBootstrapPosts, filterPublicMultiaddrs, filterRelayBootstrapPostsByProfile, filterRelayBootstrapPostsByRegistration, isBrowserDialableMultiaddr, isPublicMultiaddr, relayBootstrapMultiaddrsHash, relayBootstrapTrustMode, selectCompactRelayBootstrapMultiaddrs, selectCurrentRelayBootstrapPosts, signRelayBootstrapAuthorization, signRelayBootstrapProof, unsupportedRelayBootstrapReason, verifyRelayBootstrapAuthorization, verifyRelayBootstrapDualKeyContent, verifyRelayBootstrapProof };
|
package/index.js
CHANGED
|
@@ -595,20 +595,24 @@ async function fetchAlephBootstrapPosts(options = {}) {
|
|
|
595
595
|
}
|
|
596
596
|
const payload = await response.json();
|
|
597
597
|
const posts = (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null).sort((left, right) => compareRelayBootstrapPostRecency(right, left));
|
|
598
|
-
posts.
|
|
599
|
-
return posts;
|
|
598
|
+
return usableRelayBootstrapPosts(posts, options.onUnsupportedPost);
|
|
600
599
|
}
|
|
601
|
-
function
|
|
600
|
+
function unsupportedRelayBootstrapReason(post) {
|
|
602
601
|
if (post.type === "relay-bootstrap") {
|
|
603
|
-
|
|
604
|
-
"Legacy relay-bootstrap record encountered. Only relay-bootstrap-v2 is supported."
|
|
605
|
-
);
|
|
602
|
+
return "legacy relay-bootstrap record; only relay-bootstrap-v2 is supported";
|
|
606
603
|
}
|
|
607
604
|
if (post.type && post.type !== DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE) {
|
|
608
|
-
|
|
609
|
-
`Unsupported relay bootstrap post type: ${post.type}. Expected ${DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE}.`
|
|
610
|
-
);
|
|
605
|
+
return `unsupported post type ${post.type}; expected ${DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE}`;
|
|
611
606
|
}
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
function usableRelayBootstrapPosts(posts, onUnsupportedPost) {
|
|
610
|
+
return posts.filter((post) => {
|
|
611
|
+
const reason = unsupportedRelayBootstrapReason(post);
|
|
612
|
+
if (!reason) return true;
|
|
613
|
+
onUnsupportedPost?.({ hash: post.itemHash ?? post.hash, reason });
|
|
614
|
+
return false;
|
|
615
|
+
});
|
|
612
616
|
}
|
|
613
617
|
function compareRelayBootstrapPostRecency(left, right) {
|
|
614
618
|
const leftUpdatedAt = left.content?.updatedAt ?? 0;
|
|
@@ -629,8 +633,7 @@ function selectCurrentRelayBootstrapPosts(posts, options = {}) {
|
|
|
629
633
|
const maxAgeMs = options.maxAgeMs ?? DEFAULT_BOOTSTRAP_MAX_AGE_MS;
|
|
630
634
|
const now = options.now ?? Date.now();
|
|
631
635
|
const selected = /* @__PURE__ */ new Map();
|
|
632
|
-
for (const post of posts) {
|
|
633
|
-
assertSupportedRelayBootstrapPost(post);
|
|
636
|
+
for (const post of usableRelayBootstrapPosts(posts, options.onUnsupportedPost)) {
|
|
634
637
|
const content = post.content;
|
|
635
638
|
if (!content) continue;
|
|
636
639
|
if (now - content.updatedAt > maxAgeMs) continue;
|
|
@@ -653,6 +656,14 @@ function filterRelayBootstrapPostsByProfile(posts, profile) {
|
|
|
653
656
|
(post) => post.content != null && allowed.has(String(post.content.profile))
|
|
654
657
|
);
|
|
655
658
|
}
|
|
659
|
+
function filterRelayBootstrapPostsByRegistration(posts, registrationId) {
|
|
660
|
+
const wanted = (typeof registrationId === "string" ? [registrationId] : registrationId ?? []).map((entry) => entry.trim()).filter(Boolean);
|
|
661
|
+
if (wanted.length === 0) return [...posts];
|
|
662
|
+
const allowed = new Set(wanted);
|
|
663
|
+
return posts.filter(
|
|
664
|
+
(post) => post.content != null && allowed.has(String(post.content.registrationId))
|
|
665
|
+
);
|
|
666
|
+
}
|
|
656
667
|
async function filterTrustedRelayBootstrapPosts(posts, options = {}) {
|
|
657
668
|
const requireDualKeyAttestation = options.requireDualKeyAttestation ?? false;
|
|
658
669
|
const verifyDualKeyAttestation = options.verifyDualKeyAttestation ?? true;
|
|
@@ -692,11 +703,14 @@ async function discoverAlephBootstrapMultiaddrs(options = {}) {
|
|
|
692
703
|
pagination
|
|
693
704
|
});
|
|
694
705
|
collectedPosts.push(...pagePosts);
|
|
695
|
-
const selectedPosts =
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
706
|
+
const selectedPosts = filterRelayBootstrapPostsByRegistration(
|
|
707
|
+
filterRelayBootstrapPostsByProfile(
|
|
708
|
+
selectCurrentRelayBootstrapPosts(collectedPosts, {
|
|
709
|
+
maxAgeMs: options.maxAgeMs
|
|
710
|
+
}),
|
|
711
|
+
options.profile
|
|
712
|
+
),
|
|
713
|
+
options.registrationId
|
|
700
714
|
);
|
|
701
715
|
const trustedPosts = await filterTrustedRelayBootstrapPosts(selectedPosts, {
|
|
702
716
|
requireDualKeyAttestation: options.requireDualKeyAttestation,
|
|
@@ -757,6 +771,7 @@ export {
|
|
|
757
771
|
fetchAlephBootstrapPosts,
|
|
758
772
|
filterPublicMultiaddrs,
|
|
759
773
|
filterRelayBootstrapPostsByProfile,
|
|
774
|
+
filterRelayBootstrapPostsByRegistration,
|
|
760
775
|
isBrowserDialableMultiaddr,
|
|
761
776
|
isPublicMultiaddr,
|
|
762
777
|
relayBootstrapMultiaddrsHash,
|
|
@@ -765,6 +780,7 @@ export {
|
|
|
765
780
|
selectCurrentRelayBootstrapPosts,
|
|
766
781
|
signRelayBootstrapAuthorization,
|
|
767
782
|
signRelayBootstrapProof,
|
|
783
|
+
unsupportedRelayBootstrapReason,
|
|
768
784
|
verifyRelayBootstrapAuthorization,
|
|
769
785
|
verifyRelayBootstrapDualKeyContent,
|
|
770
786
|
verifyRelayBootstrapProof
|