@varnir/agent-server 0.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/LICENSE +20 -0
- package/README.md +282 -0
- package/dist/bearer.d.ts +5 -0
- package/dist/bearer.d.ts.map +1 -0
- package/dist/bearer.js +12 -0
- package/dist/bearer.js.map +1 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +151 -0
- package/dist/config.js.map +1 -0
- package/dist/guard.d.ts +28 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +56 -0
- package/dist/guard.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/dist/index.js.map +1 -0
- package/dist/key-match.d.ts +2 -0
- package/dist/key-match.d.ts.map +1 -0
- package/dist/key-match.js +33 -0
- package/dist/key-match.js.map +1 -0
- package/dist/log.d.ts +8 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +30 -0
- package/dist/log.js.map +1 -0
- package/dist/mcp-http.d.ts +58 -0
- package/dist/mcp-http.d.ts.map +1 -0
- package/dist/mcp-http.js +121 -0
- package/dist/mcp-http.js.map +1 -0
- package/dist/mcp-stdio.d.ts +11 -0
- package/dist/mcp-stdio.d.ts.map +1 -0
- package/dist/mcp-stdio.js +14 -0
- package/dist/mcp-stdio.js.map +1 -0
- package/dist/mcp.d.ts +10 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +18 -0
- package/dist/mcp.js.map +1 -0
- package/dist/rest-methods.d.ts +31 -0
- package/dist/rest-methods.d.ts.map +1 -0
- package/dist/rest-methods.js +66 -0
- package/dist/rest-methods.js.map +1 -0
- package/dist/rest.d.ts +17 -0
- package/dist/rest.d.ts.map +1 -0
- package/dist/rest.js +84 -0
- package/dist/rest.js.map +1 -0
- package/dist/tool-error.d.ts +31 -0
- package/dist/tool-error.d.ts.map +1 -0
- package/dist/tool-error.js +66 -0
- package/dist/tool-error.js.map +1 -0
- package/dist/tools.d.ts +31 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +223 -0
- package/dist/tools.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License (MIT)
|
|
2
|
+
Copyright (c) 2026 Adam Walker
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# @varnir/agent-server
|
|
2
|
+
|
|
3
|
+
A self-hosted server that lets your own AI assistant (Claude, Gemini, ChatGPT,
|
|
4
|
+
or anything else that speaks MCP or plain HTTP) trade and move funds on
|
|
5
|
+
Varnir, bounded by an on-ledger spending policy attached to the key you give
|
|
6
|
+
it. Varnir never holds the key - it lives in your environment, on your host,
|
|
7
|
+
under your control.
|
|
8
|
+
|
|
9
|
+
It exposes thirteen tools over MCP - seven reads (identity, balances, tokens,
|
|
10
|
+
the order book, transaction history, a transfer's settlement receipt, the
|
|
11
|
+
key's own spend policy) and six writes (post an order, fill an order, place a
|
|
12
|
+
market order, cancel an order, send to an L1 address, transfer to another
|
|
13
|
+
Varnir identity on L2). The same
|
|
14
|
+
operations are also reachable over a plain REST API, off by default - see
|
|
15
|
+
"The REST SecureAPI" below. Writes are refused outright unless the key this
|
|
16
|
+
server signs with actually carries a spend policy; see "Before you start".
|
|
17
|
+
|
|
18
|
+
## Before you start
|
|
19
|
+
|
|
20
|
+
**Mint a policy-scoped agent key in the scanner's keys card, with the spend
|
|
21
|
+
policy you want the assistant bound by, before you put anything in
|
|
22
|
+
`VARNIR_PRIVATE_KEY`.**
|
|
23
|
+
|
|
24
|
+
Do not hand this server your identity's owner key. It will not do what you
|
|
25
|
+
expect: at boot, the server reads its own key back off the ledger and checks
|
|
26
|
+
what it actually is before allowing any write. If that key turns out to be
|
|
27
|
+
|
|
28
|
+
- the identity's **owner** key (no spend policy exists to bound it),
|
|
29
|
+
- a key with **no spend policy** attached, or
|
|
30
|
+
- a key with the **`readonly`** or **`offledger`** role (the money contracts
|
|
31
|
+
refuse these write access regardless of any policy attached to them),
|
|
32
|
+
|
|
33
|
+
the server starts anyway, but **read-only**: every write tool and every write
|
|
34
|
+
REST method is refused before it ever reaches the ledger, with a message
|
|
35
|
+
explaining why. This is deliberate - the entire security argument for this
|
|
36
|
+
server is that the on-ledger policy is the hard bound on what a connected
|
|
37
|
+
assistant can spend, and that argument only holds when the key actually
|
|
38
|
+
carries one. There is an escape hatch (`VARNIR_ALLOW_UNSCOPED_KEY=i-understand`
|
|
39
|
+
in `.env.example`) for operators who understand they are removing that bound;
|
|
40
|
+
do not reach for it to make an error message go away.
|
|
41
|
+
|
|
42
|
+
Once you have the key, set `VARNIR_PRIVATE_KEY`, `VARNIR_IDENTITY`, and
|
|
43
|
+
`VARNIR_NETWORK` (e.g. `testnet`).
|
|
44
|
+
|
|
45
|
+
## Install and run
|
|
46
|
+
|
|
47
|
+
Run these from the repo root:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pnpm install
|
|
51
|
+
pnpm --filter @varnir/agent-server build
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Then, with the environment set (see `.env.example`), still from the repo root:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
node apps/agent-server/dist/index.js
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The server logs which mode it started in (`writes`, `read-only`, or it exits
|
|
61
|
+
with `refuse` if the key isn't even attached to `VARNIR_IDENTITY`) before it
|
|
62
|
+
starts serving anything.
|
|
63
|
+
|
|
64
|
+
## Deploying to your own cloud account
|
|
65
|
+
|
|
66
|
+
You can also run this server in your own Google Cloud, Azure, or AWS
|
|
67
|
+
account instead of on a local machine - it is still your account and your
|
|
68
|
+
key either way; Varnir runs none of this infrastructure and never sees the
|
|
69
|
+
key. See `deploy/README.md` for one-click deploy buttons, the ARM/
|
|
70
|
+
CloudFormation templates, and `Dockerfile` for the container image (built
|
|
71
|
+
from the published npm package, not this repo's source).
|
|
72
|
+
|
|
73
|
+
## Connecting a client
|
|
74
|
+
|
|
75
|
+
By default the server speaks MCP over stdio (`VARNIR_MCP_STDIO=on`), which is
|
|
76
|
+
how a client that can launch a local process connects. Copy the block for
|
|
77
|
+
your client and fill in the real values - `VARNIR_PRIVATE_KEY` and
|
|
78
|
+
`VARNIR_IDENTITY` particularly should come from wherever you actually keep
|
|
79
|
+
them, not pasted in plaintext if you can avoid it. Replace
|
|
80
|
+
`/absolute/path/to/apps/agent-server/dist/index.js` with the real absolute
|
|
81
|
+
path to the built entry point on your machine (the package's `bin` name is
|
|
82
|
+
`varnir-agent-server` if you install it globally instead of pointing at
|
|
83
|
+
`dist/index.js` directly).
|
|
84
|
+
|
|
85
|
+
### Claude Code
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
claude mcp add --transport stdio varnir \
|
|
89
|
+
--env VARNIR_PRIVATE_KEY=... --env VARNIR_IDENTITY=... --env VARNIR_NETWORK=testnet \
|
|
90
|
+
-- node /absolute/path/to/apps/agent-server/dist/index.js
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Claude Desktop
|
|
94
|
+
|
|
95
|
+
Add to `claude_desktop_config.json`:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"mcpServers": {
|
|
100
|
+
"varnir": {
|
|
101
|
+
"command": "node",
|
|
102
|
+
"args": ["/absolute/path/to/apps/agent-server/dist/index.js"],
|
|
103
|
+
"env": {
|
|
104
|
+
"VARNIR_PRIVATE_KEY": "...",
|
|
105
|
+
"VARNIR_IDENTITY": "...",
|
|
106
|
+
"VARNIR_NETWORK": "testnet"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Gemini CLI
|
|
114
|
+
|
|
115
|
+
This recipe substitutes `$VARNIR_PRIVATE_KEY` and `$VARNIR_IDENTITY` from the
|
|
116
|
+
shell environment, so `export` both in the shell you launch `gemini` from
|
|
117
|
+
first - an unset variable resolves to an empty string here rather than
|
|
118
|
+
erroring, which just pushes the failure to server boot instead of catching it
|
|
119
|
+
here.
|
|
120
|
+
|
|
121
|
+
Add to `~/.gemini/settings.json`:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"mcpServers": {
|
|
126
|
+
"varnir": {
|
|
127
|
+
"command": "node",
|
|
128
|
+
"args": ["/absolute/path/to/apps/agent-server/dist/index.js"],
|
|
129
|
+
"env": {
|
|
130
|
+
"VARNIR_PRIVATE_KEY": "$VARNIR_PRIVATE_KEY",
|
|
131
|
+
"VARNIR_IDENTITY": "$VARNIR_IDENTITY",
|
|
132
|
+
"VARNIR_NETWORK": "testnet"
|
|
133
|
+
},
|
|
134
|
+
"timeout": 30000
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### ChatGPT (Streamable HTTP, over a tunnel)
|
|
141
|
+
|
|
142
|
+
ChatGPT cannot launch a local process - it needs a publicly reachable HTTPS
|
|
143
|
+
URL, so this recipe uses the MCP HTTP transport (`POST /mcp`) behind a tunnel
|
|
144
|
+
instead of stdio. `VARNIR_MCP_HTTP_TOKEN` (at least 32 characters) is now
|
|
145
|
+
**mandatory whenever `VARNIR_MCP_HTTP=on`, even on loopback** - without it,
|
|
146
|
+
any other local user or process on the same host could reach the full write
|
|
147
|
+
set with no authentication at all. A remote bind additionally requires
|
|
148
|
+
`VARNIR_MCP_HTTP_ALLOW_REMOTE=i-understand` - `cloudflared` puts you on a
|
|
149
|
+
public hostname even though the process itself still listens on loopback, so
|
|
150
|
+
in practice you need the token either way. Without the token set, the server
|
|
151
|
+
refuses to start with `VARNIR_MCP_HTTP=on` at all.
|
|
152
|
+
|
|
153
|
+
Run this from the repo root, same as "Install and run" above:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
VARNIR_PRIVATE_KEY=... VARNIR_IDENTITY=... VARNIR_NETWORK=testnet \
|
|
157
|
+
VARNIR_MCP_HTTP=on VARNIR_MCP_HTTP_TOKEN=<a-random-string-32-chars-or-longer> \
|
|
158
|
+
VARNIR_MCP_HTTP_ALLOW_REMOTE=i-understand \
|
|
159
|
+
node apps/agent-server/dist/index.js
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Expose it:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cloudflared tunnel --url http://localhost:8787
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Then in ChatGPT: Settings -> Connectors -> Advanced -> Developer mode -> Add
|
|
169
|
+
custom connector, URL `<tunnel-url>/mcp`, bearer token
|
|
170
|
+
`<the token you set above>`.
|
|
171
|
+
|
|
172
|
+
## The REST SecureAPI
|
|
173
|
+
|
|
174
|
+
The SDK's methods are also reachable over plain HTTP, for integrating
|
|
175
|
+
software that can only call HTTP endpoints rather than speak MCP. It is off
|
|
176
|
+
by default (`VARNIR_HTTP_API=off`) and every gate below is enforced in
|
|
177
|
+
`src/config.ts` before the process will even start with it on.
|
|
178
|
+
|
|
179
|
+
| Variable | Default | Required | Notes |
|
|
180
|
+
|---|---|---|---|
|
|
181
|
+
| `VARNIR_HTTP_API` | off | No | Enable with `on`/`true`/`1` |
|
|
182
|
+
| `VARNIR_HTTP_API_TOKEN` | - | Yes, when enabled | No default, minimum 32 characters |
|
|
183
|
+
| `VARNIR_HTTP_API_HOST` | `127.0.0.1` | No | Loopback by default |
|
|
184
|
+
| `VARNIR_HTTP_API_PORT` | 8788 | No | Integer [1, 65535] |
|
|
185
|
+
| `VARNIR_HTTP_API_WRITES` | off | No | Reads only until set `on` |
|
|
186
|
+
| `VARNIR_HTTP_API_ALLOW_REMOTE` | - | Conditional | Exactly `i-understand`; required for a non-loopback host |
|
|
187
|
+
|
|
188
|
+
**Turning this on with writes enabled and a non-loopback bind puts a
|
|
189
|
+
money-moving endpoint on the network.** Anyone who can reach that host and
|
|
190
|
+
port and holds the bearer token can move funds within whatever the signing
|
|
191
|
+
key's on-ledger policy allows - the policy still bounds them, but the network
|
|
192
|
+
exposure itself is real and irreversible the moment someone finds the port.
|
|
193
|
+
Keep it on loopback with writes off unless you have a specific, considered
|
|
194
|
+
reason to do otherwise, and prefer a tunnel with its own auth in front of it
|
|
195
|
+
over binding a public interface directly.
|
|
196
|
+
|
|
197
|
+
Every write method is also refused if the server's own key came up
|
|
198
|
+
read-only at boot (see "Before you start"), on top of the `VARNIR_HTTP_API_WRITES`
|
|
199
|
+
gate - two independent checks, not one.
|
|
200
|
+
|
|
201
|
+
One method needs calling out specifically: **`getWallet` is classified as a
|
|
202
|
+
write**, even though the name and its use elsewhere as a read look
|
|
203
|
+
innocuous. When this identity has no wallet yet on a given chain, `getWallet`
|
|
204
|
+
falls through to claiming a pregenerated L1 wallet from the pool and signing
|
|
205
|
+
a `Varnir.AssignOwner` transaction to establish ownership - a real
|
|
206
|
+
ledger-mutating write, on its first call for that chain. `send` calls
|
|
207
|
+
`getWallet` internally for the same reason. Both are refused with
|
|
208
|
+
`VARNIR_HTTP_API_WRITES=off` or in read-only mode, exactly like any other
|
|
209
|
+
write.
|
|
210
|
+
|
|
211
|
+
Example, once you have `VARNIR_HTTP_API=on` and a token set:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
curl -s -X POST http://127.0.0.1:8788/v1/getBalances \
|
|
215
|
+
-H "Authorization: Bearer $VARNIR_HTTP_API_TOKEN" \
|
|
216
|
+
-H "Content-Type: application/json" \
|
|
217
|
+
-d '{}'
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`GET /v1/methods` (also bearer-authenticated) lists every method the server
|
|
221
|
+
will accept and whether it is a write. `GET /v1/health` is unauthenticated and
|
|
222
|
+
carries no identity or key data - just whether the process is up and whether
|
|
223
|
+
it is running read-only.
|
|
224
|
+
|
|
225
|
+
## Security
|
|
226
|
+
|
|
227
|
+
The signing key lives in your environment - your host is the trust boundary,
|
|
228
|
+
not Varnir. Whoever can read that environment (or the process's memory) can
|
|
229
|
+
sign whatever the key's on-ledger policy allows. The on-ledger spending
|
|
230
|
+
policy - per-transaction ceiling, rolling-period cap, recipient allowlist,
|
|
231
|
+
co-sign threshold - is the real bound on a connected assistant, not this
|
|
232
|
+
server; this server is defence in depth around that bound (see `src/guard.ts`
|
|
233
|
+
for exactly what it checks and why). To revoke access, remove the key from
|
|
234
|
+
the identity in the scanner - that is the authoritative shutoff, independent
|
|
235
|
+
of whether this process is still running.
|
|
236
|
+
|
|
237
|
+
Reads are ungated by design: `get_balances`, `list_transactions`,
|
|
238
|
+
`get_identity`, and the equivalent REST methods, do not check the guard mode
|
|
239
|
+
and are always answerable once the server is running at all. Anything that
|
|
240
|
+
can talk to this server - over stdio, MCP HTTP, or the REST API - can see
|
|
241
|
+
your balances and transaction history, whether or not it can move anything.
|
|
242
|
+
Do not treat "read-only mode" as "safe to expose"; treat it as "cannot sign."
|
|
243
|
+
|
|
244
|
+
## What it will not do
|
|
245
|
+
|
|
246
|
+
- **No key management on any surface.** There is no tool and no REST method
|
|
247
|
+
to add a key, remove a key, change an approval threshold, or register or
|
|
248
|
+
revoke an API key - `addKey`, `removeKey`, `setApprovalThreshold`,
|
|
249
|
+
`registerApiKey` and `revokeApiKey` are absent from `REST_METHODS` on
|
|
250
|
+
purpose and must stay absent.
|
|
251
|
+
- **One deliberate, narrower exception:** a `verifier` or `dual` key (both
|
|
252
|
+
stake 80) that has NOT itself raised a transfer proposal can still hit the
|
|
253
|
+
ledger's co-sign band on that proposal - the contract treats that as a
|
|
254
|
+
genuine co-sign confirmation and releases it without re-checking that
|
|
255
|
+
key's own spend policy, because the policy was already checked when the
|
|
256
|
+
proposal was raised. `confirmTransfer` and `listPendingApprovals` would let
|
|
257
|
+
a connected assistant enumerate and release someone else's held proposal at
|
|
258
|
+
the proposal's amount, ignoring its own per-tx ceiling, period cap and
|
|
259
|
+
allowlist - the one way this server's design lets a key widen its own
|
|
260
|
+
effective authority. Both are absent from `REST_METHODS` for exactly this
|
|
261
|
+
reason and must stay absent; neither has an MCP tool, so nothing regresses.
|
|
262
|
+
Aside from this, nothing reachable from a connected assistant can widen
|
|
263
|
+
that assistant's own authority.
|
|
264
|
+
- **No remaining-allowance figure.** `get_spend_policy` reports the
|
|
265
|
+
*configured limits* on this key - per-transaction ceiling, rolling-period
|
|
266
|
+
cap, allowlist, co-sign threshold - not how much of the period's cap is
|
|
267
|
+
left. Remaining allowance against a rolling-period cap is not readable
|
|
268
|
+
through the SDK yet.
|
|
269
|
+
- **`get_transfer` cannot tell "still settling" apart from "held for a
|
|
270
|
+
co-sign that will never settle on its own"**, given only a umid. That
|
|
271
|
+
distinction genuinely exists only in `send`'s/`transfer_on_ledger`'s own
|
|
272
|
+
return at submission time (`awaiting_cosign` plus the `proposal`) - looking
|
|
273
|
+
the umid up afterwards, which is all `get_transfer` does, cannot recover it.
|
|
274
|
+
Telling them apart from a umid alone would need reading this identity's
|
|
275
|
+
pending-approvals list, which is deliberately not wired into any MCP tool
|
|
276
|
+
(see the point above) - so `get_transfer` says so plainly rather than
|
|
277
|
+
guessing, and a `not_settled` result should be read as "no receipt yet,"
|
|
278
|
+
not "still settling."
|
|
279
|
+
- **No writes behind an owner, policy-less, `readonly` or `offledger` key.**
|
|
280
|
+
The server starts read-only in all four cases rather than silently running
|
|
281
|
+
with unbounded or contract-refused authority; see "Before you start" for
|
|
282
|
+
what each one means and how to fix it.
|
package/dist/bearer.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bearer.d.ts","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKzE"}
|
package/dist/bearer.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Timing-safe bearer token comparison to prevent timing-based token guessing attacks.
|
|
4
|
+
*/
|
|
5
|
+
export function tokenMatches(presented, expected) {
|
|
6
|
+
const a = Buffer.from(presented);
|
|
7
|
+
const b = Buffer.from(expected);
|
|
8
|
+
if (a.length !== b.length)
|
|
9
|
+
return false;
|
|
10
|
+
return timingSafeEqual(a, b);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=bearer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bearer.js","sourceRoot":"","sources":["../src/bearer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,eAAe,EAAC,MAAM,aAAa,CAAC;AAE5C;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,SAAiB,EAAE,QAAgB;IAC9D,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,OAAO,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/B,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { redact } from './log.js';
|
|
2
|
+
export { redact };
|
|
3
|
+
/** A configuration problem that must stop the process before it serves anything. */
|
|
4
|
+
export declare class ConfigError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export interface ServerConfig {
|
|
8
|
+
privateKeyHex: string;
|
|
9
|
+
identity: string;
|
|
10
|
+
network: string;
|
|
11
|
+
/** True only for the exact phrase `i-understand`. See the guard in guard.ts. */
|
|
12
|
+
allowUnscopedKey: boolean;
|
|
13
|
+
mcpStdio: boolean;
|
|
14
|
+
mcpHttp: {
|
|
15
|
+
enabled: false;
|
|
16
|
+
} | {
|
|
17
|
+
enabled: true;
|
|
18
|
+
port: number;
|
|
19
|
+
host: string;
|
|
20
|
+
token: string;
|
|
21
|
+
remote: boolean;
|
|
22
|
+
};
|
|
23
|
+
restApi: {
|
|
24
|
+
enabled: false;
|
|
25
|
+
} | {
|
|
26
|
+
enabled: true;
|
|
27
|
+
host: string;
|
|
28
|
+
port: number;
|
|
29
|
+
token: string;
|
|
30
|
+
writes: boolean;
|
|
31
|
+
remote: boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
type Env = Record<string, string | undefined>;
|
|
35
|
+
export declare function loadConfig(env: Env): ServerConfig;
|
|
36
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,MAAM,EAAC,MAAM,UAAU,CAAC;AAEhC,OAAO,EAAC,MAAM,EAAC,CAAC;AAEhB,oFAAoF;AACpF,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAK5B;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE;QAAC,OAAO,EAAE,KAAK,CAAA;KAAC,GAAG;QAAC,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAC,CAAC;IACxG,OAAO,EACH;QAAC,OAAO,EAAE,KAAK,CAAA;KAAC,GAChB;QAAC,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAC,CAAC;CAClG;AAED,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AA8D9C,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,YAAY,CAkGjD"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { keyTypeOf } from '@varnir/chain-client';
|
|
2
|
+
import { redact } from './log.js';
|
|
3
|
+
export { redact };
|
|
4
|
+
/** A configuration problem that must stop the process before it serves anything. */
|
|
5
|
+
export class ConfigError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
this.name = 'ConfigError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const LOOPBACK = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
13
|
+
const MIN_TOKEN_LEN = 32;
|
|
14
|
+
function required(env, name) {
|
|
15
|
+
const v = env[name]?.trim();
|
|
16
|
+
if (!v) {
|
|
17
|
+
throw new ConfigError(`${name} is required. Set it in the environment (a secret manager should supply it) and start again.`);
|
|
18
|
+
}
|
|
19
|
+
return v;
|
|
20
|
+
}
|
|
21
|
+
/** `on`/`true`/`1` are on; anything else (including unset) is off. */
|
|
22
|
+
function flag(env, name, fallback = false) {
|
|
23
|
+
const v = env[name]?.trim().toLowerCase();
|
|
24
|
+
if (v === undefined || v === '')
|
|
25
|
+
return fallback;
|
|
26
|
+
return v === 'on' || v === 'true' || v === '1';
|
|
27
|
+
}
|
|
28
|
+
function port(env, name, fallback) {
|
|
29
|
+
const v = env[name]?.trim();
|
|
30
|
+
if (!v)
|
|
31
|
+
return fallback;
|
|
32
|
+
const n = Number(v);
|
|
33
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
34
|
+
throw new ConfigError(`${name} must be an integer port in [1, 65535]; got ${JSON.stringify(v)}.`);
|
|
35
|
+
}
|
|
36
|
+
return n;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Precedence: VARNIR_MCP_HTTP_PORT -> PORT -> 8787.
|
|
40
|
+
*
|
|
41
|
+
* Google Cloud Run and Azure Container Apps both inject a `PORT` environment
|
|
42
|
+
* variable at container start and require the process to listen on exactly
|
|
43
|
+
* that port - a container bound elsewhere is health-checked as down and
|
|
44
|
+
* killed, regardless of what it's actually doing. This server's own knob is
|
|
45
|
+
* VARNIR_MCP_HTTP_PORT (default 8787), which predates any cloud deployment
|
|
46
|
+
* target and must keep working unchanged for stdio/local/self-hosted users
|
|
47
|
+
* who set it deliberately. Falling back to PORT only when
|
|
48
|
+
* VARNIR_MCP_HTTP_PORT is unset lets the same image boot correctly on either
|
|
49
|
+
* platform without extra configuration, while an operator who explicitly
|
|
50
|
+
* sets VARNIR_MCP_HTTP_PORT (e.g. to run two instances behind one host)
|
|
51
|
+
* still gets exactly that value.
|
|
52
|
+
*
|
|
53
|
+
* The REST API's port (VARNIR_HTTP_API_PORT, default 8788) deliberately does
|
|
54
|
+
* NOT get this same PORT fallback: a container only has one port that is
|
|
55
|
+
* "the platform's" port (the one Cloud Run/ACA route traffic to and health
|
|
56
|
+
* check), and the MCP HTTP server is that surface for a cloud deployment -
|
|
57
|
+
* the REST API is off by default and, when enabled at all, is a secondary,
|
|
58
|
+
* self-hosted-oriented surface that isn't part of the one-click cloud path.
|
|
59
|
+
* Wiring PORT to REST's port too would silently break MCP whenever only one
|
|
60
|
+
* of the two servers is meant to claim the platform's assigned port.
|
|
61
|
+
*/
|
|
62
|
+
function mcpHttpPort(env) {
|
|
63
|
+
if (env.VARNIR_MCP_HTTP_PORT?.trim())
|
|
64
|
+
return port(env, 'VARNIR_MCP_HTTP_PORT', 8787);
|
|
65
|
+
if (env.PORT?.trim())
|
|
66
|
+
return port(env, 'PORT', 8787);
|
|
67
|
+
return 8787;
|
|
68
|
+
}
|
|
69
|
+
export function loadConfig(env) {
|
|
70
|
+
const privateKeyHex = required(env, 'VARNIR_PRIVATE_KEY');
|
|
71
|
+
// Validate key format based on key type
|
|
72
|
+
const keyType = keyTypeOf(privateKeyHex);
|
|
73
|
+
if (keyType === 'secp256k1') {
|
|
74
|
+
// secp256k1: must be hex
|
|
75
|
+
if (!/^(0x)?[0-9a-fA-F]+$/.test(privateKeyHex)) {
|
|
76
|
+
// Deliberately does not echo the value.
|
|
77
|
+
throw new ConfigError('VARNIR_PRIVATE_KEY must be a hex string.');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Post-quantum: must be <type>:<secretKey>:<publicKey>
|
|
82
|
+
const parts = privateKeyHex.split(':');
|
|
83
|
+
// Two-part format is a public key (<type>:<publicKey>), not a private
|
|
84
|
+
// key - but only when that second part is actually present. `<type>:`
|
|
85
|
+
// alone (parts[1] === '') isn't a public key either; it's simply a
|
|
86
|
+
// malformed private key, and must fall through to the generic format
|
|
87
|
+
// error below rather than be misdiagnosed as "here is a public key."
|
|
88
|
+
if (parts.length === 2 && parts[1]) {
|
|
89
|
+
throw new ConfigError(`VARNIR_PRIVATE_KEY appears to be a public key (${keyType}). A private key is required.`);
|
|
90
|
+
}
|
|
91
|
+
if (parts.length !== 3 || !parts[1] || !parts[2]) {
|
|
92
|
+
// Deliberately does not echo the value.
|
|
93
|
+
throw new ConfigError(`VARNIR_PRIVATE_KEY must be a valid ${keyType} private key in the format <type>:<secretKey>:<publicKey>.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const identity = required(env, 'VARNIR_IDENTITY');
|
|
97
|
+
const network = required(env, 'VARNIR_NETWORK');
|
|
98
|
+
const allowUnscopedKey = env.VARNIR_ALLOW_UNSCOPED_KEY?.trim() === 'i-understand';
|
|
99
|
+
const mcpStdio = flag(env, 'VARNIR_MCP_STDIO', true);
|
|
100
|
+
let mcpHttp = { enabled: false };
|
|
101
|
+
if (flag(env, 'VARNIR_MCP_HTTP')) {
|
|
102
|
+
const host = env.VARNIR_MCP_HTTP_HOST?.trim() || '127.0.0.1';
|
|
103
|
+
const remote = !LOOPBACK.has(host);
|
|
104
|
+
if (remote && env.VARNIR_MCP_HTTP_ALLOW_REMOTE?.trim() !== 'i-understand') {
|
|
105
|
+
throw new ConfigError(`VARNIR_MCP_HTTP_HOST is ${host}, which is not loopback. Binding the MCP HTTP server to a reachable address exposes a money-moving endpoint to the network. Set VARNIR_MCP_HTTP_ALLOW_REMOTE=i-understand if that is genuinely what you want.`);
|
|
106
|
+
}
|
|
107
|
+
// I4: mandatory even on loopback. VARNIR_MCP_HTTP=on alone used to put the
|
|
108
|
+
// full write set on 127.0.0.1 with NO authentication - any other local
|
|
109
|
+
// user or process on a shared host could move funds. REST already
|
|
110
|
+
// requires a token unconditionally; MCP HTTP now matches it.
|
|
111
|
+
const token = env.VARNIR_MCP_HTTP_TOKEN?.trim();
|
|
112
|
+
if (!token) {
|
|
113
|
+
throw new ConfigError('VARNIR_MCP_HTTP is on but VARNIR_MCP_HTTP_TOKEN is not set. The MCP HTTP server moves money; it will not start without a token, even on loopback - anything else on the host could otherwise reach it unauthenticated.');
|
|
114
|
+
}
|
|
115
|
+
if (token.length < MIN_TOKEN_LEN) {
|
|
116
|
+
throw new ConfigError(`VARNIR_MCP_HTTP_TOKEN must be at least ${MIN_TOKEN_LEN} characters. A weak token on a money-moving endpoint is a misconfiguration, not a preference.`);
|
|
117
|
+
}
|
|
118
|
+
mcpHttp = {
|
|
119
|
+
enabled: true,
|
|
120
|
+
port: mcpHttpPort(env),
|
|
121
|
+
host,
|
|
122
|
+
token,
|
|
123
|
+
remote,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
let restApi = { enabled: false };
|
|
127
|
+
if (flag(env, 'VARNIR_HTTP_API')) {
|
|
128
|
+
const token = env.VARNIR_HTTP_API_TOKEN?.trim();
|
|
129
|
+
if (!token) {
|
|
130
|
+
throw new ConfigError('VARNIR_HTTP_API is on but VARNIR_HTTP_API_TOKEN is not set. The REST API moves money; it will not start without a token. There is no default and none is generated for you.');
|
|
131
|
+
}
|
|
132
|
+
if (token.length < MIN_TOKEN_LEN) {
|
|
133
|
+
throw new ConfigError(`VARNIR_HTTP_API_TOKEN must be at least ${MIN_TOKEN_LEN} characters. A weak token on a money-moving endpoint is a misconfiguration, not a preference.`);
|
|
134
|
+
}
|
|
135
|
+
const host = env.VARNIR_HTTP_API_HOST?.trim() || '127.0.0.1';
|
|
136
|
+
const remote = !LOOPBACK.has(host);
|
|
137
|
+
if (remote && env.VARNIR_HTTP_API_ALLOW_REMOTE?.trim() !== 'i-understand') {
|
|
138
|
+
throw new ConfigError(`VARNIR_HTTP_API_HOST is ${host}, which is not loopback. Binding the REST API to a reachable address exposes a money-moving endpoint to the network. Set VARNIR_HTTP_API_ALLOW_REMOTE=i-understand if that is genuinely what you want.`);
|
|
139
|
+
}
|
|
140
|
+
restApi = {
|
|
141
|
+
enabled: true,
|
|
142
|
+
host,
|
|
143
|
+
port: port(env, 'VARNIR_HTTP_API_PORT', 8788),
|
|
144
|
+
token,
|
|
145
|
+
writes: flag(env, 'VARNIR_HTTP_API_WRITES'),
|
|
146
|
+
remote,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return { privateKeyHex, identity, network, allowUnscopedKey, mcpStdio, mcpHttp, restApi };
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,sBAAsB,CAAC;AAC/C,OAAO,EAAC,MAAM,EAAC,MAAM,UAAU,CAAC;AAEhC,OAAO,EAAC,MAAM,EAAC,CAAC;AAEhB,oFAAoF;AACpF,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF;AAiBD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;AAC5D,MAAM,aAAa,GAAG,EAAE,CAAC;AAEzB,SAAS,QAAQ,CAAC,GAAQ,EAAE,IAAY;IACtC,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,IAAI,WAAW,CACnB,GAAG,IAAI,8FAA8F,CACtG,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,sEAAsE;AACtE,SAAS,IAAI,CAAC,GAAQ,EAAE,IAAY,EAAE,QAAQ,GAAG,KAAK;IACpD,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC1C,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACjD,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,GAAG,CAAC;AACjD,CAAC;AAED,SAAS,IAAI,CAAC,GAAQ,EAAE,IAAY,EAAE,QAAgB;IACpD,MAAM,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,CAAC;QAAE,OAAO,QAAQ,CAAC;IACxB,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACpB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC;QAC/C,MAAM,IAAI,WAAW,CAAC,GAAG,IAAI,+CAA+C,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACpG,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAS,WAAW,CAAC,GAAQ;IAC3B,IAAI,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,sBAAsB,EAAE,IAAI,CAAC,CAAC;IACrF,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IACrD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAQ;IACjC,MAAM,aAAa,GAAG,QAAQ,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC;IAE1D,wCAAwC;IACxC,MAAM,OAAO,GAAG,SAAS,CAAC,aAAa,CAAC,CAAC;IACzC,IAAI,OAAO,KAAK,WAAW,EAAE,CAAC;QAC5B,yBAAyB;QACzB,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;YAC/C,wCAAwC;YACxC,MAAM,IAAI,WAAW,CAAC,0CAA0C,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;SAAM,CAAC;QACN,uDAAuD;QACvD,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACvC,sEAAsE;QACtE,sEAAsE;QACtE,mEAAmE;QACnE,qEAAqE;QACrE,qEAAqE;QACrE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACnC,MAAM,IAAI,WAAW,CAAC,kDAAkD,OAAO,+BAA+B,CAAC,CAAC;QAClH,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YACjD,wCAAwC;YACxC,MAAM,IAAI,WAAW,CAAC,sCAAsC,OAAO,4DAA4D,CAAC,CAAC;QACnI,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IAEhD,MAAM,gBAAgB,GAAG,GAAG,CAAC,yBAAyB,EAAE,IAAI,EAAE,KAAK,cAAc,CAAC;IAClF,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAC;IAErD,IAAI,OAAO,GAA4B,EAAC,OAAO,EAAE,KAAK,EAAC,CAAC;IACxD,IAAI,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,GAAG,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,IAAI,WAAW,CAAC;QAC7D,MAAM,MAAM,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,MAAM,IAAI,GAAG,CAAC,4BAA4B,EAAE,IAAI,EAAE,KAAK,cAAc,EAAE,CAAC;YAC1E,MAAM,IAAI,WAAW,CACnB,2BAA2B,IAAI,+MAA+M,CAC/O,CAAC;QACJ,CAAC;QACD,2EAA2E;QAC3E,uEAAuE;QACvE,kEAAkE;QAClE,6DAA6D;QAC7D,MAAM,KAAK,GAAG,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,CAAC;QAChD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,WAAW,CACnB,wNAAwN,CACzN,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;YACjC,MAAM,IAAI,WAAW,CACnB,0CAA0C,aAAa,+FAA+F,CACvJ,CAAC;QACJ,CAAC;QACD,OAAO,GAAG;YACR,OAAO,EAAE,IAAI;YACb,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC;YACtB,IAAI;YACJ,KAAK;YACL,MAAM;SACP,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,GAA4B,EAAC,OAAO,EAAE,KAAK,EAAC,CAAC;IACxD,IAAI,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,GAAG,CAAC,qBAAqB,EAAE,IAAI,EAAE,CAAC;QAChD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,WAAW,CACnB,6KAA6K,CAC9K,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,GAAG,aAAa,EAAE,CAAC;YACjC,MAAM,IAAI,WAAW,CACnB,0CAA0C,aAAa,+FAA+F,CACvJ,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,IAAI,WAAW,CAAC;QAC7D,MAAM,MAAM,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,MAAM,IAAI,GAAG,CAAC,4BAA4B,EAAE,IAAI,EAAE,KAAK,cAAc,EAAE,CAAC;YAC1E,MAAM,IAAI,WAAW,CACnB,2BAA2B,IAAI,wMAAwM,CACxO,CAAC;QACJ,CAAC;QACD,OAAO,GAAG;YACR,OAAO,EAAE,IAAI;YACb,IAAI;YACJ,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,sBAAsB,EAAE,IAAI,CAAC;YAC7C,KAAK;YACL,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,wBAAwB,CAAC;YAC3C,MAAM;SACP,CAAC;IACJ,CAAC;IAED,OAAO,EAAC,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAC,CAAC;AAC1F,CAAC"}
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { VarnirLedgerKey } from '@varnir/chain-client';
|
|
2
|
+
/**
|
|
3
|
+
* The whole security argument for this server is that the on-ledger spending
|
|
4
|
+
* policy is the hard bound on what a connected assistant can do. That argument
|
|
5
|
+
* holds ONLY if the key in VARNIR_PRIVATE_KEY is a policy-scoped agent key.
|
|
6
|
+
*
|
|
7
|
+
* Nothing stops an operator pasting their owner key into the environment, and
|
|
8
|
+
* the result would appear to work perfectly while having unbounded authority
|
|
9
|
+
* over the identity. So at boot we read our own key off the ledger and check
|
|
10
|
+
* what it actually is.
|
|
11
|
+
*
|
|
12
|
+
* This is defence in depth, not the bound itself - the contracts remain the
|
|
13
|
+
* real enforcement. Its value is catching the single most likely operator
|
|
14
|
+
* mistake before it can cost money.
|
|
15
|
+
*/
|
|
16
|
+
export type GuardMode = 'writes' | 'read-only' | 'refuse';
|
|
17
|
+
export interface KeyClassification {
|
|
18
|
+
mode: GuardMode;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `onWirePublicKey` MUST be the UNTAGGED on-wire value - i.e.
|
|
23
|
+
* `onWirePublicKey(privateKeyHex).publicKey`, NOT `publicKeyForPrivateKey()`,
|
|
24
|
+
* which tags PQ keys as `falcon-512:<pk>` and would never match what
|
|
25
|
+
* `listLedgerKeys()` reports. See key-match.ts.
|
|
26
|
+
*/
|
|
27
|
+
export declare function classifyKey(keys: VarnirLedgerKey[], onWirePublicKey: string, allowUnscoped: boolean): KeyClassification;
|
|
28
|
+
//# sourceMappingURL=guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,sBAAsB,CAAC;AAG1D;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,eAAe,EAAE,EACvB,eAAe,EAAE,MAAM,EACvB,aAAa,EAAE,OAAO,GACrB,iBAAiB,CA4DnB"}
|
package/dist/guard.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { samePublicKey } from './key-match.js';
|
|
2
|
+
/**
|
|
3
|
+
* `onWirePublicKey` MUST be the UNTAGGED on-wire value - i.e.
|
|
4
|
+
* `onWirePublicKey(privateKeyHex).publicKey`, NOT `publicKeyForPrivateKey()`,
|
|
5
|
+
* which tags PQ keys as `falcon-512:<pk>` and would never match what
|
|
6
|
+
* `listLedgerKeys()` reports. See key-match.ts.
|
|
7
|
+
*/
|
|
8
|
+
export function classifyKey(keys, onWirePublicKey, allowUnscoped) {
|
|
9
|
+
const mine = keys.find(k => samePublicKey(k.publicKey, onWirePublicKey));
|
|
10
|
+
if (!mine) {
|
|
11
|
+
// Not a degradation: this key could not sign anything on this identity at
|
|
12
|
+
// all, so every write would fail later anyway. Fail loudly and early.
|
|
13
|
+
return {
|
|
14
|
+
mode: 'refuse',
|
|
15
|
+
reason: 'This key is not attached to VARNIR_IDENTITY on the ledger. Check that VARNIR_PRIVATE_KEY and VARNIR_IDENTITY belong together, and that the key has not been revoked.',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (mine.isOwner || mine.role === 'owner') {
|
|
19
|
+
if (allowUnscoped) {
|
|
20
|
+
return { mode: 'writes', reason: 'Running with the identity OWNER key because VARNIR_ALLOW_UNSCOPED_KEY is set.' };
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
mode: 'read-only',
|
|
24
|
+
reason: 'This is the identity OWNER key, which carries no spending policy - an assistant holding it would have unbounded authority over your funds. Starting read-only. Mint a policy-scoped agent key in the scanner and use that instead, or set VARNIR_ALLOW_UNSCOPED_KEY=i-understand.',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (mine.role === 'readonly') {
|
|
28
|
+
return {
|
|
29
|
+
mode: 'read-only',
|
|
30
|
+
reason: 'This key is a read-only key by ledger design - it carries a 9000 stake that gates read-only access, and the money contracts refuse it write access regardless of any spend policy. A spend policy on this key cannot grant write ability. Mint a proposer or dual key carrying a spend policy in the scanner instead.',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (mine.role === 'offledger') {
|
|
34
|
+
return {
|
|
35
|
+
mode: 'read-only',
|
|
36
|
+
reason: 'This is an off-ledger key - the money contracts refuse it any fund movement regardless of any spend policy. A spend policy on this key is inert and will not grant write ability. Mint a proposer or dual key carrying a spend policy in the scanner instead.',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (mine.role === 'verifier') {
|
|
40
|
+
return {
|
|
41
|
+
mode: 'read-only',
|
|
42
|
+
reason: 'This is a verifier key - the money contracts refuse it outright ("A verifier key cannot raise transfers") regardless of any spend policy. A spend policy on this key cannot grant write ability. Mint a proposer or dual key carrying a spend policy in the scanner instead.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (!mine.policy) {
|
|
46
|
+
if (allowUnscoped) {
|
|
47
|
+
return { mode: 'writes', reason: 'Running with a key that carries no spend policy because VARNIR_ALLOW_UNSCOPED_KEY is set.' };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
mode: 'read-only',
|
|
51
|
+
reason: 'This key carries no on-ledger spend policy, so nothing would bound what a connected assistant could spend. Starting read-only. Add a spend policy to the key in the scanner, or set VARNIR_ALLOW_UNSCOPED_KEY=i-understand.',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { mode: 'writes', reason: 'Scoped agent key with an on-ledger spend policy.' };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAuB7C;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CACzB,IAAuB,EACvB,eAAuB,EACvB,aAAsB;IAEtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC,CAAC;IAEzE,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,0EAA0E;QAC1E,sEAAsE;QACtE,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,MAAM,EACJ,sKAAsK;SACzK,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC1C,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,EAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,+EAA+E,EAAC,CAAC;QACnH,CAAC;QACD,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EACJ,mRAAmR;SACtR,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC7B,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EACJ,uTAAuT;SAC1T,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;QAC9B,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EACJ,+PAA+P;SAClQ,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC7B,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EACJ,8QAA8Q;SACjR,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACjB,IAAI,aAAa,EAAE,CAAC;YAClB,OAAO,EAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,2FAA2F,EAAC,CAAC;QAC/H,CAAC;QACD,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,MAAM,EACJ,6NAA6N;SAChO,CAAC;IACJ,CAAC;IAED,OAAO,EAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,kDAAkD,EAAC,CAAC;AACtF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|