epistery 1.5.11 → 2.0.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.example +0 -1
- package/README.md +288 -322
- package/cli/epistery.mjs +2 -583
- package/client/wallet.js +36 -84
- package/client/witness.js +44 -643
- package/dist/epistery.d.ts +1 -189
- package/dist/epistery.d.ts.map +1 -1
- package/dist/epistery.js +83 -1528
- package/dist/epistery.js.map +1 -1
- package/dist/utils/CliWallet.d.ts +3 -2
- package/dist/utils/CliWallet.d.ts.map +1 -1
- package/dist/utils/CliWallet.js +6 -3
- package/dist/utils/CliWallet.js.map +1 -1
- package/dist/utils/Utils.d.ts +2 -174
- package/dist/utils/Utils.d.ts.map +1 -1
- package/dist/utils/Utils.js +0 -1154
- package/dist/utils/Utils.js.map +1 -1
- package/dist/utils/types.d.ts +4 -127
- package/dist/utils/types.d.ts.map +1 -1
- package/dist/utils/types.js +0 -13
- package/dist/utils/types.js.map +1 -1
- package/index.mjs +44 -596
- package/package.json +1 -5
- package/routes/connect.mjs +35 -35
- package/routes/fido.mjs +1 -1
- package/routes/identity.mjs +3 -29
- package/routes/index.mjs +6 -38
- package/routes/status.mjs +4 -82
- package/src/epistery.ts +86 -1918
- package/src/utils/CliWallet.ts +11 -5
- package/src/utils/Utils.ts +2 -1469
- package/src/utils/types.ts +15 -145
- package/test/fixtures/wallets.ts +6 -3
- package/test/routes/connect.test.ts +12 -6
- package/test/setup.ts +2 -43
- package/test/utils.ts +9 -41
- package/EpisteryRegistry.md +0 -396
- package/MONGODB_GOTCHA.md +0 -69
- package/MobileIdentity.md +0 -182
- package/client/notabot.js +0 -679
- package/client/status.html +0 -1580
- package/contracts/IAddressNaming.sol +0 -42
- package/contracts/ICreditAccount.sol +0 -48
- package/contracts/IUserRegistry.sol +0 -34
- package/contracts/IdentityContract.sol +0 -421
- package/contracts/agent.sol +0 -1095
- package/demo/README.md +0 -50
- package/demo/index.html +0 -13
- package/demo/package.json +0 -15
- package/demo/server.mjs +0 -174
- package/demo/whitelist-test.html +0 -710
- package/hardhat.config.js +0 -61
- package/routes/approval.mjs +0 -257
- package/routes/contract.mjs +0 -23
- package/routes/data.mjs +0 -394
- package/routes/list.mjs +0 -76
- package/routes/notabot.mjs +0 -268
- package/routes/whitelist/client/admin.html +0 -1249
- package/routes/whitelist/client/client.js +0 -289
- package/routes/whitelist/client/icon.svg +0 -5
- package/routes/whitelist/client/widget.html +0 -90
- package/routes/whitelist/index.mjs +0 -832
- package/scripts/deploy-agent.js +0 -206
- package/scripts/fund-test-wallets.js +0 -204
- package/scripts/setup-test-env.js +0 -191
- package/scripts/verify-agent.js +0 -39
- package/src/utils/Aqua.ts +0 -198
- package/test/routes/approval.test.ts +0 -282
- package/test/routes/contract.test.ts +0 -58
- package/test/routes/data.test.ts +0 -474
- package/test/routes/list.test.ts +0 -172
- package/test/routes/notabot.test.ts +0 -157
- package/test/routes/whitelist.test.ts +0 -596
package/README.md
CHANGED
|
@@ -2,399 +2,365 @@
|
|
|
2
2
|
|
|
3
3
|
_Epistemology is the study of knowledge. An Epistery, it follows, is a place to share the knowledge of knowledge._
|
|
4
4
|
|
|
5
|
-
**Epistery
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
5
|
+
**Epistery is the identity foundation for web applications.** It gives a host one
|
|
6
|
+
thing it can trust on every request — a cryptographically proven address — and
|
|
7
|
+
binds that address to an on-chain IdentityContract when the user wants a durable
|
|
8
|
+
multi-device identity. Everything else (data, ACLs, naming, content) is the host
|
|
9
|
+
application's concern, not epistery's.
|
|
10
|
+
|
|
11
|
+
> **Status — sealed contract, v1.2 (2026-05-27).** This README defines what
|
|
12
|
+
> epistery is responsible for, what it is not, and its interface. Code is held to
|
|
13
|
+
> this document. The `agent.sol` surface (data wallets, approvals, whitelist /
|
|
14
|
+
> lists / roles, name registry, notabot) was **removed in v1.2**; see the wiki
|
|
15
|
+
> archives ([[NotABot]], [[Whitelist]], [[DataWallets]], [[Approvals]],
|
|
16
|
+
> [[ContractStandards]]) and git tag `epistery-pre-identity-refactor` for the
|
|
17
|
+
> retired implementations. The dated **[Known divergences](#known-divergences-audit)**
|
|
18
|
+
> section at the end lists where the current code still fails this contract.
|
|
19
|
+
> If behavior and this document disagree, that is a bug in the code, not the doc.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Responsibility (what epistery OWNS)
|
|
24
|
+
|
|
25
|
+
Epistery is the **single owner** of:
|
|
26
|
+
|
|
27
|
+
1. **Identity** — proving *who* a request is from. The proof is a wallet
|
|
28
|
+
signature, carried either by a short-lived signed session cookie (`_epistery`,
|
|
29
|
+
established via the `/connect` handshake) or a per-request `Bot` signature.
|
|
30
|
+
The result is a trusted **address** on `req.episteryClient`.
|
|
31
|
+
2. **Identity binding** — relating a device key (rivet) to an **IdentityContract**,
|
|
32
|
+
verified on-chain (`isAuthorized`). When bound, epistery presents the *contract*
|
|
33
|
+
as the identity.
|
|
34
|
+
3. **Key custody (client)** — generating and protecting the user's signing key in
|
|
35
|
+
the browser (non-extractable; see [Key custody](#identity--key-custody)).
|
|
36
|
+
4. **Domain/server wallet & config** — the host's own wallet and the path-based
|
|
37
|
+
`~/.epistery` configuration.
|
|
38
|
+
5. **FIDO blob storage** — server-side backup of WebAuthn-PRF-wrapped rivet keys
|
|
39
|
+
so they survive iOS ITP IndexedDB eviction.
|
|
40
|
+
|
|
41
|
+
No consumer may bypass, re-derive, or duplicate any of these. In particular: a
|
|
42
|
+
downstream service **never** trusts a client-supplied identity header and
|
|
43
|
+
**never** re-implements identity resolution.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## What Epistery DOES
|
|
48
|
+
|
|
49
|
+
- **Authenticates every request** to a trusted address (`req.episteryClient`),
|
|
50
|
+
via signed `_epistery` session cookie or `Bot` signature.
|
|
51
|
+
- **Mints/loads wallets** for the browser (rivet / FIDO / web3) and server
|
|
52
|
+
(per-domain).
|
|
53
|
+
- **Binds a device to an IdentityContract** and verifies that binding on-chain.
|
|
54
|
+
- **Serves client libraries** at `/lib/*` (`witness.js`, `wallet.js`, `ethers.js`,
|
|
55
|
+
…) and contract artifacts at `/artifacts/*` for consumers.
|
|
56
|
+
- **Persists FIDO blobs** (`/fido/blob`) — encrypted, PRF-wrapped rivet keys for
|
|
57
|
+
WebAuthn-backed identities.
|
|
58
|
+
- **Exposes a CLI** for stateless bot-authenticated requests (`curl`), the
|
|
59
|
+
Streamable-HTTP MCP bridge (`mcp`), domain initialization, and basic info.
|
|
60
|
+
|
|
61
|
+
## What Epistery does NOT do
|
|
62
|
+
|
|
63
|
+
- **Does not store application data.** Apps own their storage; epistery records
|
|
64
|
+
identity, not your documents.
|
|
65
|
+
- **Does not manage contracts.** It *binds keys to existing contracts* and
|
|
66
|
+
*verifies* the binding on-chain — it does not deploy, write to, or own
|
|
67
|
+
application contracts. Contract creation and on-chain ACL/state live in host
|
|
68
|
+
contracts (e.g. `IdentityContractV3.sol`, `DomainContract.sol`).
|
|
69
|
+
- **Does not define application- or session-level ACLs.** Authorization is the
|
|
70
|
+
host's job, evaluated against the trusted address epistery provides.
|
|
71
|
+
- **Does not run a name registry.** Per-domain naming is a relay service; epistery
|
|
72
|
+
carries no name → address mapping.
|
|
73
|
+
- **Does not accept a client's claim of identity.** The only identity is the one
|
|
74
|
+
epistery itself proved (`req.episteryClient`). There is no "I am contract X"
|
|
75
|
+
header. Contract identity claims are verified on-chain at `/connect` and sealed
|
|
76
|
+
into the signed cookie.
|
|
77
|
+
- **Does not let downstream code adjudicate auth.** Re-deriving identity or
|
|
78
|
+
re-checking signatures outside epistery is a contract violation.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## The trust contract: `req.episteryClient`
|
|
83
|
+
|
|
84
|
+
The attach middleware sets exactly this on each request (or leaves it `undefined`):
|
|
85
|
+
|
|
86
|
+
| Field | Meaning |
|
|
87
|
+
|-------------------|---------|
|
|
88
|
+
| `signerAddress` | **The signer.** The rivet whose signature was verified (cookie session or `Bot`). Always non-null. The only thing the client can assert by itself. |
|
|
89
|
+
| `contractAddress` | **A verified contract claim.** When the client claimed an IdentityContract at `/connect`, this is that contract, verified on-chain via `isAuthorized(contractAddress, signerAddress)`. `null` when no claim. |
|
|
90
|
+
| `identityAddress` | **The canonical identity.** Derived: `contractAddress || signerAddress`. This is what host ACLs evaluate against. Always non-null. |
|
|
91
|
+
| `publicKey` | The signer's public key. |
|
|
92
|
+
| `authenticated` | Whether the session/handshake completed. |
|
|
93
|
+
| `authType` | `"bot"` for `Bot`-signed requests; `"cookie"` for session-cookie. |
|
|
94
|
+
|
|
95
|
+
The three roles are kept separate on purpose. `signerAddress` is a fact the
|
|
96
|
+
client proves; `contractAddress` is a claim the server verifies; `identityAddress`
|
|
97
|
+
is the server's derivation. The wire never asks the client to pick which role
|
|
98
|
+
its address plays.
|
|
99
|
+
|
|
100
|
+
**Rule for consumers:** authorize against `identityAddress`. The signer vs.
|
|
101
|
+
contract distinction is available but rarely your concern.
|
|
38
102
|
|
|
39
103
|
```javascript
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const app = express();
|
|
45
|
-
|
|
46
|
-
// Connect and attach epistery
|
|
47
|
-
const epistery = await Epistery.connect();
|
|
48
|
-
await epistery.setDomain('mydomain.com');
|
|
49
|
-
await epistery.attach(app);
|
|
50
|
-
|
|
51
|
-
// Optional: Add authentication callback
|
|
52
|
-
const episteryWithAuth = await Epistery.connect({
|
|
53
|
-
authentication: async (clientInfo) => {
|
|
54
|
-
// clientInfo.address contains the wallet address
|
|
55
|
-
// Return user profile or null
|
|
56
|
-
return await getUserProfile(clientInfo.address);
|
|
57
|
-
},
|
|
58
|
-
onAuthenticated: async (clientInfo, req, res) => {
|
|
59
|
-
// Called after successful authentication
|
|
60
|
-
console.log('User authenticated:', clientInfo.address);
|
|
61
|
-
}
|
|
104
|
+
app.get('/thing', (req, res) => {
|
|
105
|
+
const me = req.episteryClient; // the ONLY source of identity
|
|
106
|
+
if (!me?.authenticated) return res.status(401).end();
|
|
107
|
+
// authorize against your host's contracts / policy using me.identityAddress
|
|
62
108
|
});
|
|
63
|
-
|
|
64
|
-
// Start your server
|
|
65
|
-
const https_server = https.createServer(epistery.config.SNI, app);
|
|
66
|
-
https_server.listen(443);
|
|
67
109
|
```
|
|
68
110
|
|
|
69
|
-
|
|
70
|
-
- `/.well-known/epistery` - Server wallet status (JSON)
|
|
71
|
-
- `/.well-known/epistery/status` - Human-readable status page
|
|
72
|
-
- `/.well-known/epistery/connect` - Client key exchange endpoint
|
|
73
|
-
- `/.well-known/epistery/data/*` - Data wallet operations
|
|
74
|
-
- `/.well-known/epistery/whitelist` - Access control endpoints
|
|
111
|
+
### The wire (POST `/connect`)
|
|
75
112
|
|
|
76
|
-
|
|
113
|
+
The handshake body carries facts only:
|
|
77
114
|
|
|
78
|
-
|
|
115
|
+
| Field | Required | Meaning |
|
|
116
|
+
|-------------------|----------|---------|
|
|
117
|
+
| `signerAddress` | yes | The rivet. Must equal the address recovered from `signature` over `message`. |
|
|
118
|
+
| `signerPublicKey` | yes | The signer's public key. |
|
|
119
|
+
| `contractAddress` | yes | An IdentityContract claim, or `null`. When non-null, the server verifies it on-chain via `isAuthorized(contractAddress, signerAddress)`. |
|
|
120
|
+
| `challenge`, `message`, `signature` | yes | Proof of signer (see [Identity & key custody](#identity--key-custody)). |
|
|
121
|
+
| `walletSource` | no | `"rivet"` / `"fido"` / `"web3"` / etc. — informational. |
|
|
79
122
|
|
|
80
|
-
|
|
123
|
+
There is no `clientAddress`, no `identityAddress` on the wire. Either of those
|
|
124
|
+
would force the receiver to guess which role the address plays. The server
|
|
125
|
+
derives `identityAddress` from the two facts and exposes it on
|
|
126
|
+
`req.episteryClient`; the client never tells the server what its identity *is*.
|
|
81
127
|
|
|
82
|
-
|
|
83
|
-
```javascript
|
|
84
|
-
// Load client library in your HTML
|
|
85
|
-
<script src="/.well-known/epistery/lib/client.js"></script>
|
|
86
|
-
<script>
|
|
87
|
-
const client = new EpisteryClient();
|
|
88
|
-
await client.connect(); // Automatic key exchange
|
|
89
|
-
console.log('Connected as:', client.address);
|
|
90
|
-
</script>
|
|
91
|
-
```
|
|
128
|
+
---
|
|
92
129
|
|
|
93
|
-
|
|
94
|
-
```javascript
|
|
95
|
-
// Access authenticated client in routes
|
|
96
|
-
app.get('/profile', (req, res) => {
|
|
97
|
-
if (req.episteryClient?.authenticated) {
|
|
98
|
-
res.json({ address: req.episteryClient.address });
|
|
99
|
-
} else {
|
|
100
|
-
res.status(401).json({ error: 'Not authenticated' });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### 2. Data Wallets
|
|
130
|
+
## Identity & key custody
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
The browser signing key is created and protected by epistery. Custody depends on
|
|
133
|
+
wallet type:
|
|
108
134
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
metadata: { tags: ['important'] }
|
|
115
|
-
});
|
|
135
|
+
| Wallet | Key custody | Security property |
|
|
136
|
+
|----------------|-------------|-------------------|
|
|
137
|
+
| **RivetWallet** (default) | secp256k1 private key **encrypted at rest** by a **non-extractable** AES-GCM `CryptoKey` held in IndexedDB (WebCrypto). Only ciphertext + a key id are persisted. **Refuses to create the wallet if WebCrypto is unavailable** — no plaintext fallback. | The signing key cannot be exported — the core "unextractable device key" property. |
|
|
138
|
+
| **FidoWallet** | Rivet key wrapped by a WebAuthn PRF secret (Secure Enclave); blob optionally backed up server-side via `/fido/blob` (survives iOS ITP eviction). | Key release gated by platform authenticator. |
|
|
139
|
+
| **Web3Wallet** | External plugin (e.g. MetaMask) holds the key. | Custody is the plugin's. |
|
|
116
140
|
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
A device can hold **multiple independent rivets** (Browser/FIDO/Web3 are all
|
|
142
|
+
rivets — different ways of presenting a device-locked signing key). This is how
|
|
143
|
+
the system enforces one-key-one-identity without a hard cross-context check: the
|
|
144
|
+
user mints another isolated rivet rather than pointing one key at two contracts.
|
|
119
145
|
|
|
120
|
-
|
|
121
|
-
await client.transferOwnership(newOwnerAddress);
|
|
122
|
-
```
|
|
146
|
+
Server/domain wallets live in `~/.epistery/<domain>/config.ini` (0600).
|
|
123
147
|
|
|
124
|
-
|
|
125
|
-
- **Blockchain Contracts**: Each data wallet is a smart contract on-chain
|
|
126
|
-
- **Encryption**: Data can be encrypted before storage
|
|
127
|
-
- **Sharing**: Grant read/write access to specific addresses
|
|
128
|
-
- **Transferable**: Ownership can be transferred to other wallets
|
|
129
|
-
- **IPFS Storage**: Content stored on IPFS by default, with hashes on-chain
|
|
130
|
-
- **Provenance Tracking**: Full ownership and modification history on-chain
|
|
148
|
+
### The `/connect` handshake & contract binding
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const whitelist = await epistery.getWhitelist();
|
|
142
|
-
```
|
|
150
|
+
1. The client `Witness` signs a challenge with its rivet and POSTs to `/connect`
|
|
151
|
+
with `signerAddress` (the rivet), `signerPublicKey`, and `contractAddress`
|
|
152
|
+
(the claim, or `null`).
|
|
153
|
+
2. The server verifies the signature recovers to `signerAddress`. If
|
|
154
|
+
`contractAddress` is non-null, it calls `IdentityContract.isAuthorized(signerAddress)`
|
|
155
|
+
**on-chain** — the chain is truth.
|
|
156
|
+
3. On success it issues the signed `_epistery` cookie, recording `signerAddress`
|
|
157
|
+
and (if verified) `contractAddress`. The auth middleware then exposes
|
|
158
|
+
`req.episteryClient.identityAddress = contractAddress || signerAddress`.
|
|
143
159
|
|
|
144
|
-
|
|
160
|
+
A rivet is bound to a contract client-side via `wallet.upgradeToContract(contract)`
|
|
161
|
+
— afterward the wallet's derived `identityAddress` is the contract while
|
|
162
|
+
`signerAddress` is still the rivet. A fresh key exchange follows; the witness
|
|
163
|
+
short-circuits when (and only when) the cookie's `identityAddress` already
|
|
164
|
+
matches the wallet's `identityAddress`. The rivet→contract relation in
|
|
165
|
+
localStorage is not cryptographically sealed in the browser — but spoofing it
|
|
166
|
+
is useless: the contract knows its authorized signers and can't be spoofed; the
|
|
167
|
+
on-chain verification at `/connect` is the gate.
|
|
145
168
|
|
|
146
|
-
|
|
169
|
+
---
|
|
147
170
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
# Initialize a CLI wallet
|
|
152
|
-
epistery initialize localhost
|
|
153
|
-
epistery set-default localhost
|
|
154
|
-
|
|
155
|
-
# Make authenticated GET request
|
|
156
|
-
epistery curl https://api.example.com/data
|
|
157
|
-
|
|
158
|
-
# PUT request with JSON data (note single quotes)
|
|
159
|
-
epistery curl -X PUT -d '{"title":"Test","body":"Content"}' https://api.example.com/wiki/Test
|
|
160
|
-
|
|
161
|
-
# Use specific wallet
|
|
162
|
-
epistery curl -w production.example.com https://api.example.com/data
|
|
163
|
-
|
|
164
|
-
# Verbose output for debugging
|
|
165
|
-
epistery curl -v https://api.example.com/data
|
|
166
|
-
```
|
|
171
|
+
## HTTP interface
|
|
167
172
|
|
|
168
|
-
|
|
169
|
-
- Testing authenticated endpoints
|
|
170
|
-
- Building automation scripts
|
|
171
|
-
- Creating bots and agents
|
|
172
|
-
- CI/CD integration
|
|
173
|
+
Mounted under `rootPath` (default `/.well-known/epistery`, RFC 8615):
|
|
173
174
|
|
|
174
|
-
|
|
175
|
+
| Path | Methods | Purpose |
|
|
176
|
+
|------|---------|---------|
|
|
177
|
+
| `/` | GET | Server status JSON (`Witness.connect` probes this for chain/provider info). No HTML UI. |
|
|
178
|
+
| `/lib/:module` | GET | Client libraries (`witness.js`, `wallet.js`, `client.js`, `ethers.js`, …) |
|
|
179
|
+
| `/artifacts/:file` | GET | Contract ABIs/artifacts |
|
|
180
|
+
| `/connect` | GET / POST | Session check / key-exchange handshake (sets `_epistery`; on-chain `isAuthorized` verify for contract claims) |
|
|
181
|
+
| `/create` | GET | Wallet creation helper |
|
|
182
|
+
| `/auth/account/claim`, `/auth/dns/claim`, `/auth/account/check-admin` | GET/POST | Domain claiming & admin checks |
|
|
183
|
+
| `/identity/prepare-add-rivet` | POST | Unsigned tx for adding a rivet to an existing IdentityContract (client signs, then `/data/submit-signed`-style broadcast) |
|
|
184
|
+
| `/domain/initialize` | POST | Initialize a domain wallet |
|
|
185
|
+
| `/fido/blob`, `/fido/blob/:credentialId` | POST/GET | PRF-wrapped rivet key blob storage |
|
|
175
186
|
|
|
176
|
-
|
|
187
|
+
---
|
|
177
188
|
|
|
178
|
-
##
|
|
189
|
+
## Server API
|
|
179
190
|
|
|
180
|
-
|
|
191
|
+
```javascript
|
|
192
|
+
import { Epistery, Config } from 'epistery';
|
|
181
193
|
|
|
194
|
+
const epistery = await Epistery.connect({
|
|
195
|
+
authentication: async (clientInfo) => { /* return profile or null */ },
|
|
196
|
+
onAuthenticated: async (clientInfo, req, res) => { /* post-auth hook */ },
|
|
197
|
+
});
|
|
198
|
+
await epistery.setDomain('mydomain.com');
|
|
199
|
+
await epistery.attach(app); // mounts middleware + routes under rootPath
|
|
182
200
|
```
|
|
183
|
-
~/.epistery/
|
|
184
|
-
├── config.ini # Root config (profile, IPFS, defaults)
|
|
185
|
-
├── mydomain.com/
|
|
186
|
-
│ └── config.ini # Domain config (wallet, provider)
|
|
187
|
-
└── .ssl/
|
|
188
|
-
└── mydomain.com/ # SSL certificates (optional)
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Root Config (`~/.epistery/config.ini`)
|
|
192
|
-
|
|
193
|
-
```ini
|
|
194
|
-
[profile]
|
|
195
|
-
name=Your Name
|
|
196
|
-
email=you@example.com
|
|
197
|
-
|
|
198
|
-
[ipfs]
|
|
199
|
-
url=https://rootz.digital/api/v0
|
|
200
|
-
|
|
201
|
-
[cli]
|
|
202
|
-
default_domain=localhost
|
|
203
|
-
|
|
204
|
-
# Which chain the claim-page dropdown selects by default.
|
|
205
|
-
[default]
|
|
206
|
-
defaultChainId=137
|
|
207
|
-
|
|
208
|
-
# Private RPC overrides, keyed by chainId. Only needed when the chain's
|
|
209
|
-
# built-in public RPC isn't sufficient (rate-limited, needs an API key).
|
|
210
|
-
# Everything else — name, public RPC, currency, fee policy — lives in
|
|
211
|
-
# the chain classes (src/chains/) and doesn't need to be configured.
|
|
212
201
|
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
The `clientInfo` passed to both hooks has the same shape as
|
|
203
|
+
`req.episteryClient`: `{ signerAddress, contractAddress, identityAddress,
|
|
204
|
+
publicKey }` (plus `authenticated` and `profile` after `authentication`
|
|
205
|
+
resolves). Authorize against `identityAddress`.
|
|
215
206
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Legacy single-provider format is also supported:
|
|
207
|
+
`Epistery` (exported as `EpisteryAttach`): `connect`, `setDomain`, `attach`,
|
|
208
|
+
`resolveClient(req)` (auth resolution for non-middleware contexts, e.g. WebSocket
|
|
209
|
+
upgrades), `buildStatus`, `routes`.
|
|
221
210
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
chainId=137
|
|
225
|
-
privateRpc=https://polygon-mainnet.infura.io/v3/YOUR_KEY
|
|
226
|
-
```
|
|
211
|
+
Also exported: `Config`, `chainFor`, `registerChain`, `configuredChains`,
|
|
212
|
+
`defaultChainId`, `Chain`.
|
|
227
213
|
|
|
228
|
-
|
|
214
|
+
The core `Epistery` static API (`src/epistery.ts`): `initialize`, `createWallet`,
|
|
215
|
+
`getStatus`, `handleKeyExchange` (consumed by `/connect`),
|
|
216
|
+
`prepareAddRivetToContract` (unsigned tx builder), `submitSignedTransaction`
|
|
217
|
+
(generic broadcaster for client-signed transactions — this is the
|
|
218
|
+
"server-requests-signature, interactive wallet (FIDO/MetaMask) signs, then submit"
|
|
219
|
+
path).
|
|
229
220
|
|
|
230
|
-
|
|
231
|
-
[domain]
|
|
232
|
-
domain=mydomain.com
|
|
221
|
+
### Config
|
|
233
222
|
|
|
234
|
-
|
|
235
|
-
address=0x...
|
|
236
|
-
mnemonic=word word word...
|
|
237
|
-
publicKey=0x04...
|
|
238
|
-
privateKey=0x...
|
|
223
|
+
Path-based ini config under `~/.epistery` (`src/utils/Config.ts`):
|
|
239
224
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
225
|
+
```javascript
|
|
226
|
+
import { Config } from 'epistery';
|
|
227
|
+
const config = new Config();
|
|
228
|
+
config.setPath('/'); // ~/.epistery/config.ini (root)
|
|
229
|
+
config.load();
|
|
230
|
+
config.data.profile.email = 'user@example.com';
|
|
231
|
+
config.save();
|
|
232
|
+
config.setPath('/mydomain.com'); // ~/.epistery/mydomain.com/config.ini
|
|
244
233
|
```
|
|
245
234
|
|
|
246
|
-
|
|
235
|
+
Methods: `setPath`, `getPath`, `load`, `save` (+ `data`).
|
|
247
236
|
|
|
248
|
-
|
|
237
|
+
---
|
|
249
238
|
|
|
250
|
-
|
|
239
|
+
## Client API (`Witness`)
|
|
251
240
|
|
|
252
|
-
|
|
241
|
+
Served at `/.well-known/epistery/lib/witness.js`:
|
|
253
242
|
|
|
254
243
|
```javascript
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// clientInfo: { address, publicKey }
|
|
258
|
-
|
|
259
|
-
// Look up user in your database
|
|
260
|
-
const user = await db.users.findOne({
|
|
261
|
-
walletAddress: clientInfo.address
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
if (!user) return null;
|
|
265
|
-
|
|
266
|
-
// Return profile data
|
|
267
|
-
return {
|
|
268
|
-
id: user.id,
|
|
269
|
-
username: user.username,
|
|
270
|
-
permissions: user.permissions
|
|
271
|
-
};
|
|
272
|
-
},
|
|
273
|
-
onAuthenticated: async (clientInfo, req, res) => {
|
|
274
|
-
// Called after successful authentication
|
|
275
|
-
// clientInfo includes: address, publicKey, profile, authenticated
|
|
276
|
-
|
|
277
|
-
// Set up session, log authentication, etc.
|
|
278
|
-
req.session.userId = clientInfo.profile.id;
|
|
279
|
-
}
|
|
280
|
-
});
|
|
244
|
+
import Witness from '/.well-known/epistery/lib/witness.js';
|
|
245
|
+
const witness = await Witness.connect({ rootPath: '/' }); // creates/loads wallet, runs key exchange
|
|
281
246
|
```
|
|
282
247
|
|
|
283
|
-
|
|
248
|
+
Public surface: `connect`, `performKeyExchange`, `getWallets`, `getStatus`,
|
|
249
|
+
`addBrowserWallet` / `addFidoWallet` / `addWeb3Wallet`, `setDefaultWallet`,
|
|
250
|
+
`removeWallet`, `updateWalletLabel`, `bindToEpisteryIdentity` (cross-host identity
|
|
251
|
+
ferry). Wallet classes: `RivetWallet`, `FidoWallet`, `Web3Wallet`; binding via
|
|
252
|
+
`wallet.upgradeToContract`.
|
|
284
253
|
|
|
285
|
-
|
|
254
|
+
Identity properties on every wallet — the canonical surface for client code
|
|
255
|
+
deciding "who am I right now":
|
|
286
256
|
|
|
287
|
-
|
|
288
|
-
|
|
257
|
+
| Property | Meaning |
|
|
258
|
+
|-----------------------|---------|
|
|
259
|
+
| `wallet.signerAddress` | The rivet — the address we sign with. |
|
|
260
|
+
| `wallet.contractAddress` | The bound IdentityContract, or `null`. |
|
|
261
|
+
| `wallet.identityAddress` | Derived: `contractAddress || signerAddress`. What host UI and ACLs should reference. |
|
|
289
262
|
|
|
290
|
-
|
|
263
|
+
---
|
|
291
264
|
|
|
292
|
-
|
|
293
|
-
config.setPath('/');
|
|
294
|
-
config.load();
|
|
295
|
-
config.data.profile.email = 'user@example.com';
|
|
296
|
-
config.save();
|
|
265
|
+
## CLI
|
|
297
266
|
|
|
298
|
-
|
|
299
|
-
config.setPath('/mydomain.com');
|
|
300
|
-
config.load();
|
|
301
|
-
config.data.verified = true;
|
|
302
|
-
config.save();
|
|
267
|
+
Stateless bot authentication (each request independently signed):
|
|
303
268
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
269
|
+
```bash
|
|
270
|
+
epistery initialize localhost
|
|
271
|
+
epistery set-default localhost
|
|
272
|
+
epistery info localhost
|
|
273
|
+
epistery curl https://api.example.com/data
|
|
274
|
+
epistery curl -X PUT -d '{"title":"Test"}' https://api.example.com/wiki/Test
|
|
275
|
+
epistery curl -b -w production.example.com https://api.example.com/data # -b bot, -w wallet, -v verbose
|
|
276
|
+
epistery mcp https://api.example.com # stdio MCP bridge with bot-auth
|
|
309
277
|
```
|
|
310
278
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
Epistery follows a plugin architecture that integrates seamlessly with Express.js applications:
|
|
314
|
-
|
|
315
|
-
- **Server Module** (`/src/epistery.ts`): Core wallet and data wallet operations
|
|
316
|
-
- **Client Libraries** (`/client/*.js`): Browser-side authentication and data wallet tools
|
|
317
|
-
- **CLI** (`/cli/epistery.mjs`): Command-line interface for authenticated requests
|
|
318
|
-
- **Utils** (`/src/utils/`): Configuration, crypto operations, and Aqua protocol implementation
|
|
319
|
-
- **Chains** (`/src/chains/`): Per-chain provider, fee policy, and gas estimation
|
|
320
|
-
|
|
321
|
-
All endpoints follow RFC 8615 well-known URIs standard for service discovery.
|
|
322
|
-
|
|
323
|
-
See [Architecture.md](Architecture.md) for detailed architecture documentation.
|
|
279
|
+
Commands: `initialize`, `set-default`, `info`, `curl`, `mcp`, `help`. See
|
|
280
|
+
[CLI.md](CLI.md).
|
|
324
281
|
|
|
325
|
-
|
|
282
|
+
---
|
|
326
283
|
|
|
327
|
-
|
|
284
|
+
## Chains
|
|
328
285
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
| Ethereum Mainnet | 1 | Standard EIP-1559 | eth.llamarpc.com |
|
|
334
|
-
| Sepolia Testnet | 11155111 | Standard EIP-1559 | eth-sepolia.public.blastapi.io |
|
|
335
|
-
| Japan Open Chain | 81 | Legacy gasPrice, 30 gwei floor | rpc-2.japanopenchain.org |
|
|
286
|
+
Each EVM chain is a `Chain` object owning its RPC, fee policy, and gas strategy.
|
|
287
|
+
Only `chainId` is required; everything else comes from the class. Use
|
|
288
|
+
`chainFor({ chainId })`; add a chain by extending `Chain` + `registerChain()`.
|
|
289
|
+
See [src/chains/README.md](src/chains/README.md).
|
|
336
290
|
|
|
337
|
-
|
|
291
|
+
---
|
|
338
292
|
|
|
339
|
-
|
|
340
|
-
import { chainFor, registeredChains } from 'epistery';
|
|
293
|
+
## Versioning & local development
|
|
341
294
|
|
|
342
|
-
|
|
343
|
-
|
|
295
|
+
- Consumers depend on the **published** package: `npm install epistery@latest`.
|
|
296
|
+
- Local cross-package work installs a **temporary relative path**
|
|
297
|
+
(`npm install ../../rootz/epistery`) for testing only.
|
|
298
|
+
- Publishing to npm and any deployment is a deliberate, human-performed step. No
|
|
299
|
+
tooling or agent publishes, bumps versions, or deploys on its own.
|
|
344
300
|
|
|
345
|
-
|
|
346
|
-
const chain = chainFor({ chainId: 137, privateRpc: 'https://polygon-mainnet.infura.io/v3/KEY' });
|
|
301
|
+
---
|
|
347
302
|
|
|
348
|
-
|
|
349
|
-
const wallet = ethers.Wallet.fromMnemonic(mnemonic).connect(chain.provider);
|
|
303
|
+
## Known divergences (audit)
|
|
350
304
|
|
|
351
|
-
|
|
352
|
-
const feeData = await chain.getFeeData();
|
|
353
|
-
// → Polygon: { maxPriorityFeePerGas: 25 gwei, maxFeePerGas: 50 gwei }
|
|
354
|
-
// → JOC: { gasPrice: 30 gwei }
|
|
355
|
-
// → Ethereum: { maxPriorityFeePerGas: <network>, maxFeePerGas: <network> }
|
|
305
|
+
Where the code currently fails the contract above. Dated; remove as fixed.
|
|
356
306
|
|
|
357
|
-
|
|
358
|
-
const chains = registeredChains();
|
|
359
|
-
```
|
|
307
|
+
**Resolved in v1.2 (2026-05-27 identity-only refactor)**
|
|
360
308
|
|
|
361
|
-
|
|
309
|
+
- **Plaintext private keys.** `BrowserWallet` (extractable-key legacy wallet) is
|
|
310
|
+
removed; `RivetWallet` WebCrypto fallback now **throws** rather than silently
|
|
311
|
+
storing a plaintext key. No code path persists a cleartext signing key.
|
|
312
|
+
- **`agent.sol` surface.** Data wallets (`/data/*`), approvals (`/approval/*`),
|
|
313
|
+
whitelist (`/whitelist/*`), on-chain lists/roles (`/lists`, `/list`), name
|
|
314
|
+
registry (`resolveName`/`setAddressName`), `notabot`, contract creation
|
|
315
|
+
(`/identity/prepare-deploy`), `contracts/` directory — all removed. epistery
|
|
316
|
+
is now identity + storage/config + FIDO blob only.
|
|
317
|
+
- **Client header trust path.** Removed at the server boundary in v1.2: the
|
|
318
|
+
middleware no longer reads `x-identity-contract`; identity is the verified
|
|
319
|
+
identity epistery itself proved.
|
|
362
320
|
|
|
363
|
-
|
|
321
|
+
**Resolved in v1.2 follow-up (2026-05-28 naming cutover)**
|
|
364
322
|
|
|
365
|
-
- **
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
-
|
|
369
|
-
|
|
323
|
+
- **Ambiguous identity vocabulary; no in-session rivet→contract upgrade.** The
|
|
324
|
+
wire used `clientAddress` (alternately the signer or the identity), the
|
|
325
|
+
server reconstructed which-was-meant on the fly, and `Witness.performKeyExchange`
|
|
326
|
+
short-circuited by comparing the cookie's address to the signer — so a
|
|
327
|
+
device that already had a rivet cookie could never have its session re-issued
|
|
328
|
+
as contract-bound. Replaced with three distinct names everywhere: `signerAddress`
|
|
329
|
+
(fact, asserted), `contractAddress` (claim, server-verified on-chain), and the
|
|
330
|
+
derived `identityAddress` (= `contractAddress || signerAddress`, server-only).
|
|
331
|
+
The witness short-circuits when (and only when) the cookie's `identityAddress`
|
|
332
|
+
matches the wallet's `identityAddress`. The pre-cutover wire shape
|
|
333
|
+
(`clientAddress` / `clientPublicKey`) is removed without aliases — old
|
|
334
|
+
consumers fail at the handshake instead of silently degrading.
|
|
370
335
|
|
|
371
|
-
|
|
336
|
+
**Outstanding**
|
|
372
337
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
338
|
+
1. **Downstream identity bypass (consumer: `epistery.app`).** Consumers have
|
|
339
|
+
asserted contract identity via a spoofable `x-identity-contract` header +
|
|
340
|
+
localStorage instead of consuming the verified `_epistery` cookie. Now
|
|
341
|
+
unblocked by the cutover above: the consumer's adopt path should call
|
|
342
|
+
`wallet.upgradeToContract(C)` + `Witness.performKeyExchange()` and read
|
|
343
|
+
identity from `req.episteryClient.identityAddress`.
|
|
379
344
|
|
|
380
|
-
|
|
345
|
+
2. **Wallet-internal `address` field still flips on upgrade.** `RivetWallet.upgradeToContract`
|
|
346
|
+
still overwrites `wallet.address` with the contract address (the original
|
|
347
|
+
rivet survives as `wallet.rivetAddress`). The new `wallet.signerAddress` /
|
|
348
|
+
`wallet.identityAddress` getters cover the boundary, but every internal
|
|
349
|
+
caller of `wallet.address` reads an overloaded value. Phase 1b: rename the
|
|
350
|
+
persistence shape (with one-time IndexedDB migration so existing user
|
|
351
|
+
wallets keep working) and convert call sites.
|
|
381
352
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
# Run tests
|
|
387
|
-
npm test
|
|
388
|
-
```
|
|
353
|
+
3. **`PrepareTransactionRequest`/`Response` types.** Reference removed
|
|
354
|
+
`agent.sol` operations (`write` / `transferOwnership` / `createApproval`
|
|
355
|
+
/ etc.); imported but no longer consumed. Delete in the next dead-code sweep.
|
|
389
356
|
|
|
390
|
-
|
|
357
|
+
---
|
|
391
358
|
|
|
392
359
|
## License
|
|
393
360
|
|
|
394
|
-
MIT
|
|
361
|
+
MIT — see [LICENSE](LICENSE).
|
|
395
362
|
|
|
396
363
|
## Links
|
|
397
364
|
|
|
398
|
-
-
|
|
399
|
-
-
|
|
400
|
-
- **Documentation**: See [CLI.md](CLI.md), [Architecture.md](Architecture.md), [SESSION.md](SESSION.md)
|
|
365
|
+
- Repository: https://github.com/rootz-global/epistery
|
|
366
|
+
- See [CLI.md](CLI.md), [Architecture.md](Architecture.md), [SESSION.md](SESSION.md)
|