@le-space/aleph-bootstrap 0.3.0 → 0.3.2
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 +97 -2
- package/index.js +375 -5
- package/package.json +3 -2
- package/le-space-aleph-bootstrap-0.3.0.tgz +0 -0
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<
|
|
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
|
-
|
|
207
|
-
const
|
|
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.
|
|
3
|
+
"version": "0.3.2",
|
|
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
|