epistery 2.0.3 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.test.env +25 -0
- package/cli/epistery.mjs +9 -10
- package/client/wallet.js +30 -7
- package/client/witness.js +29 -12
- package/dist/chains/registry.d.ts +2 -2
- package/dist/chains/registry.d.ts.map +1 -1
- package/dist/chains/registry.js +4 -4
- package/dist/chains/registry.js.map +1 -1
- package/dist/epistery.d.ts +1 -1
- package/dist/epistery.d.ts.map +1 -1
- package/dist/epistery.js +4 -4
- package/dist/epistery.js.map +1 -1
- package/dist/utils/CliWallet.d.ts +9 -5
- package/dist/utils/CliWallet.d.ts.map +1 -1
- package/dist/utils/CliWallet.js +18 -13
- package/dist/utils/CliWallet.js.map +1 -1
- package/dist/utils/Config.d.ts +101 -48
- package/dist/utils/Config.d.ts.map +1 -1
- package/dist/utils/Config.js +257 -116
- package/dist/utils/Config.js.map +1 -1
- package/dist/utils/Utils.d.ts +10 -2
- package/dist/utils/Utils.d.ts.map +1 -1
- package/dist/utils/Utils.js +18 -9
- package/dist/utils/Utils.js.map +1 -1
- package/docs/RivetSignerConfigAuthority.md +219 -0
- package/index.mjs +10 -9
- package/package.json +1 -1
- package/routes/auth.mjs +6 -6
- package/routes/domain.mjs +2 -2
- package/routes/fido.mjs +4 -4
- package/src/chains/registry.ts +4 -4
- package/src/epistery.ts +4 -4
- package/src/utils/CliWallet.ts +18 -13
- package/src/utils/Config.ts +289 -106
- package/src/utils/Utils.ts +19 -9
- package/test/config/.epistery/127.0.0.1/config.ini +15 -0
- package/test/config/.epistery/config.ini +14 -0
- package/test/config/.epistery/localhost/config.ini +16 -0
- package/test/config/.epistery/no-pending-claim.local/config.ini +15 -0
- package/test/config/.epistery/test-claim-1782325887560.local/config.ini +20 -0
- package/test/config/.epistery/test-idempotent-1782325887610.local/config.ini +20 -0
- package/test/config/.epistery/test-init-1782325888110.local/config.ini +16 -0
- package/test/routes/auth.test.ts +7 -3
- package/test/routes/identity.test.ts +0 -41
- package/test/routes/status.test.ts +5 -39
- package/test/setup.ts +9 -9
- package/test/utils.ts +9 -3
package/src/utils/Config.ts
CHANGED
|
@@ -1,23 +1,59 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import fsp from 'fs/promises';
|
|
2
3
|
import { join } from 'path';
|
|
3
4
|
import ini from 'ini';
|
|
5
|
+
import { ethers } from 'ethers';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
* Epistery Config
|
|
8
|
+
* Epistery Config — async path-based configuration store.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
10
|
+
* Filesystem-like config management, now async so the same interface can be
|
|
11
|
+
* served by a remote authority:
|
|
9
12
|
* - setPath('/') → ~/.epistery/config.ini
|
|
10
13
|
* - setPath('/domain') → ~/.epistery/domain/config.ini
|
|
11
14
|
* - setPath('/.ssl/domain') → ~/.epistery/.ssl/domain/config.ini
|
|
12
15
|
*
|
|
16
|
+
* `Config` is a thin facade that picks a backend at construction:
|
|
17
|
+
* - if the local bootstrap ~/.epistery/config.ini has [authority] url=…,
|
|
18
|
+
* or EPISTERY_CONFIG_URL is set → RemoteConfig (talks to epistery-authority)
|
|
19
|
+
* - otherwise → LocalConfig (the filesystem store; unchanged semantics)
|
|
20
|
+
*
|
|
21
|
+
* Every IO method is async because a remote/HSM custodian cannot answer
|
|
22
|
+
* synchronously. `data` holds the snapshot from the last awaited load()/setPath().
|
|
23
|
+
*
|
|
13
24
|
* Usage:
|
|
14
25
|
* const config = new Config('epistery');
|
|
15
|
-
* config.setPath('/wiki.rootz.global');
|
|
16
|
-
* config.load();
|
|
26
|
+
* await config.setPath('/wiki.rootz.global'); // loads
|
|
17
27
|
* config.data.verified = true;
|
|
18
|
-
* config.save();
|
|
28
|
+
* await config.save();
|
|
29
|
+
*/
|
|
30
|
+
export interface ConfigStore {
|
|
31
|
+
data: any;
|
|
32
|
+
getPath(): string;
|
|
33
|
+
setPath(path: string): Promise<void>;
|
|
34
|
+
load(): Promise<void>;
|
|
35
|
+
read(path: string): Promise<any>;
|
|
36
|
+
save(): Promise<void>;
|
|
37
|
+
readFile(filename: string): Promise<Buffer>;
|
|
38
|
+
writeFile(filename: string, data: string | Buffer): Promise<void>;
|
|
39
|
+
exists(): Promise<boolean>;
|
|
40
|
+
listPaths(): Promise<string[]>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Normalize a path: leading slash, no trailing slash, lowercase. */
|
|
44
|
+
function normalizePath(path: string): string {
|
|
45
|
+
path = path.trim();
|
|
46
|
+
if (!path.startsWith('/')) path = '/' + path;
|
|
47
|
+
if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1);
|
|
48
|
+
return path.toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* LocalConfig — the filesystem backend under ~/.epistery. This is the original
|
|
53
|
+
* Config behavior, with all IO made async. It is also what the epistery-authority
|
|
54
|
+
* server mounts as its own storage backend (one implementation, not two).
|
|
19
55
|
*/
|
|
20
|
-
export class
|
|
56
|
+
export class LocalConfig implements ConfigStore {
|
|
21
57
|
public readonly rootName: string;
|
|
22
58
|
public readonly homeDir: string;
|
|
23
59
|
public readonly configDir: string;
|
|
@@ -36,151 +72,291 @@ export class Config {
|
|
|
36
72
|
this.currentDir = this.configDir;
|
|
37
73
|
this.currentFile = join(this.configDir, 'config.ini');
|
|
38
74
|
|
|
39
|
-
//
|
|
75
|
+
// Bootstrap is synchronous: seed the root config so the very first run has
|
|
76
|
+
// somewhere to read the authority URL from. Only this seed stays sync; all
|
|
77
|
+
// subsequent IO is async.
|
|
40
78
|
if (!fs.existsSync(this.currentFile)) {
|
|
41
|
-
this.
|
|
79
|
+
this.initializeSync();
|
|
42
80
|
}
|
|
43
81
|
}
|
|
44
82
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
private initializeSync(): void {
|
|
84
|
+
if (!fs.existsSync(this.currentDir)) {
|
|
85
|
+
fs.mkdirSync(this.currentDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
const defaultContent = this.currentPath === '/' ? defaultIni : '';
|
|
88
|
+
fs.writeFileSync(this.currentFile, defaultContent);
|
|
89
|
+
this.data = ini.decode(defaultContent);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public getPath(): string {
|
|
93
|
+
return this.currentPath;
|
|
94
|
+
}
|
|
57
95
|
|
|
96
|
+
public async setPath(path: string): Promise<void> {
|
|
97
|
+
path = normalizePath(path);
|
|
58
98
|
this.currentPath = path;
|
|
59
99
|
|
|
60
|
-
// Calculate directory and file paths
|
|
61
100
|
if (path === '/') {
|
|
62
101
|
this.currentDir = this.configDir;
|
|
63
102
|
this.currentFile = join(this.configDir, 'config.ini');
|
|
64
103
|
} else {
|
|
65
|
-
this.currentDir = join(this.configDir, path.slice(1));
|
|
104
|
+
this.currentDir = join(this.configDir, path.slice(1));
|
|
66
105
|
this.currentFile = join(this.currentDir, 'config.ini');
|
|
67
106
|
}
|
|
68
107
|
|
|
69
|
-
|
|
70
|
-
this.load();
|
|
108
|
+
await this.load();
|
|
71
109
|
}
|
|
72
110
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
111
|
+
public async load(): Promise<void> {
|
|
112
|
+
try {
|
|
113
|
+
const fileData = await fsp.readFile(this.currentFile, 'utf8');
|
|
114
|
+
this.data = ini.decode(fileData);
|
|
115
|
+
} catch {
|
|
116
|
+
this.data = {};
|
|
117
|
+
}
|
|
78
118
|
}
|
|
79
119
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
120
|
+
public async read(path: string): Promise<any> {
|
|
121
|
+
path = normalizePath(path);
|
|
122
|
+
const configFile = path === '/'
|
|
123
|
+
? join(this.configDir, 'config.ini')
|
|
124
|
+
: join(this.configDir, path.slice(1), 'config.ini');
|
|
125
|
+
try {
|
|
126
|
+
const fileData = await fsp.readFile(configFile, 'utf8');
|
|
127
|
+
return ini.decode(fileData);
|
|
128
|
+
} catch {
|
|
129
|
+
return {};
|
|
86
130
|
}
|
|
131
|
+
}
|
|
87
132
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.data = ini.decode(defaultContent);
|
|
133
|
+
public async save(): Promise<void> {
|
|
134
|
+
await fsp.mkdir(this.currentDir, { recursive: true });
|
|
135
|
+
await fsp.writeFile(this.currentFile, ini.stringify(this.data));
|
|
92
136
|
}
|
|
93
137
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
public load(): void {
|
|
98
|
-
if (!fs.existsSync(this.currentFile)) {
|
|
99
|
-
this.data = {};
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
138
|
+
public async readFile(filename: string): Promise<Buffer> {
|
|
139
|
+
return fsp.readFile(join(this.currentDir, filename));
|
|
140
|
+
}
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
this.
|
|
142
|
+
public async writeFile(filename: string, data: string | Buffer): Promise<void> {
|
|
143
|
+
await fsp.mkdir(this.currentDir, { recursive: true });
|
|
144
|
+
await fsp.writeFile(join(this.currentDir, filename), data);
|
|
105
145
|
}
|
|
106
146
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Normalize path
|
|
114
|
-
path = path.trim();
|
|
115
|
-
if (!path.startsWith('/')) path = '/' + path;
|
|
116
|
-
if (path.length > 1 && path.endsWith('/')) path = path.slice(0, -1);
|
|
117
|
-
path = path.toLowerCase();
|
|
118
|
-
|
|
119
|
-
// Calculate file location
|
|
120
|
-
let configFile: string;
|
|
121
|
-
if (path === '/') {
|
|
122
|
-
configFile = join(this.configDir, 'config.ini');
|
|
123
|
-
} else {
|
|
124
|
-
configFile = join(this.configDir, path.slice(1), 'config.ini');
|
|
147
|
+
public async exists(): Promise<boolean> {
|
|
148
|
+
try {
|
|
149
|
+
await fsp.access(this.currentFile);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
125
153
|
}
|
|
154
|
+
}
|
|
126
155
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
156
|
+
public async listPaths(): Promise<string[]> {
|
|
157
|
+
try {
|
|
158
|
+
const entries = await fsp.readdir(this.currentDir, { withFileTypes: true });
|
|
159
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
160
|
+
} catch {
|
|
161
|
+
return [];
|
|
130
162
|
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* RemoteConfig — HTTP client to an epistery-authority server. Implements the
|
|
168
|
+
* same ConfigStore interface; the authority mounts a LocalConfig behind it.
|
|
169
|
+
*
|
|
170
|
+
* Auth is the rivet key-exchange: the machine signs a challenge with its
|
|
171
|
+
* device key, and the authority issues a bearer token (see epistery-authority
|
|
172
|
+
* lib/auth.mjs). In Phase 1 config data (including the wallet mnemonic) is
|
|
173
|
+
* still served; Phase 2 splits public from secret and adds /sign/*.
|
|
174
|
+
*/
|
|
175
|
+
export class RemoteConfig implements ConfigStore {
|
|
176
|
+
public data: any = {};
|
|
177
|
+
private currentPath: string = '/';
|
|
178
|
+
private token: string | null = null;
|
|
131
179
|
|
|
132
|
-
|
|
133
|
-
|
|
180
|
+
constructor(
|
|
181
|
+
private readonly baseUrl: string,
|
|
182
|
+
private readonly machineAddress: string,
|
|
183
|
+
private readonly signChallenge: (message: string) => Promise<string>,
|
|
184
|
+
) {
|
|
185
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
134
186
|
}
|
|
135
187
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
188
|
+
public getPath(): string {
|
|
189
|
+
return this.currentPath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private async authenticate(): Promise<void> {
|
|
193
|
+
const fetch = (globalThis as any).fetch;
|
|
194
|
+
const cRes = await fetch(`${this.baseUrl}/auth/challenge`, {
|
|
195
|
+
method: 'POST',
|
|
196
|
+
headers: { 'content-type': 'application/json' },
|
|
197
|
+
body: JSON.stringify({ machineAddress: this.machineAddress }),
|
|
198
|
+
});
|
|
199
|
+
if (!cRes.ok) throw new Error(`authority challenge failed: ${cRes.status}`);
|
|
200
|
+
const { challenge } = await cRes.json();
|
|
201
|
+
|
|
202
|
+
const message = `Epistery Key Exchange - ${this.machineAddress} - ${challenge}`;
|
|
203
|
+
const signature = await this.signChallenge(message);
|
|
204
|
+
|
|
205
|
+
const vRes = await fetch(`${this.baseUrl}/auth/verify`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'content-type': 'application/json' },
|
|
208
|
+
body: JSON.stringify({ machineAddress: this.machineAddress, message, signature }),
|
|
209
|
+
});
|
|
210
|
+
if (!vRes.ok) throw new Error(`authority verify failed: ${vRes.status}`);
|
|
211
|
+
this.token = (await vRes.json()).token;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private async authedFetch(pathAndQuery: string, init: any = {}): Promise<any> {
|
|
215
|
+
const fetch = (globalThis as any).fetch;
|
|
216
|
+
if (!this.token) await this.authenticate();
|
|
217
|
+
const withAuth = () => ({ ...init, headers: { ...(init.headers || {}), authorization: `Bearer ${this.token}` } });
|
|
218
|
+
let res = await fetch(this.baseUrl + pathAndQuery, withAuth());
|
|
219
|
+
if (res.status === 401) {
|
|
220
|
+
this.token = null; // expired — re-auth once
|
|
221
|
+
await this.authenticate();
|
|
222
|
+
res = await fetch(this.baseUrl + pathAndQuery, withAuth());
|
|
142
223
|
}
|
|
224
|
+
return res;
|
|
225
|
+
}
|
|
143
226
|
|
|
144
|
-
|
|
145
|
-
|
|
227
|
+
public async setPath(path: string): Promise<void> {
|
|
228
|
+
this.currentPath = normalizePath(path);
|
|
229
|
+
await this.load();
|
|
146
230
|
}
|
|
147
231
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
*/
|
|
151
|
-
public readFile(filename: string): Buffer {
|
|
152
|
-
return fs.readFileSync(join(this.currentDir, filename));
|
|
232
|
+
public async load(): Promise<void> {
|
|
233
|
+
this.data = await this.read(this.currentPath);
|
|
153
234
|
}
|
|
154
235
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
236
|
+
public async read(path: string): Promise<any> {
|
|
237
|
+
path = normalizePath(path);
|
|
238
|
+
const res = await this.authedFetch(`/config${path === '/' ? '/' : path}`);
|
|
239
|
+
if (!res.ok) return {};
|
|
240
|
+
return (await res.json()).data || {};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public async save(): Promise<void> {
|
|
244
|
+
const path = this.currentPath;
|
|
245
|
+
const res = await this.authedFetch(`/config${path === '/' ? '/' : path}`, {
|
|
246
|
+
method: 'PUT',
|
|
247
|
+
headers: { 'content-type': 'application/json' },
|
|
248
|
+
body: JSON.stringify({ data: this.data }),
|
|
249
|
+
});
|
|
250
|
+
if (!res.ok) throw new Error(`authority save failed: ${res.status}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private filePrefix(): string {
|
|
254
|
+
return this.currentPath === '/' ? '' : this.currentPath;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
public async readFile(filename: string): Promise<Buffer> {
|
|
258
|
+
const res = await this.authedFetch(`/file${this.filePrefix()}/${filename}`);
|
|
259
|
+
if (!res.ok) throw new Error(`authority file read failed: ${res.status}`);
|
|
260
|
+
return Buffer.from(await res.arrayBuffer());
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
public async writeFile(filename: string, data: string | Buffer): Promise<void> {
|
|
264
|
+
const res = await this.authedFetch(`/file${this.filePrefix()}/${filename}`, {
|
|
265
|
+
method: 'PUT',
|
|
266
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
267
|
+
body: data,
|
|
268
|
+
});
|
|
269
|
+
if (!res.ok) throw new Error(`authority file write failed: ${res.status}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public async exists(): Promise<boolean> {
|
|
273
|
+
const data = await this.read(this.currentPath);
|
|
274
|
+
return !!data && Object.keys(data).length > 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public async listPaths(): Promise<string[]> {
|
|
278
|
+
const res = await this.authedFetch(`/paths${this.currentPath === '/' ? '/' : this.currentPath}`);
|
|
279
|
+
if (!res.ok) return [];
|
|
280
|
+
return (await res.json()).paths || [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Config — the public facade. `new Config()` selects the backend synchronously
|
|
286
|
+
* (reading only the local bootstrap), then delegates every async operation.
|
|
287
|
+
* Existing call sites change only by adding `await`.
|
|
288
|
+
*/
|
|
289
|
+
export class Config implements ConfigStore {
|
|
290
|
+
private readonly backend: ConfigStore;
|
|
291
|
+
private readonly local: LocalConfig;
|
|
292
|
+
|
|
293
|
+
constructor(rootName: string = 'epistery') {
|
|
294
|
+
this.local = new LocalConfig(rootName);
|
|
295
|
+
const authorityUrl = Config.resolveAuthorityUrl(rootName);
|
|
296
|
+
if (authorityUrl) {
|
|
297
|
+
const { machineAddress, sign } = Config.machineSigner(rootName);
|
|
298
|
+
this.backend = new RemoteConfig(authorityUrl, machineAddress, sign);
|
|
299
|
+
} else {
|
|
300
|
+
this.backend = this.local;
|
|
161
301
|
}
|
|
162
|
-
fs.writeFileSync(join(this.currentDir, filename), data);
|
|
163
302
|
}
|
|
164
303
|
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
304
|
+
/** Read the local bootstrap config.ini synchronously (authority selection only). */
|
|
305
|
+
private static readBootstrap(rootName: string): any {
|
|
306
|
+
const homeDir = (process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME) || '';
|
|
307
|
+
const file = join(homeDir, '.' + rootName, 'config.ini');
|
|
308
|
+
try {
|
|
309
|
+
return ini.decode(fs.readFileSync(file, 'utf8'));
|
|
310
|
+
} catch {
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private static resolveAuthorityUrl(rootName: string): string | null {
|
|
316
|
+
if (process.env.EPISTERY_CONFIG_URL) return process.env.EPISTERY_CONFIG_URL;
|
|
317
|
+
const boot = Config.readBootstrap(rootName);
|
|
318
|
+
return boot?.authority?.url || null;
|
|
170
319
|
}
|
|
171
320
|
|
|
172
321
|
/**
|
|
173
|
-
*
|
|
322
|
+
* Build the machine signer used to authenticate to the authority. The machine
|
|
323
|
+
* rivet key lives in the local bootstrap [authority] section (Phase 1). A
|
|
324
|
+
* future TPM-backed key replaces this without changing the call site.
|
|
174
325
|
*/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
326
|
+
private static machineSigner(rootName: string): { machineAddress: string; sign: (m: string) => Promise<string> } {
|
|
327
|
+
const boot = Config.readBootstrap(rootName);
|
|
328
|
+
const a = boot?.authority || {};
|
|
329
|
+
let wallet: ethers.Wallet;
|
|
330
|
+
if (a.machineMnemonic) {
|
|
331
|
+
wallet = ethers.Wallet.fromMnemonic(a.machineMnemonic);
|
|
332
|
+
} else if (a.machineKey) {
|
|
333
|
+
wallet = new ethers.Wallet(a.machineKey);
|
|
334
|
+
} else {
|
|
335
|
+
throw new Error(
|
|
336
|
+
'Config: [authority] url is set but no machine credential ([authority] machineMnemonic or machineKey) is configured to authenticate to it.',
|
|
337
|
+
);
|
|
178
338
|
}
|
|
179
|
-
|
|
180
|
-
return fs.readdirSync(this.currentDir, { withFileTypes: true })
|
|
181
|
-
.filter(dirent => dirent.isDirectory())
|
|
182
|
-
.map(dirent => dirent.name);
|
|
339
|
+
return { machineAddress: wallet.address, sign: (m: string) => wallet.signMessage(m) };
|
|
183
340
|
}
|
|
341
|
+
|
|
342
|
+
// Local-machine facts (always the bootstrap LocalConfig, even in remote mode):
|
|
343
|
+
// these are filesystem paths some callers need (e.g. CliWallet session dir).
|
|
344
|
+
public get rootName(): string { return this.local.rootName; }
|
|
345
|
+
public get homeDir(): string { return this.local.homeDir; }
|
|
346
|
+
public get configDir(): string { return this.local.configDir; }
|
|
347
|
+
|
|
348
|
+
public get data(): any { return this.backend.data; }
|
|
349
|
+
public set data(v: any) { this.backend.data = v; }
|
|
350
|
+
|
|
351
|
+
public getPath(): string { return this.backend.getPath(); }
|
|
352
|
+
public setPath(path: string): Promise<void> { return this.backend.setPath(path); }
|
|
353
|
+
public load(): Promise<void> { return this.backend.load(); }
|
|
354
|
+
public read(path: string): Promise<any> { return this.backend.read(path); }
|
|
355
|
+
public save(): Promise<void> { return this.backend.save(); }
|
|
356
|
+
public readFile(filename: string): Promise<Buffer> { return this.backend.readFile(filename); }
|
|
357
|
+
public writeFile(filename: string, data: string | Buffer): Promise<void> { return this.backend.writeFile(filename, data); }
|
|
358
|
+
public exists(): Promise<boolean> { return this.backend.exists(); }
|
|
359
|
+
public listPaths(): Promise<string[]> { return this.backend.listPaths(); }
|
|
184
360
|
}
|
|
185
361
|
|
|
186
362
|
const defaultIni =
|
|
@@ -224,4 +400,11 @@ nativeCurrencyDecimals=18
|
|
|
224
400
|
; minPriorityFeeGwei=25 ; Polygon RPC floor (don't lower)
|
|
225
401
|
; [default.rpc.81.policy]
|
|
226
402
|
; maxGasPriceGwei=1000 ; legacy-chain analogue (JOC)
|
|
227
|
-
|
|
403
|
+
|
|
404
|
+
; Shared Configuration Authority (optional). When set, Config reads/writes
|
|
405
|
+
; through the epistery-authority server instead of the local filesystem, so
|
|
406
|
+
; this host becomes a stateless pool member. EPISTERY_CONFIG_URL overrides url.
|
|
407
|
+
; [authority]
|
|
408
|
+
; url=https://epistery-authority-1.internal:4500
|
|
409
|
+
; machineMnemonic=... ; this machine's rivet (or machineKey=0x…)
|
|
410
|
+
`;
|
package/src/utils/Utils.ts
CHANGED
|
@@ -11,7 +11,7 @@ export class Utils {
|
|
|
11
11
|
// across concurrent requests for different domains on a multi-tenant host.
|
|
12
12
|
private static walletCache: Map<string, ethers.Wallet> = new Map();
|
|
13
13
|
|
|
14
|
-
public static InitServerWallet(domain: string = 'localhost'): ethers.Wallet | null {
|
|
14
|
+
public static async InitServerWallet(domain: string = 'localhost'): Promise<ethers.Wallet | null> {
|
|
15
15
|
// Fast path: return cached wallet for this domain. Avoids both the
|
|
16
16
|
// wallet-rebuild cost and the static config setPath mutation.
|
|
17
17
|
const cached = this.walletCache.get(domain);
|
|
@@ -25,7 +25,7 @@ export class Utils {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// Load domain config
|
|
28
|
-
this.config.setPath(domain);
|
|
28
|
+
await this.config.setPath(domain);
|
|
29
29
|
|
|
30
30
|
const domainConfig = this.config.data.domain ? this.config.data : {domain: domain};
|
|
31
31
|
|
|
@@ -41,10 +41,10 @@ export class Utils {
|
|
|
41
41
|
// "provider=undefined", and which on reload becomes a truthy
|
|
42
42
|
// string that bypasses the `||` fallback below at chainFor).
|
|
43
43
|
if (!domainConfig.provider) {
|
|
44
|
-
this.config.setPath('/');
|
|
44
|
+
await this.config.setPath('/');
|
|
45
45
|
domainConfig.provider =
|
|
46
46
|
this.config.data.default?.provider ?? this.config.data.provider;
|
|
47
|
-
this.config.setPath(domain); // Switch back to domain
|
|
47
|
+
await this.config.setPath(domain); // Switch back to domain
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (!domainConfig.wallet) {
|
|
@@ -69,7 +69,7 @@ export class Utils {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
this.config.data = domainConfig;
|
|
72
|
-
this.config.save();
|
|
72
|
+
await this.config.save();
|
|
73
73
|
|
|
74
74
|
console.log(`[debug] Created new wallet for domain: ${domain}`);
|
|
75
75
|
console.log(`[debug] Wallet address: ${wallet.address}`);
|
|
@@ -98,6 +98,17 @@ export class Utils {
|
|
|
98
98
|
return this.serverWallet;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Synchronous accessor for a domain's already-initialized wallet, read from
|
|
103
|
+
* the per-domain cache. Returns null if InitServerWallet(domain) has not been
|
|
104
|
+
* awaited yet. This lets the synchronous `get signer()` getter survive the
|
|
105
|
+
* async migration: setDomain() awaits InitServerWallet to warm the cache, and
|
|
106
|
+
* the getter reads it here without re-entering async config IO.
|
|
107
|
+
*/
|
|
108
|
+
public static GetServerWalletFor(domain: string): ethers.Wallet | null {
|
|
109
|
+
return this.walletCache.get(domain) || null;
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
public static GetConfig(): Config {
|
|
102
113
|
if (!this.config) {
|
|
103
114
|
this.config = new Config();
|
|
@@ -105,13 +116,12 @@ export class Utils {
|
|
|
105
116
|
return this.config;
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
public static GetDomainInfo(domain: string = 'localhost'): DomainConfig {
|
|
119
|
+
public static async GetDomainInfo(domain: string = 'localhost'): Promise<DomainConfig> {
|
|
109
120
|
if (!this.config) {
|
|
110
121
|
this.config = new Config();
|
|
111
122
|
}
|
|
112
123
|
|
|
113
|
-
this.config.setPath(`/${domain}`);
|
|
114
|
-
this.config.load();
|
|
124
|
+
await this.config.setPath(`/${domain}`);
|
|
115
125
|
|
|
116
126
|
if (!this.config.data.domain)
|
|
117
127
|
return {domain:domain};
|
|
@@ -126,7 +136,7 @@ export class Utils {
|
|
|
126
136
|
// read epistery.domain.provider.rpc (e.g. connect's on-chain contract
|
|
127
137
|
// verification) get no RPC. read('/') doesn't move the current path.
|
|
128
138
|
if (!domainConfig.provider) {
|
|
129
|
-
const rootData = this.config.read('/');
|
|
139
|
+
const rootData = await this.config.read('/');
|
|
130
140
|
domainConfig.provider = rootData.default?.provider ?? rootData.provider;
|
|
131
141
|
}
|
|
132
142
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
domain=127.0.0.1
|
|
2
|
+
|
|
3
|
+
[provider]
|
|
4
|
+
chainId=80002
|
|
5
|
+
name=Polygon Amoy Testnet
|
|
6
|
+
rpc=https://rpc-amoy.polygon.technology
|
|
7
|
+
nativeCurrencyName=POL
|
|
8
|
+
nativeCurrencySymbol=POL
|
|
9
|
+
nativeCurrencyDecimals=18
|
|
10
|
+
|
|
11
|
+
[wallet]
|
|
12
|
+
address=0x94C8407888499483CB6795cd5155801B9e767515
|
|
13
|
+
mnemonic=meadow chimney grain search thrive away visual grace wear powder giant profit
|
|
14
|
+
publicKey=0x04098c8c61f007b937dc9f5ec63ec41857b4b50ece763d8c1501876f3ae33b8758d7cdf0e4cb6cf44f715b7e8395b9ee81b0ff258816a1b6ce9ea76e91626962eb
|
|
15
|
+
privateKey=0x4f2bd152b2c3b08472ed4e61297058d8df367bd96e66ee76fed6ad778de92aaa
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[profile]
|
|
2
|
+
name=Test Profile
|
|
3
|
+
email=test@epistery.test
|
|
4
|
+
|
|
5
|
+
[default.provider]
|
|
6
|
+
chainId=80002
|
|
7
|
+
name=Polygon Amoy Testnet
|
|
8
|
+
rpc=https://rpc-amoy.polygon.technology
|
|
9
|
+
nativeCurrencyName=POL
|
|
10
|
+
nativeCurrencySymbol=POL
|
|
11
|
+
nativeCurrencyDecimals=18
|
|
12
|
+
|
|
13
|
+
[cli]
|
|
14
|
+
default_domain=localhost
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
domain=localhost
|
|
2
|
+
pending=true
|
|
3
|
+
|
|
4
|
+
[wallet]
|
|
5
|
+
address=0x5277286b5821D1bF5c2395F38c3250A5B0F14da3
|
|
6
|
+
mnemonic=attack sweet hint arctic skull vessel refuse mountain tiny kind feature amazing
|
|
7
|
+
publicKey=0x041de8ef7ae49c387ed8f8c138f33901acefdd63e03300dbd205bd407efba3b95002b5337ce4437924b52c9581cf7bda21eb6df5f0d3e17fbd99ccddfefc63f0d8
|
|
8
|
+
privateKey=0x91806b336879df60435ab8f95673bbdd7b24d47dec5ac35c10098934f214049d
|
|
9
|
+
|
|
10
|
+
[provider]
|
|
11
|
+
name=Polygon Amoy Testnet
|
|
12
|
+
chainId=80002
|
|
13
|
+
rpc=https://rpc-amoy.polygon.technology
|
|
14
|
+
nativeCurrencySymbol=POL
|
|
15
|
+
nativeCurrencyName=POL
|
|
16
|
+
nativeCurrencyDecimals=18
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
domain=no-pending-claim.local
|
|
2
|
+
|
|
3
|
+
[provider]
|
|
4
|
+
chainId=80002
|
|
5
|
+
name=Polygon Amoy Testnet
|
|
6
|
+
rpc=https://rpc-amoy.polygon.technology
|
|
7
|
+
nativeCurrencyName=POL
|
|
8
|
+
nativeCurrencySymbol=POL
|
|
9
|
+
nativeCurrencyDecimals=18
|
|
10
|
+
|
|
11
|
+
[wallet]
|
|
12
|
+
address=0xaDBD5bBE73B80430E047A3496d277B05191c45CA
|
|
13
|
+
mnemonic=devote link lens finger february close soda harbor urge chunk zoo knife
|
|
14
|
+
publicKey=0x042c874d737d4c4620bc03a255ebf7733f369c603e8bdf8cbcddbdd0042d257e06f684374b1d4502d2db202ced4dcaa7139c9ec92c93deafa2ca718265a9ad8580
|
|
15
|
+
privateKey=0x5b02e2b6f91e742d52d4d75b91be1cea416561fb67a4873c2842ba0b2a023dfd
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
domain=test-claim-1782325887560.local
|
|
2
|
+
pending=true
|
|
3
|
+
challenge_token=3027fd6c76df13ea5c5a22c8e89b496cf541e6a55484fb67dc20df4c1b033ed6
|
|
4
|
+
challenge_address=0xdcb8454e5ce6b911dba93c11ad530c61d2520d8e
|
|
5
|
+
challenge_created=2026-06-24T18:31:27.608Z
|
|
6
|
+
challenge_requester_ip=::ffff:127.0.0.1
|
|
7
|
+
|
|
8
|
+
[provider]
|
|
9
|
+
name=Polygon Amoy Testnet
|
|
10
|
+
chainId=80002
|
|
11
|
+
rpc=https://rpc-amoy.polygon.technology
|
|
12
|
+
nativeCurrencySymbol=POL
|
|
13
|
+
nativeCurrencyName=POL
|
|
14
|
+
nativeCurrencyDecimals=18
|
|
15
|
+
|
|
16
|
+
[wallet]
|
|
17
|
+
address=0xcB676926b57659261Cb4C8bA765E895E9A61363B
|
|
18
|
+
mnemonic=clinic tragic regular leave sunset expect manage rally street derive swap rapid
|
|
19
|
+
publicKey=0x047389a40eb30c1da9f0fe88d4e05df15f4940022f5c1b3928b4de08f205274ba220d19811d2d8ab5dac45e0c0ff2caf6dad1db8e834354df65da2973e306fdbc8
|
|
20
|
+
privateKey=0xa26ab628421a96202f49516d236c15144cc9825c636146ddb3091f649e51501b
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
domain=test-idempotent-1782325887610.local
|
|
2
|
+
pending=true
|
|
3
|
+
challenge_token=4a6b22e1c5fbcc409a6a7b2818cc59adcdc209c9f17a830f6597a154112a393d
|
|
4
|
+
challenge_address=0xdcb8454e5ce6b911dba93c11ad530c61d2520d8e
|
|
5
|
+
challenge_created=2026-06-24T18:31:27.654Z
|
|
6
|
+
challenge_requester_ip=::ffff:127.0.0.1
|
|
7
|
+
|
|
8
|
+
[provider]
|
|
9
|
+
name=Polygon Amoy Testnet
|
|
10
|
+
chainId=80002
|
|
11
|
+
rpc=https://rpc-amoy.polygon.technology
|
|
12
|
+
nativeCurrencySymbol=POL
|
|
13
|
+
nativeCurrencyName=POL
|
|
14
|
+
nativeCurrencyDecimals=18
|
|
15
|
+
|
|
16
|
+
[wallet]
|
|
17
|
+
address=0x3088095FfBcfB4c5c41bF2013aaC445925D97053
|
|
18
|
+
mnemonic=jump youth pigeon erupt pigeon decorate pattern evil cage piece mean dawn
|
|
19
|
+
publicKey=0x04a6c3b50b9c644e9165c694b2aa999cf68dcb2c84d0d25944e77dbbce02efab7415726e7d73117df9923431238d1b2ba9041c1f0dc623102fd57d3bf80ec662ba
|
|
20
|
+
privateKey=0x759580a4cf6b6d0fe1903b59071186e2c6ecc6224d5c2852e76a031ea1218acf
|