@le-space/aleph-bootstrap 0.3.0 → 0.3.3

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 CHANGED
@@ -14,6 +14,12 @@ It is designed for two complementary jobs:
14
14
  - `createLibp2pAlephBootstrap(options)`
15
15
  - `filterPublicMultiaddrs(addrs, options?)`
16
16
  - `createRelayBootstrapPost(options)`
17
+ - `signRelayBootstrapAuthorization(args)`
18
+ - `signRelayBootstrapProof(args)`
19
+ - `verifyRelayBootstrapAuthorization(record)`
20
+ - `verifyRelayBootstrapProof(record, options?)`
21
+ - `verifyRelayBootstrapDualKeyContent(content, options?)`
22
+ - `relayBootstrapTrustMode(content)`
17
23
 
18
24
  ## Default Aleph convention
19
25
 
@@ -24,3 +30,34 @@ The package defaults to the shared relay-bootstrap namespace:
24
30
  - post type: `relay-bootstrap`
25
31
 
26
32
  All values are overrideable per app or environment.
33
+
34
+ ## Discovery Trust Modes
35
+
36
+ The package accepts both:
37
+
38
+ - legacy wallet-signed bootstrap posts
39
+ - dual-key-attested bootstrap posts
40
+
41
+ By default, discovery will:
42
+
43
+ - accept legacy posts
44
+ - verify dual-key records when they are present
45
+ - ignore malformed or invalid dual-key records
46
+
47
+ If a consumer wants to require the stronger model:
48
+
49
+ ```ts
50
+ const list = await discoverAlephBootstrapMultiaddrs({
51
+ requireDualKeyAttestation: true,
52
+ })
53
+ ```
54
+
55
+ ## Dual-Key Model
56
+
57
+ The intended stronger trust model is:
58
+
59
+ - owner key `A` authorizes relay publisher key `B`
60
+ - relay publisher key `B` signs the bootstrap payload
61
+ - the Aleph bootstrap `POST` is published by `B`
62
+ - readers verify both the owner authorization and relay proof before trusting
63
+ the record
package/index.d.ts CHANGED
@@ -1,15 +1,59 @@
1
+ import { bootstrap } from '@libp2p/bootstrap';
2
+
1
3
  declare const DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
2
4
  declare const DEFAULT_ALEPH_BOOTSTRAP_CHANNEL = "simple-todo";
3
5
  declare const DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
4
6
  declare const DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
5
7
  declare const DEFAULT_BOOTSTRAP_MAX_AGE_MS: number;
6
8
  declare const DEFAULT_BOOTSTRAP_PAGINATION = 50;
9
+ declare const DEFAULT_BOOTSTRAP_MAX_PAGES = 5;
10
+ declare const RELAY_BOOTSTRAP_SIGNATURE_SCHEME = "personal_sign";
11
+ interface RelayBootstrapAuthorizationPayload {
12
+ ownerAddress: string;
13
+ publisherAddress: string;
14
+ peerId: string;
15
+ registrationId?: string;
16
+ profile?: string;
17
+ version?: string;
18
+ instanceItemHash?: string;
19
+ issuedAt: number;
20
+ expiresAt?: number;
21
+ }
22
+ interface RelayBootstrapAuthorizationRecord {
23
+ scheme: string;
24
+ payload: RelayBootstrapAuthorizationPayload;
25
+ signature: string;
26
+ }
27
+ interface RelayBootstrapProofPayload {
28
+ peerId: string;
29
+ multiaddrs: string[];
30
+ browserMultiaddrs?: string[];
31
+ registrationId?: string;
32
+ profile?: string;
33
+ version?: string;
34
+ updatedAt: number;
35
+ }
36
+ interface RelayBootstrapProofRecord {
37
+ scheme: string;
38
+ payload: RelayBootstrapProofPayload;
39
+ signature: string;
40
+ }
41
+ interface RelayBootstrapVerificationResult {
42
+ ok: boolean;
43
+ errors: string[];
44
+ }
45
+ type RelayBootstrapProofSigner = (address: string, payload: string) => Promise<string>;
7
46
  interface RelayBootstrapContent {
8
47
  peerId: string;
9
48
  multiaddrs: string[];
10
49
  browserMultiaddrs?: string[];
50
+ registrationId?: string;
11
51
  profile?: string;
12
52
  version?: string;
53
+ ownerAddress?: string;
54
+ publisherAddress?: string;
55
+ authorization?: RelayBootstrapAuthorizationRecord;
56
+ relayProof?: RelayBootstrapProofRecord;
13
57
  updatedAt: number;
14
58
  }
15
59
  interface RelayBootstrapPostContent {
@@ -35,8 +79,11 @@ interface DiscoverAlephBootstrapOptions {
35
79
  postType?: string;
36
80
  page?: number;
37
81
  pagination?: number;
82
+ maxPages?: number;
38
83
  maxAgeMs?: number;
39
84
  browserDialableOnly?: boolean;
85
+ requireDualKeyAttestation?: boolean;
86
+ verifyDualKeyAttestation?: boolean;
40
87
  fetch?: typeof fetch;
41
88
  }
42
89
  interface FilterPublicMultiaddrsOptions {
@@ -48,14 +95,20 @@ interface CreateRelayBootstrapPostOptions {
48
95
  peerId: string;
49
96
  multiaddrs: string[];
50
97
  browserMultiaddrs?: string[];
98
+ registrationId?: string;
51
99
  ref?: string;
52
100
  channel?: string;
53
101
  postType?: string;
54
102
  profile?: string;
55
103
  version?: string;
104
+ ownerAddress?: string;
105
+ publisherAddress?: string;
106
+ authorization?: RelayBootstrapAuthorizationRecord;
107
+ relayProof?: RelayBootstrapProofRecord;
56
108
  now?: number;
57
109
  hasher: (payload: string) => Promise<string> | string;
58
110
  }
111
+ type RelayBootstrapTrustMode = "legacy-wallet-signed" | "dual-key-attested";
59
112
  declare function dedupeMultiaddrs(addrs: readonly string[]): string[];
60
113
  declare function isPublicMultiaddr(addr: string): boolean;
61
114
  declare function filterPublicMultiaddrs(addrs: readonly string[], options?: FilterPublicMultiaddrsOptions): string[];
@@ -64,10 +117,15 @@ declare function buildRelayBootstrapPostContent(args: {
64
117
  peerId: string;
65
118
  multiaddrs: string[];
66
119
  browserMultiaddrs?: string[];
120
+ registrationId?: string;
67
121
  ref?: string;
68
122
  postType?: string;
69
123
  profile?: string;
70
124
  version?: string;
125
+ ownerAddress?: string;
126
+ publisherAddress?: string;
127
+ authorization?: RelayBootstrapAuthorizationRecord;
128
+ relayProof?: RelayBootstrapProofRecord;
71
129
  now?: number;
72
130
  }): RelayBootstrapPostContent;
73
131
  declare function createRelayBootstrapPost(args: CreateRelayBootstrapPostOptions): Promise<{
@@ -80,11 +138,48 @@ declare function createRelayBootstrapPost(args: CreateRelayBootstrapPostOptions)
80
138
  item_content: string;
81
139
  item_hash: string;
82
140
  }>;
141
+ declare function relayBootstrapTrustMode(content: RelayBootstrapContent | null | undefined): RelayBootstrapTrustMode;
142
+ declare function signRelayBootstrapAuthorization(args: {
143
+ ownerAddress: string;
144
+ publisherAddress: string;
145
+ peerId: string;
146
+ registrationId?: string;
147
+ profile?: string;
148
+ version?: string;
149
+ instanceItemHash?: string;
150
+ issuedAt?: number;
151
+ expiresAt?: number;
152
+ signer: RelayBootstrapProofSigner;
153
+ }): Promise<RelayBootstrapAuthorizationRecord>;
154
+ declare function signRelayBootstrapProof(args: {
155
+ publisherAddress: string;
156
+ peerId: string;
157
+ multiaddrs: string[];
158
+ browserMultiaddrs?: string[];
159
+ registrationId?: string;
160
+ profile?: string;
161
+ version?: string;
162
+ updatedAt?: number;
163
+ signer: RelayBootstrapProofSigner;
164
+ }): Promise<RelayBootstrapProofRecord>;
165
+ declare function verifyRelayBootstrapAuthorization(authorization: RelayBootstrapAuthorizationRecord | null | undefined, options?: {
166
+ now?: number;
167
+ }): Promise<RelayBootstrapVerificationResult>;
168
+ declare function verifyRelayBootstrapProof(proof: RelayBootstrapProofRecord | null | undefined, options?: {
169
+ expectedPublisherAddress?: string;
170
+ expectedPeerId?: string;
171
+ }): Promise<RelayBootstrapVerificationResult>;
172
+ declare function verifyRelayBootstrapDualKeyContent(content: RelayBootstrapContent | null | undefined, options?: {
173
+ now?: number;
174
+ }): Promise<RelayBootstrapVerificationResult>;
83
175
  declare function fetchAlephBootstrapPosts(options?: DiscoverAlephBootstrapOptions): Promise<RelayBootstrapPostRecord[]>;
176
+ declare function selectCurrentRelayBootstrapPosts(posts: readonly RelayBootstrapPostRecord[], options?: Pick<DiscoverAlephBootstrapOptions, "maxAgeMs"> & {
177
+ now?: number;
178
+ }): RelayBootstrapPostRecord[];
84
179
  declare function discoverAlephBootstrapMultiaddrs(options?: DiscoverAlephBootstrapOptions): Promise<string[]>;
85
180
  declare function createLibp2pAlephBootstrap(options?: DiscoverAlephBootstrapOptions & {
86
181
  timeout?: number;
87
182
  tagName?: string;
88
- }): Promise<unknown>;
183
+ }): Promise<ReturnType<typeof bootstrap>>;
89
184
 
90
- export { type CreateRelayBootstrapPostOptions, DEFAULT_ALEPH_API_HOST, DEFAULT_ALEPH_BOOTSTRAP_CHANNEL, DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE, DEFAULT_ALEPH_BOOTSTRAP_REF, DEFAULT_BOOTSTRAP_MAX_AGE_MS, DEFAULT_BOOTSTRAP_PAGINATION, type DiscoverAlephBootstrapOptions, type FilterPublicMultiaddrsOptions, type RelayBootstrapContent, type RelayBootstrapPostContent, type RelayBootstrapPostRecord, buildRelayBootstrapPostContent, createLibp2pAlephBootstrap, createRelayBootstrapPost, dedupeMultiaddrs, discoverAlephBootstrapMultiaddrs, fetchAlephBootstrapPosts, filterPublicMultiaddrs, isPublicMultiaddr };
185
+ export { type CreateRelayBootstrapPostOptions, DEFAULT_ALEPH_API_HOST, DEFAULT_ALEPH_BOOTSTRAP_CHANNEL, DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE, DEFAULT_ALEPH_BOOTSTRAP_REF, 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, isPublicMultiaddr, relayBootstrapTrustMode, selectCurrentRelayBootstrapPosts, signRelayBootstrapAuthorization, signRelayBootstrapProof, verifyRelayBootstrapAuthorization, verifyRelayBootstrapDualKeyContent, verifyRelayBootstrapProof };
package/index.js CHANGED
@@ -1,11 +1,44 @@
1
1
  // src/index.ts
2
2
  import { bootstrap } from "@libp2p/bootstrap";
3
+ import { recoverMessageAddress } from "viem";
3
4
  var DEFAULT_ALEPH_API_HOST = "https://api2.aleph.im";
4
5
  var DEFAULT_ALEPH_BOOTSTRAP_CHANNEL = "simple-todo";
5
6
  var DEFAULT_ALEPH_BOOTSTRAP_REF = "simple-todo-bootstrap";
6
7
  var DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE = "relay-bootstrap";
7
8
  var DEFAULT_BOOTSTRAP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
8
9
  var DEFAULT_BOOTSTRAP_PAGINATION = 50;
10
+ var DEFAULT_BOOTSTRAP_MAX_PAGES = 5;
11
+ var RELAY_BOOTSTRAP_SIGNATURE_SCHEME = "personal_sign";
12
+ function serializeOwnerAuthorizationPayload(payload) {
13
+ return JSON.stringify({
14
+ ownerAddress: payload.ownerAddress,
15
+ publisherAddress: payload.publisherAddress,
16
+ peerId: payload.peerId,
17
+ registrationId: payload.registrationId,
18
+ profile: payload.profile,
19
+ version: payload.version,
20
+ instanceItemHash: payload.instanceItemHash,
21
+ issuedAt: payload.issuedAt,
22
+ expiresAt: payload.expiresAt
23
+ });
24
+ }
25
+ function serializeRelayProofPayload(payload) {
26
+ return JSON.stringify({
27
+ peerId: payload.peerId,
28
+ multiaddrs: dedupeMultiaddrs(payload.multiaddrs),
29
+ browserMultiaddrs: payload.browserMultiaddrs ? dedupeMultiaddrs(payload.browserMultiaddrs) : void 0,
30
+ registrationId: payload.registrationId,
31
+ profile: payload.profile,
32
+ version: payload.version,
33
+ updatedAt: payload.updatedAt
34
+ });
35
+ }
36
+ async function recoverAddressForSignature(payload, signature) {
37
+ return recoverMessageAddress({
38
+ message: payload,
39
+ signature
40
+ });
41
+ }
9
42
  function asTrimmedString(value) {
10
43
  return typeof value === "string" && value.trim() ? value.trim() : null;
11
44
  }
@@ -20,6 +53,13 @@ function asNumber(value) {
20
53
  function normalizeHost(host) {
21
54
  return host.trim().toLowerCase();
22
55
  }
56
+ function normalizeAddress(address) {
57
+ return address.trim().toLowerCase();
58
+ }
59
+ function normalizeSignature(signature) {
60
+ const trimmed = signature.trim();
61
+ return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
62
+ }
23
63
  function splitMultiaddr(addr) {
24
64
  return addr.split("/").filter(Boolean);
25
65
  }
@@ -102,11 +142,75 @@ function normalizeRelayBootstrapContent(value) {
102
142
  peerId,
103
143
  multiaddrs: dedupeMultiaddrs(multiaddrs),
104
144
  browserMultiaddrs: browserMultiaddrs ? dedupeMultiaddrs(browserMultiaddrs) : void 0,
145
+ registrationId: asTrimmedString(content.registrationId) ?? void 0,
105
146
  profile: asTrimmedString(content.profile) ?? void 0,
106
147
  version: asTrimmedString(content.version) ?? void 0,
148
+ ownerAddress: asTrimmedString(content.ownerAddress) ?? void 0,
149
+ publisherAddress: asTrimmedString(content.publisherAddress) ?? void 0,
150
+ authorization: normalizeRelayBootstrapAuthorizationRecord(content.authorization) ?? void 0,
151
+ relayProof: normalizeRelayBootstrapProofRecord(content.relayProof) ?? void 0,
152
+ updatedAt
153
+ };
154
+ }
155
+ function normalizeRelayBootstrapAuthorizationPayload(value) {
156
+ if (!value || typeof value !== "object") return null;
157
+ const payload = value;
158
+ const ownerAddress = asTrimmedString(payload.ownerAddress);
159
+ const publisherAddress = asTrimmedString(payload.publisherAddress);
160
+ const peerId = asTrimmedString(payload.peerId);
161
+ const issuedAt = asNumber(payload.issuedAt);
162
+ const expiresAt = asNumber(payload.expiresAt) ?? void 0;
163
+ if (!ownerAddress || !publisherAddress || !peerId || issuedAt == null) {
164
+ return null;
165
+ }
166
+ return {
167
+ ownerAddress,
168
+ publisherAddress,
169
+ peerId,
170
+ registrationId: asTrimmedString(payload.registrationId) ?? void 0,
171
+ profile: asTrimmedString(payload.profile) ?? void 0,
172
+ version: asTrimmedString(payload.version) ?? void 0,
173
+ instanceItemHash: asTrimmedString(payload.instanceItemHash) ?? void 0,
174
+ issuedAt,
175
+ expiresAt
176
+ };
177
+ }
178
+ function normalizeRelayBootstrapAuthorizationRecord(value) {
179
+ if (!value || typeof value !== "object") return null;
180
+ const record = value;
181
+ const scheme = asTrimmedString(record.scheme);
182
+ const signature = asTrimmedString(record.signature);
183
+ const payload = normalizeRelayBootstrapAuthorizationPayload(record.payload);
184
+ if (!scheme || !signature || !payload) return null;
185
+ return { scheme, signature, payload };
186
+ }
187
+ function normalizeRelayBootstrapProofPayload(value) {
188
+ if (!value || typeof value !== "object") return null;
189
+ const payload = value;
190
+ const peerId = asTrimmedString(payload.peerId);
191
+ const updatedAt = asNumber(payload.updatedAt);
192
+ if (!peerId || updatedAt == null) return null;
193
+ const multiaddrs = Array.isArray(payload.multiaddrs) ? payload.multiaddrs.filter((entry) => typeof entry === "string") : [];
194
+ const browserMultiaddrs = Array.isArray(payload.browserMultiaddrs) ? payload.browserMultiaddrs.filter((entry) => typeof entry === "string") : void 0;
195
+ return {
196
+ peerId,
197
+ multiaddrs: dedupeMultiaddrs(multiaddrs),
198
+ browserMultiaddrs: browserMultiaddrs ? dedupeMultiaddrs(browserMultiaddrs) : void 0,
199
+ registrationId: asTrimmedString(payload.registrationId) ?? void 0,
200
+ profile: asTrimmedString(payload.profile) ?? void 0,
201
+ version: asTrimmedString(payload.version) ?? void 0,
107
202
  updatedAt
108
203
  };
109
204
  }
205
+ function normalizeRelayBootstrapProofRecord(value) {
206
+ if (!value || typeof value !== "object") return null;
207
+ const record = value;
208
+ const scheme = asTrimmedString(record.scheme);
209
+ const signature = asTrimmedString(record.signature);
210
+ const payload = normalizeRelayBootstrapProofPayload(record.payload);
211
+ if (!scheme || !signature || !payload) return null;
212
+ return { scheme, signature, payload };
213
+ }
110
214
  function normalizeRelayBootstrapPostRecord(value) {
111
215
  if (!value || typeof value !== "object") return null;
112
216
  const entry = value;
@@ -133,8 +237,13 @@ function buildRelayBootstrapPostContent(args) {
133
237
  browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
134
238
  browserDialableOnly: true
135
239
  }) : void 0,
240
+ registrationId: args.registrationId,
136
241
  profile: args.profile,
137
242
  version: args.version,
243
+ ownerAddress: args.ownerAddress,
244
+ publisherAddress: args.publisherAddress,
245
+ authorization: args.authorization,
246
+ relayProof: args.relayProof,
138
247
  updatedAt: Math.round(updatedAt)
139
248
  },
140
249
  time: now
@@ -148,10 +257,15 @@ async function createRelayBootstrapPost(args) {
148
257
  peerId: args.peerId,
149
258
  multiaddrs: args.multiaddrs,
150
259
  browserMultiaddrs: args.browserMultiaddrs,
260
+ registrationId: args.registrationId,
151
261
  ref: args.ref ?? DEFAULT_ALEPH_BOOTSTRAP_REF,
152
262
  postType: args.postType ?? DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
153
263
  profile: args.profile,
154
264
  version: args.version,
265
+ ownerAddress: args.ownerAddress,
266
+ publisherAddress: args.publisherAddress,
267
+ authorization: args.authorization,
268
+ relayProof: args.relayProof,
155
269
  now: nowMillis
156
270
  });
157
271
  itemContent.time = nowSeconds;
@@ -168,6 +282,165 @@ async function createRelayBootstrapPost(args) {
168
282
  item_hash: itemHash
169
283
  };
170
284
  }
285
+ function relayBootstrapTrustMode(content) {
286
+ if (content?.authorization && content?.relayProof && content.authorization.payload.publisherAddress && content.relayProof.payload.peerId) {
287
+ return "dual-key-attested";
288
+ }
289
+ return "legacy-wallet-signed";
290
+ }
291
+ async function signRelayBootstrapAuthorization(args) {
292
+ const payload = {
293
+ ownerAddress: args.ownerAddress,
294
+ publisherAddress: args.publisherAddress,
295
+ peerId: args.peerId,
296
+ registrationId: args.registrationId,
297
+ profile: args.profile,
298
+ version: args.version,
299
+ instanceItemHash: args.instanceItemHash,
300
+ issuedAt: args.issuedAt ?? Date.now(),
301
+ expiresAt: args.expiresAt
302
+ };
303
+ const serialized = serializeOwnerAuthorizationPayload(payload);
304
+ const signature = await args.signer(args.ownerAddress, serialized);
305
+ return {
306
+ scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
307
+ payload,
308
+ signature: normalizeSignature(signature)
309
+ };
310
+ }
311
+ async function signRelayBootstrapProof(args) {
312
+ const payload = {
313
+ peerId: args.peerId,
314
+ multiaddrs: filterPublicMultiaddrs(args.multiaddrs),
315
+ browserMultiaddrs: args.browserMultiaddrs ? filterPublicMultiaddrs(args.browserMultiaddrs, {
316
+ browserDialableOnly: true
317
+ }) : void 0,
318
+ registrationId: args.registrationId,
319
+ profile: args.profile,
320
+ version: args.version,
321
+ updatedAt: args.updatedAt ?? Date.now()
322
+ };
323
+ const serialized = serializeRelayProofPayload(payload);
324
+ const signature = await args.signer(args.publisherAddress, serialized);
325
+ return {
326
+ scheme: RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
327
+ payload,
328
+ signature: normalizeSignature(signature)
329
+ };
330
+ }
331
+ async function verifyRelayBootstrapAuthorization(authorization, options = {}) {
332
+ const errors = [];
333
+ if (!authorization) {
334
+ errors.push("Missing owner authorization.");
335
+ return { ok: false, errors };
336
+ }
337
+ if (authorization.scheme !== RELAY_BOOTSTRAP_SIGNATURE_SCHEME) {
338
+ errors.push(`Unsupported owner authorization scheme: ${authorization.scheme}`);
339
+ return { ok: false, errors };
340
+ }
341
+ const payload = authorization.payload;
342
+ const serialized = serializeOwnerAuthorizationPayload(payload);
343
+ try {
344
+ const recovered = await recoverAddressForSignature(
345
+ serialized,
346
+ authorization.signature
347
+ );
348
+ if (normalizeAddress(recovered) !== normalizeAddress(payload.ownerAddress)) {
349
+ errors.push("Owner authorization signature does not recover the owner address.");
350
+ }
351
+ } catch (error) {
352
+ errors.push(
353
+ `Owner authorization signature could not be recovered: ${error instanceof Error ? error.message : String(error)}`
354
+ );
355
+ }
356
+ const now = options.now ?? Date.now();
357
+ if (payload.expiresAt != null && now > payload.expiresAt) {
358
+ errors.push("Owner authorization has expired.");
359
+ }
360
+ return { ok: errors.length === 0, errors };
361
+ }
362
+ async function verifyRelayBootstrapProof(proof, options = {}) {
363
+ const errors = [];
364
+ if (!proof) {
365
+ errors.push("Missing relay proof.");
366
+ return { ok: false, errors };
367
+ }
368
+ if (proof.scheme !== RELAY_BOOTSTRAP_SIGNATURE_SCHEME) {
369
+ errors.push(`Unsupported relay proof scheme: ${proof.scheme}`);
370
+ return { ok: false, errors };
371
+ }
372
+ const payload = proof.payload;
373
+ const serialized = serializeRelayProofPayload(payload);
374
+ try {
375
+ const recovered = await recoverAddressForSignature(serialized, proof.signature);
376
+ if (options.expectedPublisherAddress && normalizeAddress(recovered) !== normalizeAddress(options.expectedPublisherAddress)) {
377
+ errors.push("Relay proof signature does not recover the expected publisher address.");
378
+ }
379
+ } catch (error) {
380
+ errors.push(
381
+ `Relay proof signature could not be recovered: ${error instanceof Error ? error.message : String(error)}`
382
+ );
383
+ }
384
+ if (options.expectedPeerId && payload.peerId !== options.expectedPeerId) {
385
+ errors.push("Relay proof peer ID does not match the expected peer ID.");
386
+ }
387
+ return { ok: errors.length === 0, errors };
388
+ }
389
+ async function verifyRelayBootstrapDualKeyContent(content, options = {}) {
390
+ const errors = [];
391
+ if (!content) {
392
+ errors.push("Missing relay bootstrap content.");
393
+ return { ok: false, errors };
394
+ }
395
+ const authorization = await verifyRelayBootstrapAuthorization(
396
+ content.authorization,
397
+ options
398
+ );
399
+ errors.push(...authorization.errors);
400
+ const expectedPublisherAddress = content.publisherAddress ?? content.authorization?.payload.publisherAddress ?? void 0;
401
+ const proof = await verifyRelayBootstrapProof(content.relayProof, {
402
+ expectedPublisherAddress,
403
+ expectedPeerId: content.peerId
404
+ });
405
+ errors.push(...proof.errors);
406
+ if (content.ownerAddress && content.authorization) {
407
+ if (normalizeAddress(content.ownerAddress) !== normalizeAddress(content.authorization.payload.ownerAddress)) {
408
+ errors.push("Content ownerAddress does not match the owner authorization payload.");
409
+ }
410
+ }
411
+ if (expectedPublisherAddress && content.authorization) {
412
+ if (normalizeAddress(expectedPublisherAddress) !== normalizeAddress(content.authorization.payload.publisherAddress)) {
413
+ errors.push(
414
+ "Content publisherAddress does not match the owner authorization payload."
415
+ );
416
+ }
417
+ }
418
+ if (content.authorization && content.authorization.payload.peerId !== content.peerId) {
419
+ errors.push("Owner authorization peer ID does not match the bootstrap content peer ID.");
420
+ }
421
+ const proofPayload = content.relayProof?.payload;
422
+ if (proofPayload) {
423
+ if (proofPayload.registrationId !== content.registrationId) {
424
+ errors.push("Relay proof registrationId does not match the bootstrap content.");
425
+ }
426
+ if (proofPayload.profile !== content.profile) {
427
+ errors.push("Relay proof profile does not match the bootstrap content.");
428
+ }
429
+ if (proofPayload.version !== content.version) {
430
+ errors.push("Relay proof version does not match the bootstrap content.");
431
+ }
432
+ if (proofPayload.updatedAt !== content.updatedAt) {
433
+ errors.push("Relay proof updatedAt does not match the bootstrap content.");
434
+ }
435
+ if (JSON.stringify(dedupeMultiaddrs(proofPayload.multiaddrs)) !== JSON.stringify(dedupeMultiaddrs(content.multiaddrs))) {
436
+ errors.push("Relay proof multiaddrs do not match the bootstrap content.");
437
+ }
438
+ if (JSON.stringify(dedupeMultiaddrs(proofPayload.browserMultiaddrs ?? [])) !== JSON.stringify(dedupeMultiaddrs(content.browserMultiaddrs ?? []))) {
439
+ errors.push("Relay proof browserMultiaddrs do not match the bootstrap content.");
440
+ }
441
+ }
442
+ return { ok: errors.length === 0, errors };
443
+ }
171
444
  async function fetchAlephBootstrapPosts(options = {}) {
172
445
  const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
173
446
  if (typeof fetchImpl !== "function") {
@@ -203,16 +476,104 @@ async function fetchAlephBootstrapPosts(options = {}) {
203
476
  const payload = await response.json();
204
477
  return (payload.posts ?? []).map((entry) => normalizeRelayBootstrapPostRecord(entry)).filter((entry) => entry != null);
205
478
  }
206
- async function discoverAlephBootstrapMultiaddrs(options = {}) {
207
- const posts = await fetchAlephBootstrapPosts(options);
479
+ function compareRelayBootstrapPostRecency(left, right) {
480
+ const leftUpdatedAt = left.content?.updatedAt ?? 0;
481
+ const rightUpdatedAt = right.content?.updatedAt ?? 0;
482
+ if (leftUpdatedAt !== rightUpdatedAt) return leftUpdatedAt - rightUpdatedAt;
483
+ const leftTime = left.time ?? 0;
484
+ const rightTime = right.time ?? 0;
485
+ return leftTime - rightTime;
486
+ }
487
+ function relayBootstrapRecordIdentity(post) {
488
+ const content = post.content;
489
+ if (!content) return null;
490
+ if (content.registrationId) return `registration:${content.registrationId}`;
491
+ if (post.address) return `sender:${normalizeAddress(post.address)}`;
492
+ return content.peerId ? `peer:${content.peerId}` : null;
493
+ }
494
+ function selectCurrentRelayBootstrapPosts(posts, options = {}) {
208
495
  const maxAgeMs = options.maxAgeMs ?? DEFAULT_BOOTSTRAP_MAX_AGE_MS;
209
- const now = Date.now();
496
+ const now = options.now ?? Date.now();
497
+ const selected = /* @__PURE__ */ new Map();
498
+ for (const post of posts) {
499
+ const content = post.content;
500
+ if (!content) continue;
501
+ if (now - content.updatedAt > maxAgeMs) continue;
502
+ const identity = relayBootstrapRecordIdentity(post);
503
+ if (!identity) continue;
504
+ const previous = selected.get(identity);
505
+ if (!previous || compareRelayBootstrapPostRecency(post, previous) > 0) {
506
+ selected.set(identity, post);
507
+ }
508
+ }
509
+ return [...selected.values()].sort(
510
+ (left, right) => compareRelayBootstrapPostRecency(right, left)
511
+ );
512
+ }
513
+ async function filterTrustedRelayBootstrapPosts(posts, options = {}) {
514
+ const requireDualKeyAttestation = options.requireDualKeyAttestation ?? false;
515
+ const verifyDualKeyAttestation = options.verifyDualKeyAttestation ?? true;
516
+ const trusted = [];
517
+ for (const post of posts) {
518
+ const content = post.content;
519
+ if (!content) continue;
520
+ const trustMode = relayBootstrapTrustMode(content);
521
+ if (trustMode === "legacy-wallet-signed") {
522
+ if (!requireDualKeyAttestation) {
523
+ trusted.push(post);
524
+ }
525
+ continue;
526
+ }
527
+ if (!verifyDualKeyAttestation) {
528
+ trusted.push(post);
529
+ continue;
530
+ }
531
+ const verification = await verifyRelayBootstrapDualKeyContent(content);
532
+ if (verification.ok) {
533
+ trusted.push(post);
534
+ }
535
+ }
536
+ return trusted;
537
+ }
538
+ async function discoverAlephBootstrapMultiaddrs(options = {}) {
210
539
  const browserDialableOnly = options.browserDialableOnly ?? true;
540
+ const pagination = options.pagination ?? DEFAULT_BOOTSTRAP_PAGINATION;
541
+ const startPage = options.page ?? 1;
542
+ const maxPages = Math.max(1, options.maxPages ?? DEFAULT_BOOTSTRAP_MAX_PAGES);
543
+ const collectedPosts = [];
544
+ for (let offset = 0; offset < maxPages; offset += 1) {
545
+ const page = startPage + offset;
546
+ const pagePosts = await fetchAlephBootstrapPosts({
547
+ ...options,
548
+ page,
549
+ pagination
550
+ });
551
+ collectedPosts.push(...pagePosts);
552
+ const selectedPosts = selectCurrentRelayBootstrapPosts(collectedPosts, {
553
+ maxAgeMs: options.maxAgeMs
554
+ });
555
+ const trustedPosts = await filterTrustedRelayBootstrapPosts(selectedPosts, {
556
+ requireDualKeyAttestation: options.requireDualKeyAttestation,
557
+ verifyDualKeyAttestation: options.verifyDualKeyAttestation
558
+ });
559
+ const addrs = relayBootstrapPostsToMultiaddrs(
560
+ trustedPosts,
561
+ browserDialableOnly
562
+ );
563
+ if (addrs.length > 0) {
564
+ return addrs;
565
+ }
566
+ if (pagePosts.length < pagination) {
567
+ break;
568
+ }
569
+ }
570
+ return [];
571
+ }
572
+ function relayBootstrapPostsToMultiaddrs(posts, browserDialableOnly) {
211
573
  const addrs = [];
212
574
  for (const post of posts) {
213
575
  const content = post.content;
214
576
  if (!content) continue;
215
- if (now - content.updatedAt > maxAgeMs) continue;
216
577
  const candidates = browserDialableOnly && Array.isArray(content.browserMultiaddrs) && content.browserMultiaddrs.length > 0 ? content.browserMultiaddrs : content.multiaddrs;
217
578
  addrs.push(
218
579
  ...filterPublicMultiaddrs(candidates, {
@@ -236,7 +597,9 @@ export {
236
597
  DEFAULT_ALEPH_BOOTSTRAP_POST_TYPE,
237
598
  DEFAULT_ALEPH_BOOTSTRAP_REF,
238
599
  DEFAULT_BOOTSTRAP_MAX_AGE_MS,
600
+ DEFAULT_BOOTSTRAP_MAX_PAGES,
239
601
  DEFAULT_BOOTSTRAP_PAGINATION,
602
+ RELAY_BOOTSTRAP_SIGNATURE_SCHEME,
240
603
  buildRelayBootstrapPostContent,
241
604
  createLibp2pAlephBootstrap,
242
605
  createRelayBootstrapPost,
@@ -244,5 +607,12 @@ export {
244
607
  discoverAlephBootstrapMultiaddrs,
245
608
  fetchAlephBootstrapPosts,
246
609
  filterPublicMultiaddrs,
247
- isPublicMultiaddr
610
+ isPublicMultiaddr,
611
+ relayBootstrapTrustMode,
612
+ selectCurrentRelayBootstrapPosts,
613
+ signRelayBootstrapAuthorization,
614
+ signRelayBootstrapProof,
615
+ verifyRelayBootstrapAuthorization,
616
+ verifyRelayBootstrapDualKeyContent,
617
+ verifyRelayBootstrapProof
248
618
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/aleph-bootstrap",
3
- "version": "0.3.0",
3
+ "version": "0.3.3",
4
4
  "description": "Aleph-backed libp2p bootstrap discovery and relay registration helpers.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,6 +16,7 @@
16
16
  "access": "public"
17
17
  },
18
18
  "dependencies": {
19
- "@libp2p/bootstrap": "^11.0.46"
19
+ "@libp2p/bootstrap": "^11.0.46",
20
+ "viem": "^2.38.5"
20
21
  }
21
22
  }
Binary file