epistery 2.0.4 → 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/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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Rivet Signer & Config Authority — Interface Spec
|
|
2
|
+
|
|
3
|
+
Status: draft for review · 2026-06-23 · feeds RootzOracleTenancy · repo: `epistery-authority` (instance `epistery-authority-1`)
|
|
4
|
+
|
|
5
|
+
This specifies the **interfaces** for turning `~/.epistery` local state into a shared
|
|
6
|
+
configuration/key authority, so pool members become stateless and interchangeable.
|
|
7
|
+
Interfaces first — no implementation here. It is the contract the authority, the
|
|
8
|
+
`Config` rewrite, and the client/server signers all build against.
|
|
9
|
+
|
|
10
|
+
## 0. The one idea
|
|
11
|
+
|
|
12
|
+
A **rivet** is *an unextractable-but-usable signing identity*. The private key lives
|
|
13
|
+
with a **custodian** that never hands it out; callers get **signatures**, not keys.
|
|
14
|
+
|
|
15
|
+
This is already true client-side. `client/wallet.js` `RivetWallet` keeps the secp256k1
|
|
16
|
+
key encrypted under a non-extractable WebCrypto AES key in IndexedDB and only ever
|
|
17
|
+
decrypts it inside a function closure that signs and returns (`wallet.js:399-434`,
|
|
18
|
+
`446-490`); `FidoWallet` wraps the key with a WebAuthn-PRF-derived key in the Secure
|
|
19
|
+
Enclave (`wallet.js:1272-1316`). The key never escapes the custodian.
|
|
20
|
+
|
|
21
|
+
The **server rivet** is the same object with a different custodian: the domain wallet's
|
|
22
|
+
key lives in `epistery-authority-1` and is used via a signing RPC. Today that key is the
|
|
23
|
+
plaintext mnemonic in `~/.epistery/<domain>/config.ini`, and **six call sites
|
|
24
|
+
independently re-derive a wallet from it** — `Utils.ts:80`, `epistery.ts:109` & `:170`,
|
|
25
|
+
host `DomainChain.mjs:78` & `index.mjs:239`, `relay/index.mjs:141`
|
|
26
|
+
(plus `message-board` and `chat/` deploy scripts). That scatter is the parallel-channel
|
|
27
|
+
we are collapsing into one owner.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
custodian (holds key, never releases) rivet (used via signatures)
|
|
31
|
+
client WebCrypto/IndexedDB | FIDO Secure Enclave RivetWallet | FidoWallet
|
|
32
|
+
server epistery-config-1 (HSM/TPM) RemoteSigner (domain wallet)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The custodian/rivet split is the shared abstraction. The authority is to the server
|
|
36
|
+
rivet what the FIDO authenticator is to the client rivet.
|
|
37
|
+
|
|
38
|
+
## 1. `Rivet` — the capability interface (shared)
|
|
39
|
+
|
|
40
|
+
The narrow async contract every rivet satisfies, regardless of custodian. All methods
|
|
41
|
+
are async because a remote/HSM/FIDO custodian cannot answer synchronously.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export interface Rivet {
|
|
45
|
+
readonly address: string; // the rivet (signer) address — always present
|
|
46
|
+
readonly publicKey: string; // uncompressed secp256k1, 0x04…
|
|
47
|
+
getAddress(): Promise<string>;
|
|
48
|
+
signMessage(message: string | Uint8Array): Promise<string>;
|
|
49
|
+
signTransaction(tx: UnsignedTransaction): Promise<string>; // returns raw signed tx hex
|
|
50
|
+
// Optional ECDH peer crypto — implemented by custodians that can compute a
|
|
51
|
+
// shared secret without exposing the key (RivetWallet/FidoWallet do this today,
|
|
52
|
+
// wallet.js:495-530, 1321-1331). The authority MAY offer it as an RPC.
|
|
53
|
+
encryptForPeer?(peerPublicKey: string, plaintext: Uint8Array): Promise<EciesBlob>;
|
|
54
|
+
decryptFromPeer?(peerPublicKey: string, blob: EciesBlob): Promise<Uint8Array>;
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Notes:
|
|
59
|
+
- This intentionally matches ethers v5 `Signer`'s `getAddress`/`signMessage`/
|
|
60
|
+
`signTransaction`. The **server realization of `Rivet` is an `ethers.Signer`**
|
|
61
|
+
(`RemoteSigner extends ethers.Signer`) because every server caller already consumes
|
|
62
|
+
a `Signer` (`Utils.InitServerWallet` returns `ethers.Wallet` today). So adopting
|
|
63
|
+
`Rivet` on the server is a no-op at the call sites — only the construction changes.
|
|
64
|
+
- The **client `Wallet` hierarchy is adapted to `Rivet`**: drop the `ethers` argument
|
|
65
|
+
threaded through `sign(message, ethers)`/`signTransaction(tx, ethers)` (the browser
|
|
66
|
+
no-bundler quirk) by binding `ethers` at construction; expose `getAddress`. The
|
|
67
|
+
identity getters (`signerAddress`, `identityAddress`, `wallet.js:36-44`) stay — they
|
|
68
|
+
are an identity concern layered above signing, not part of `Rivet`.
|
|
69
|
+
|
|
70
|
+
## 2. Two server rivets, layered
|
|
71
|
+
|
|
72
|
+
| Rivet | Custodian | Unextractable | Purpose |
|
|
73
|
+
|---|---|---|---|
|
|
74
|
+
| **machine/device rivet** | the box's TPM (self-minted per instance, per the wiki) | yes (hardware) | authenticates *this host* to the authority |
|
|
75
|
+
| **domain server rivet** | `epistery-authority-1` | Phase 2: yes | the on-chain identity the authority wields *on behalf of* authorized machines |
|
|
76
|
+
|
|
77
|
+
A pool member never holds the domain key. It proves itself with its machine rivet, and
|
|
78
|
+
borrows the domain rivet's signature from the authority. **N stateless machines share
|
|
79
|
+
one on-chain domain identity without sharing its key** — the wiki's statelessness and
|
|
80
|
+
its security story in one sentence.
|
|
81
|
+
|
|
82
|
+
## 3. `Config` becomes async (breaking)
|
|
83
|
+
|
|
84
|
+
Decided: all clients upgrade; no sync-snapshot shim. `Config` keeps its path-based shape
|
|
85
|
+
(`src/utils/Config.ts`) but `load`/`save`/`setPath`/`read` become async fetch/push points;
|
|
86
|
+
`.data` reflects the last `await`ed load.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
export interface ConfigStore {
|
|
90
|
+
setPath(path: string): Promise<void>; // was sync; now awaits a fetch in remote mode
|
|
91
|
+
getPath(): string;
|
|
92
|
+
load(): Promise<void>;
|
|
93
|
+
read(path: string): Promise<any>; // read without moving current path
|
|
94
|
+
save(): Promise<void>;
|
|
95
|
+
readFile(name: string): Promise<Buffer>;
|
|
96
|
+
writeFile(name: string, data: string | Buffer): Promise<void>;
|
|
97
|
+
exists(): Promise<boolean>;
|
|
98
|
+
listPaths(): Promise<string[]>;
|
|
99
|
+
data: any; // snapshot from the last load()
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Backend selection** happens in the `Config` constructor:
|
|
104
|
+
- If local `~/.epistery/config.ini` has `[authority] url=…`, or `EPISTERY_CONFIG_URL`
|
|
105
|
+
is set → `RemoteConfig` (talks to `epistery-config-1`).
|
|
106
|
+
- Else → `LocalConfig` (today's filesystem behavior, unchanged).
|
|
107
|
+
|
|
108
|
+
The local file never disappears — in remote mode it shrinks to **bootstrap state**: the
|
|
109
|
+
authority URL and this machine's credential (its machine-rivet handle). That is the
|
|
110
|
+
"machine data" the wiki carves out and the only per-box state a pool member keeps.
|
|
111
|
+
|
|
112
|
+
**Public/secret split in the data model.** Today `[wallet]` co-mingles public
|
|
113
|
+
(`address`, `publicKey`) and secret (`mnemonic`, `privateKey`) — `types.ts:18-23`. The
|
|
114
|
+
authority must serve the public fields freely and treat the secret as custodied:
|
|
115
|
+
|
|
116
|
+
| Class | Examples | Phase 1 | Phase 2 |
|
|
117
|
+
|---|---|---|---|
|
|
118
|
+
| public config | provider, contract addresses, claim state, prefs | served R/W | served R/W |
|
|
119
|
+
| signing key | wallet mnemonic / privateKey | released | **never released — authority signs** |
|
|
120
|
+
| opaque secret blob | TLS cert+key, storj/S3, mongo password | released to authorized machines | released (governed safe; cannot be HSM'd) |
|
|
121
|
+
|
|
122
|
+
**The authority is also a safe.** `readFile`/`writeFile` already persist arbitrary
|
|
123
|
+
blobs (`Config.ts:151-163`), and `Config`'s `.ssl/<domain>` path is already in the path
|
|
124
|
+
model — so the authority is two things at once: an **HSM** for signing keys (never
|
|
125
|
+
released) and a **safe** for opaque blobs (governed release via `/file/*path/:name`).
|
|
126
|
+
Anything that is a *bearer* secret — a key the host must itself present to a third party
|
|
127
|
+
or to a TLS handshake — lives in the safe, because the authority can't wield it remotely
|
|
128
|
+
the way it wields an Ethereum signing key.
|
|
129
|
+
|
|
130
|
+
## 4. Authority HTTP API
|
|
131
|
+
|
|
132
|
+
Auth is the **rivet key-exchange already in the codebase** (`Epistery.handleKeyExchange`,
|
|
133
|
+
`epistery.ts:82-137`; wire shapes `KeyExchangeRequest`/`Response`, `types.ts:98-117`),
|
|
134
|
+
pointed at the authority. One mechanism, not a bespoke machine-auth.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
POST /auth/challenge { machineAddress } -> { challenge }
|
|
138
|
+
POST /auth/verify { machineAddress, message, signature, publicKey }
|
|
139
|
+
message = "Epistery Key Exchange - <machineAddress> - <challenge>"
|
|
140
|
+
-> { token } # session/bearer
|
|
141
|
+
|
|
142
|
+
# Config data (token-gated; ACL decides which paths this machine may see)
|
|
143
|
+
GET /config/*path -> { data } # public fields only
|
|
144
|
+
PUT /config/*path { data } -> { ok }
|
|
145
|
+
GET /config/*path/_paths -> { paths: string[] }
|
|
146
|
+
GET /file/*path/:name -> bytes
|
|
147
|
+
PUT /file/*path/:name <- bytes
|
|
148
|
+
|
|
149
|
+
# Server rivet — public info always; key material governed
|
|
150
|
+
GET /wallet/:domain -> { address, publicKey } # never the key
|
|
151
|
+
POST /sign/message { domain, message } -> { signature } # Phase 2
|
|
152
|
+
POST /sign/transaction { domain, tx } -> { signedTransaction } # Phase 2
|
|
153
|
+
GET /secret/:domain/:name -> { value } # governed release (storj/mongo)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Governance ("like a CA"): the authority's ACL — keyed on the **machine rivet** — decides
|
|
157
|
+
which domains a host may read and which signing ops it may request. That is the
|
|
158
|
+
authority's reason to exist over a network share.
|
|
159
|
+
|
|
160
|
+
## 5. `RemoteSigner` — server-side rivet realization (Phase 2)
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
class RemoteSigner extends ethers.Signer implements Rivet {
|
|
164
|
+
constructor(domain, authorityClient, provider) { super(); /* … */ }
|
|
165
|
+
async getAddress() { return (await this.authority.wallet(this.domain)).address; }
|
|
166
|
+
async signMessage(message) { return this.authority.signMessage(this.domain, message); }
|
|
167
|
+
async signTransaction(tx) { return this.authority.signTransaction(this.domain, tx); }
|
|
168
|
+
connect(provider) { return new RemoteSigner(this.domain, this.authority, provider); }
|
|
169
|
+
// sendTransaction is inherited: signTransaction (remote) then provider.broadcast (local) —
|
|
170
|
+
// so RPC/broadcast stay on the host; only the signature is remote.
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`Utils.InitServerWallet(domain)` is the **single owner** of "get a signer for this
|
|
175
|
+
domain." It returns a `LocalWallet` (today / Phase 1) or a `RemoteSigner` (Phase 2)
|
|
176
|
+
behind the same `ethers.Signer` type. The six scattered `fromMnemonic` sites all route
|
|
177
|
+
through it — that consolidation is Phase 0 and is what makes Phase 2 a transport swap
|
|
178
|
+
rather than a client rewrite.
|
|
179
|
+
|
|
180
|
+
## 6. How each consumer adapts
|
|
181
|
+
|
|
182
|
+
- **epistery-host** — `await` the new `Config`; reads domain config/claim state from the
|
|
183
|
+
authority; gets its domain signer from `Utils.InitServerWallet` (already does, via
|
|
184
|
+
`app.locals.epistery.signer`). `DomainChain.mjs:78` & `index.mjs:239` stop calling
|
|
185
|
+
`fromMnemonic` and ask `Utils`. TLS certs are a separate workstream (see §8).
|
|
186
|
+
- **epistery-app** — minimal: `await` the root config reads (`relayUrl`, `helpOwner`);
|
|
187
|
+
manifest signing already goes through `epistery.signer`, so no signer change.
|
|
188
|
+
- **relay** — **subordinate, not merged.** Keeps credit metering, broadcast, and on-chain
|
|
189
|
+
identity *reads*. Its pool wallet (`index.mjs:141`) becomes a domain rivet custodied by
|
|
190
|
+
the authority: drop `fromMnemonic`, fetch a signer from the authority. Relay then pools
|
|
191
|
+
too.
|
|
192
|
+
- **cli** (`CliWallet.ts`) — `await` Config; `CliWallet.create/load` obtain signers via
|
|
193
|
+
the same path instead of `fromMnemonic`/`new Wallet(privateKey)` (`CliWallet.ts:153-156`).
|
|
194
|
+
|
|
195
|
+
## 7. Invariants
|
|
196
|
+
|
|
197
|
+
1. One owner for "get a signer": `Utils.InitServerWallet`. No other site derives a wallet
|
|
198
|
+
from key material.
|
|
199
|
+
2. The custodian never emits a signing key in Phase 2. `GET /wallet/:domain` returns
|
|
200
|
+
address/publicKey only.
|
|
201
|
+
3. The rivet key-exchange is the only host↔authority auth mechanism.
|
|
202
|
+
4. `Rivet` is the shared capability; `ethers.Signer` is its server realization; the client
|
|
203
|
+
`Wallet` hierarchy is adapted to it. No second signing abstraction.
|
|
204
|
+
|
|
205
|
+
## 8. Open gaps (call out, don't assume)
|
|
206
|
+
|
|
207
|
+
- **TLS certs — not a blocker.** Certs persist as-is through the safe (`writeFile`/
|
|
208
|
+
`readFile` + `/file/.ssl/<domain>/…`), so the wiki's "certs live in the shared authority"
|
|
209
|
+
goal needs no special machinery: a pool member fetches its cert+key blob and self-
|
|
210
|
+
terminates TLS, NLB stays dumb L4. Certify/ACME (`@metric-im/administrate`) can keep
|
|
211
|
+
issuing and just write the result into the safe. A more elegant SSL flow (authority-
|
|
212
|
+
driven issuance/rotation) is possible later but is **not important now**.
|
|
213
|
+
- **Nonce/ordering.** Once one domain rivet signs for N hosts, transaction nonce management
|
|
214
|
+
must centralize (at the authority or a per-domain sequencer) or concurrent hosts collide.
|
|
215
|
+
- **Availability.** The authority becomes a hard dependency for any signing host. Needs a
|
|
216
|
+
bootstrap cache / degraded-mode policy decision.
|
|
217
|
+
- **rootz-v6 alignment.** This mirrors the identity-owned Secret pattern; align at the
|
|
218
|
+
Solidity/interface level only, per standing guidance — no shared library yet.
|
|
219
|
+
```
|
package/index.mjs
CHANGED
|
@@ -11,13 +11,12 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
|
|
13
13
|
// Helper function to get or create domain configurations src/utils/Config.ts system
|
|
14
|
-
function getDomainConfig(domain) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return domainConfig;
|
|
14
|
+
async function getDomainConfig(domain) {
|
|
15
|
+
// InitServerWallet warms the per-domain wallet cache (and creates+persists a
|
|
16
|
+
// wallet on first touch). Awaiting it here keeps the synchronous `get signer()`
|
|
17
|
+
// getter able to read the cache via Utils.GetServerWalletFor.
|
|
18
|
+
await Utils.InitServerWallet(domain);
|
|
19
|
+
return await Utils.GetDomainInfo(domain);
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
class EpisteryAttach {
|
|
@@ -36,7 +35,7 @@ class EpisteryAttach {
|
|
|
36
35
|
|
|
37
36
|
async setDomain(domain) {
|
|
38
37
|
this.domainName = domain;
|
|
39
|
-
this.domain = getDomainConfig(domain);
|
|
38
|
+
this.domain = await getDomainConfig(domain);
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
/**
|
|
@@ -45,7 +44,9 @@ class EpisteryAttach {
|
|
|
45
44
|
*/
|
|
46
45
|
get signer() {
|
|
47
46
|
if (!this.domainName) return null;
|
|
48
|
-
|
|
47
|
+
// Synchronous read of the cache warmed by setDomain()→getDomainConfig()→
|
|
48
|
+
// InitServerWallet. Keeps `.signer` a sync getter despite the async config.
|
|
49
|
+
return Utils.GetServerWalletFor(this.domainName) || null;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
/**
|
package/package.json
CHANGED
package/routes/auth.mjs
CHANGED
|
@@ -25,7 +25,7 @@ export default function authRoutes(epistery) {
|
|
|
25
25
|
.json({ status: "error", message: "Domain not found" });
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
epistery.config.setPath(domain);
|
|
28
|
+
await epistery.config.setPath(domain);
|
|
29
29
|
|
|
30
30
|
if (epistery.config.data && epistery.config.data.verified) {
|
|
31
31
|
return res
|
|
@@ -97,7 +97,7 @@ export default function authRoutes(epistery) {
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
epistery.config.setPath(domain);
|
|
100
|
+
await epistery.config.setPath(domain);
|
|
101
101
|
|
|
102
102
|
if (epistery.config.data && epistery.config.data.verified) {
|
|
103
103
|
return res
|
|
@@ -125,7 +125,7 @@ export default function authRoutes(epistery) {
|
|
|
125
125
|
epistery.config.data.challenge_requester_ip = req.ip;
|
|
126
126
|
epistery.config.data.provider = providerConfig;
|
|
127
127
|
|
|
128
|
-
epistery.config.save();
|
|
128
|
+
await epistery.config.save();
|
|
129
129
|
console.log(
|
|
130
130
|
`Domain claim initiated: ${domain} by ${normalizedClientAddress}`,
|
|
131
131
|
);
|
|
@@ -149,7 +149,7 @@ export default function authRoutes(epistery) {
|
|
|
149
149
|
.json({ status: "error", message: "Domain not found" });
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
epistery.config.setPath(domain);
|
|
152
|
+
await epistery.config.setPath(domain);
|
|
153
153
|
|
|
154
154
|
if (!epistery.config.data.pending) {
|
|
155
155
|
return res.status(400).json({
|
|
@@ -208,7 +208,7 @@ export default function authRoutes(epistery) {
|
|
|
208
208
|
delete epistery.config.data.challenge_token;
|
|
209
209
|
delete epistery.config.data.challenge_address;
|
|
210
210
|
delete epistery.config.data.challenge_requester_ip;
|
|
211
|
-
epistery.config.save();
|
|
211
|
+
await epistery.config.save();
|
|
212
212
|
|
|
213
213
|
res.json({ status: "success", message: "Domain claimed successfully" });
|
|
214
214
|
} catch (error) {
|
|
@@ -229,7 +229,7 @@ export default function authRoutes(epistery) {
|
|
|
229
229
|
return res.json({ isAdmin: false });
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
epistery.config.setPath(domain);
|
|
232
|
+
await epistery.config.setPath(domain);
|
|
233
233
|
|
|
234
234
|
if (
|
|
235
235
|
!epistery.config.data ||
|
package/routes/domain.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export default function domainRoutes(epistery) {
|
|
|
24
24
|
|
|
25
25
|
// Check if domain already exists
|
|
26
26
|
const config = Utils.GetConfig();
|
|
27
|
-
config.setPath(domain);
|
|
27
|
+
await config.setPath(domain);
|
|
28
28
|
|
|
29
29
|
let domainConfig = config.data;
|
|
30
30
|
if (!domainConfig.domain) domainConfig.domain = domain;
|
|
@@ -37,7 +37,7 @@ export default function domainRoutes(epistery) {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
// Save domain config with custom provider (marked as pending)
|
|
40
|
-
config.save();
|
|
40
|
+
await config.save();
|
|
41
41
|
|
|
42
42
|
res.json({
|
|
43
43
|
status: "success",
|
package/routes/fido.mjs
CHANGED
|
@@ -64,8 +64,8 @@ export default function fidoRoutes(epistery) {
|
|
|
64
64
|
return res.status(500).json({ error: "Domain not set" });
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
epistery.config.setPath(`/${domain}/fido`);
|
|
68
|
-
epistery.config.writeFile(
|
|
67
|
+
await epistery.config.setPath(`/${domain}/fido`);
|
|
68
|
+
await epistery.config.writeFile(
|
|
69
69
|
`${credentialId}.json`,
|
|
70
70
|
JSON.stringify({
|
|
71
71
|
credentialId,
|
|
@@ -97,11 +97,11 @@ export default function fidoRoutes(epistery) {
|
|
|
97
97
|
return res.status(500).json({ error: "Domain not set" });
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
epistery.config.setPath(`/${domain}/fido`);
|
|
100
|
+
await epistery.config.setPath(`/${domain}/fido`);
|
|
101
101
|
|
|
102
102
|
let buf;
|
|
103
103
|
try {
|
|
104
|
-
buf = epistery.config.readFile(`${credentialId}.json`);
|
|
104
|
+
buf = await epistery.config.readFile(`${credentialId}.json`);
|
|
105
105
|
} catch (e) {
|
|
106
106
|
return res.status(404).json({ error: "Blob not found" });
|
|
107
107
|
}
|
package/src/chains/registry.ts
CHANGED
|
@@ -82,9 +82,9 @@ export function registeredChains(): ChainConfig[] {
|
|
|
82
82
|
*
|
|
83
83
|
* Chains without a config override are returned unchanged.
|
|
84
84
|
*/
|
|
85
|
-
export function configuredChains(): ChainConfig[] {
|
|
85
|
+
export async function configuredChains(): Promise<ChainConfig[]> {
|
|
86
86
|
const config = new Config();
|
|
87
|
-
const rootData = config.read('/');
|
|
87
|
+
const rootData = await config.read('/');
|
|
88
88
|
return registeredChains().map(chain => {
|
|
89
89
|
const id = String(chain.chainId);
|
|
90
90
|
const privateRpc = rootData?.default?.rpc?.[id]?.privateRpc
|
|
@@ -101,9 +101,9 @@ export function configuredChains(): ChainConfig[] {
|
|
|
101
101
|
* Checks `[default] defaultChainId`, then `[default.provider] chainId`,
|
|
102
102
|
* falling back to Polygon mainnet (137).
|
|
103
103
|
*/
|
|
104
|
-
export function defaultChainId(): string {
|
|
104
|
+
export async function defaultChainId(): Promise<string> {
|
|
105
105
|
const config = new Config();
|
|
106
|
-
const rootData = config.read('/');
|
|
106
|
+
const rootData = await config.read('/');
|
|
107
107
|
return String(
|
|
108
108
|
rootData?.default?.defaultChainId
|
|
109
109
|
|| rootData?.default?.provider?.chainId
|
package/src/epistery.ts
CHANGED
|
@@ -43,7 +43,7 @@ export class Epistery {
|
|
|
43
43
|
return clientWalletInfo;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
public static getStatus(client: ClientWalletInfo, server: DomainConfig): EpisteryStatus {
|
|
46
|
+
public static async getStatus(client: ClientWalletInfo, server: DomainConfig): Promise<EpisteryStatus> {
|
|
47
47
|
// Build nativeCurrency object from flat fields with sensible defaults
|
|
48
48
|
let nativeCurrency = undefined;
|
|
49
49
|
if (server?.provider?.nativeCurrencySymbol) {
|
|
@@ -56,7 +56,7 @@ export class Epistery {
|
|
|
56
56
|
|
|
57
57
|
// Read IPFS config from root config
|
|
58
58
|
const config = Utils.GetConfig();
|
|
59
|
-
const rootConfig = config.read('/');
|
|
59
|
+
const rootConfig = await config.read('/');
|
|
60
60
|
const ipfsConfig = rootConfig.ipfs;
|
|
61
61
|
|
|
62
62
|
const status: EpisteryStatus = {
|
|
@@ -155,7 +155,7 @@ export class Epistery {
|
|
|
155
155
|
): Promise<any> {
|
|
156
156
|
// RPC comes from ~/.epistery via Config (GetDomainInfo falls back to root
|
|
157
157
|
// [provider]); never process.env — env is for deployment vars only.
|
|
158
|
-
const domainInfo = Utils.GetDomainInfo(domain);
|
|
158
|
+
const domainInfo = await Utils.GetDomainInfo(domain);
|
|
159
159
|
const rpcUrl = domainInfo?.provider?.rpc;
|
|
160
160
|
if (!rpcUrl) {
|
|
161
161
|
throw new Error(`No provider RPC configured in ~/.epistery for domain "${domain}"`);
|
|
@@ -307,7 +307,7 @@ export class Epistery {
|
|
|
307
307
|
// RPC from ~/.epistery root config via Config — not process.env. A signed
|
|
308
308
|
// tx already encodes its chainId; we just need the network's RPC, which
|
|
309
309
|
// single-domain apps declare once at root [provider].
|
|
310
|
-
const rootData = Utils.GetConfig().read('/');
|
|
310
|
+
const rootData = await Utils.GetConfig().read('/');
|
|
311
311
|
const rpcUrl = rootData.provider?.rpc ?? rootData.default?.provider?.rpc;
|
|
312
312
|
if (!rpcUrl) {
|
|
313
313
|
throw new Error('No provider RPC configured in ~/.epistery (root [provider])');
|
package/src/utils/CliWallet.ts
CHANGED
|
@@ -67,32 +67,38 @@ export class CliWallet {
|
|
|
67
67
|
/**
|
|
68
68
|
* Get the default domain from config.ini [cli] section
|
|
69
69
|
*/
|
|
70
|
-
static getDefaultDomain(): string {
|
|
70
|
+
static async getDefaultDomain(): Promise<string> {
|
|
71
71
|
const config = new Config();
|
|
72
|
+
await config.setPath('/');
|
|
72
73
|
return (config.data as any).cli?.default_domain || 'localhost';
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
|
-
* Set the default domain in config.ini [cli] section
|
|
77
|
+
* Set the default domain in config.ini [cli] section.
|
|
78
|
+
*
|
|
79
|
+
* Loads the root config first so the rest of it ([profile], [default.provider],
|
|
80
|
+
* …) is preserved on save. (Before the async migration, Config did not
|
|
81
|
+
* auto-load on construction, so this wrote {cli:…} over the whole root file.)
|
|
77
82
|
*/
|
|
78
|
-
static setDefaultDomain(domain: string): void {
|
|
83
|
+
static async setDefaultDomain(domain: string): Promise<void> {
|
|
79
84
|
const config = new Config();
|
|
85
|
+
await config.setPath('/');
|
|
80
86
|
if (!(config.data as any).cli) {
|
|
81
87
|
(config.data as any).cli = {};
|
|
82
88
|
}
|
|
83
89
|
(config.data as any).cli.default_domain = domain;
|
|
84
|
-
config.save();
|
|
90
|
+
await config.save();
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
/**
|
|
88
94
|
* Initialize a new domain with wallet
|
|
89
95
|
* Creates ~/.epistery/{domain}/config.ini with new wallet
|
|
90
96
|
*/
|
|
91
|
-
static initialize(domain: string, provider?: { name: string, chainId: number, rpc: string }): CliWallet {
|
|
97
|
+
static async initialize(domain: string, provider?: { name: string, chainId: number, rpc: string }): Promise<CliWallet> {
|
|
92
98
|
const config = new Config();
|
|
93
99
|
|
|
94
100
|
// Check if domain already exists
|
|
95
|
-
config.setPath(domain);
|
|
101
|
+
await config.setPath(domain);
|
|
96
102
|
if (config.data.wallet) {
|
|
97
103
|
throw new Error(`Domain '${domain}' already initialized. Use load() to access it.`);
|
|
98
104
|
}
|
|
@@ -101,7 +107,7 @@ export class CliWallet {
|
|
|
101
107
|
const ethersWallet = ethers.Wallet.createRandom();
|
|
102
108
|
|
|
103
109
|
// Get provider from root default, argument, or fallback default
|
|
104
|
-
config.setPath('/');
|
|
110
|
+
await config.setPath('/');
|
|
105
111
|
const providerConfig = provider || config.data.default?.provider || {
|
|
106
112
|
chainId: 420420422,
|
|
107
113
|
name: 'polkadot-hub-testnet',
|
|
@@ -109,7 +115,7 @@ export class CliWallet {
|
|
|
109
115
|
};
|
|
110
116
|
|
|
111
117
|
// Create domain config
|
|
112
|
-
config.setPath(`/${domain}`);
|
|
118
|
+
await config.setPath(`/${domain}`);
|
|
113
119
|
config.data = {
|
|
114
120
|
domain: domain,
|
|
115
121
|
wallet: {
|
|
@@ -120,7 +126,7 @@ export class CliWallet {
|
|
|
120
126
|
},
|
|
121
127
|
provider: providerConfig
|
|
122
128
|
};
|
|
123
|
-
config.save();
|
|
129
|
+
await config.save();
|
|
124
130
|
|
|
125
131
|
console.log(`Initialized domain: ${domain}`);
|
|
126
132
|
console.log(`Address: ${ethersWallet.address}`);
|
|
@@ -133,12 +139,11 @@ export class CliWallet {
|
|
|
133
139
|
* Load domain wallet from config
|
|
134
140
|
* Throws if domain doesn't exist - use initialize() first
|
|
135
141
|
*/
|
|
136
|
-
static load(domain?: string): CliWallet {
|
|
142
|
+
static async load(domain?: string): Promise<CliWallet> {
|
|
137
143
|
const config = new Config();
|
|
138
|
-
const domainName = domain || CliWallet.getDefaultDomain();
|
|
144
|
+
const domainName = domain || await CliWallet.getDefaultDomain();
|
|
139
145
|
|
|
140
|
-
config.setPath(`/${domainName}`);
|
|
141
|
-
config.load();
|
|
146
|
+
await config.setPath(`/${domainName}`);
|
|
142
147
|
|
|
143
148
|
if (!config.data.wallet) {
|
|
144
149
|
throw new Error(
|