@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.
Files changed (4) hide show
  1. package/README.md +37 -0
  2. package/index.d.ts +57 -2
  3. package/index.js +32 -16
  4. 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
- declare function selectCurrentRelayBootstrapPosts(posts: readonly RelayBootstrapPostRecord[], options?: Pick<DiscoverAlephBootstrapOptions, "maxAgeMs"> & {
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.forEach(assertSupportedRelayBootstrapPost);
599
- return posts;
598
+ return usableRelayBootstrapPosts(posts, options.onUnsupportedPost);
600
599
  }
601
- function assertSupportedRelayBootstrapPost(post) {
600
+ function unsupportedRelayBootstrapReason(post) {
602
601
  if (post.type === "relay-bootstrap") {
603
- throw new Error(
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
- throw new Error(
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 = filterRelayBootstrapPostsByProfile(
696
- selectCurrentRelayBootstrapPosts(collectedPosts, {
697
- maxAgeMs: options.maxAgeMs
698
- }),
699
- options.profile
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/aleph-bootstrap",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "Aleph-backed libp2p bootstrap discovery and relay registration helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",