satgate 1.5.1
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 +21 -0
- package/README.md +207 -0
- package/dist/bin/satgate.d.ts +3 -0
- package/dist/bin/satgate.d.ts.map +1 -0
- package/dist/bin/satgate.js +7 -0
- package/dist/bin/satgate.js.map +1 -0
- package/dist/src/auth/allowlist.d.ts +24 -0
- package/dist/src/auth/allowlist.d.ts.map +1 -0
- package/dist/src/auth/allowlist.js +130 -0
- package/dist/src/auth/allowlist.js.map +1 -0
- package/dist/src/auth/middleware.d.ts +15 -0
- package/dist/src/auth/middleware.d.ts.map +1 -0
- package/dist/src/auth/middleware.js +29 -0
- package/dist/src/auth/middleware.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +275 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +133 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +237 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/discovery/llms-txt.d.ts +10 -0
- package/dist/src/discovery/llms-txt.d.ts.map +1 -0
- package/dist/src/discovery/llms-txt.js +25 -0
- package/dist/src/discovery/llms-txt.js.map +1 -0
- package/dist/src/discovery/openapi.d.ts +8 -0
- package/dist/src/discovery/openapi.d.ts.map +1 -0
- package/dist/src/discovery/openapi.js +116 -0
- package/dist/src/discovery/openapi.js.map +1 -0
- package/dist/src/discovery/well-known.d.ts +22 -0
- package/dist/src/discovery/well-known.d.ts.map +1 -0
- package/dist/src/discovery/well-known.js +44 -0
- package/dist/src/discovery/well-known.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lightning.d.ts +12 -0
- package/dist/src/lightning.d.ts.map +1 -0
- package/dist/src/lightning.js +38 -0
- package/dist/src/lightning.js.map +1 -0
- package/dist/src/logger.d.ts +16 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +99 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/proxy/capacity.d.ts +15 -0
- package/dist/src/proxy/capacity.d.ts.map +1 -0
- package/dist/src/proxy/capacity.js +28 -0
- package/dist/src/proxy/capacity.js.map +1 -0
- package/dist/src/proxy/handler.d.ts +27 -0
- package/dist/src/proxy/handler.d.ts.map +1 -0
- package/dist/src/proxy/handler.js +165 -0
- package/dist/src/proxy/handler.js.map +1 -0
- package/dist/src/proxy/pricing.d.ts +17 -0
- package/dist/src/proxy/pricing.d.ts.map +1 -0
- package/dist/src/proxy/pricing.js +42 -0
- package/dist/src/proxy/pricing.js.map +1 -0
- package/dist/src/proxy/streaming.d.ts +12 -0
- package/dist/src/proxy/streaming.d.ts.map +1 -0
- package/dist/src/proxy/streaming.js +68 -0
- package/dist/src/proxy/streaming.js.map +1 -0
- package/dist/src/proxy/token-counter.d.ts +22 -0
- package/dist/src/proxy/token-counter.d.ts.map +1 -0
- package/dist/src/proxy/token-counter.js +66 -0
- package/dist/src/proxy/token-counter.js.map +1 -0
- package/dist/src/server.d.ts +9 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +239 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/tunnel.d.ts +26 -0
- package/dist/src/tunnel.d.ts.map +1 -0
- package/dist/src/tunnel.js +78 -0
- package/dist/src/tunnel.js.map +1 -0
- package/dist/src/x402/facilitator.d.ts +7 -0
- package/dist/src/x402/facilitator.d.ts.map +1 -0
- package/dist/src/x402/facilitator.js +33 -0
- package/dist/src/x402/facilitator.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TheCryptoDonkey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# satgate
|
|
2
|
+
|
|
3
|
+
[](./LICENSE)
|
|
4
|
+
[](https://primal.net/p/npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
**Your GPU is burning money. Make it earn money.**
|
|
9
|
+
|
|
10
|
+
satgate sits in front of Ollama, vLLM, llama.cpp — any OpenAI-compatible backend — and turns it into a pay-per-token API. No accounts. No API keys. No Stripe. Clients pay per token, you earn sats before the response finishes streaming.
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx satgate --upstream http://localhost:11434
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. satgate auto-detects your models, starts accepting payments, and proxies inference requests. Clients pay per token, you earn sats.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Try it live
|
|
25
|
+
|
|
26
|
+
A public instance is running at [satgate.trotters.dev](https://satgate.trotters.dev). Open it in a browser for the chat playground, or use curl:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 250 sats of free usage per day per IP — after that you'll get a 402 + invoice
|
|
30
|
+
curl -s -w '\n%{http_code}\n' https://satgate.trotters.dev/v1/chat/completions \
|
|
31
|
+
-H "Content-Type: application/json" \
|
|
32
|
+
-d '{"model":"qwen3:0.6b","messages":[{"role":"user","content":"What is Bitcoin?"}]}'
|
|
33
|
+
|
|
34
|
+
# Check pricing
|
|
35
|
+
curl -s https://satgate.trotters.dev/.well-known/l402 | jq .
|
|
36
|
+
|
|
37
|
+
# Machine-readable description
|
|
38
|
+
curl -s https://satgate.trotters.dev/llms.txt
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## The old way vs satgate
|
|
44
|
+
|
|
45
|
+
| | The old way | With satgate |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| **Sell GPU time** | Sign up for a marketplace (OpenRouter, Together). They set the price, take a cut, own the customer. | `npx satgate --upstream http://localhost:11434`. You set the price. You keep 100%. |
|
|
48
|
+
| **Handle billing** | Stripe account, KYC, usage tracking, invoices, chargebacks | Payments settle before the response finishes streaming. No accounts, no disputes. |
|
|
49
|
+
| **Serve AI agents** | OAuth flows, API key management, billing portals — none of which machines can use | Agents discover your endpoint, pay per token from their own wallet, no human in the loop. |
|
|
50
|
+
| **Price fairly** | Flat rate per request, regardless of whether it's 10 tokens or 10,000 | Actual tokens counted from the response. Overpayments credited back. |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Built for machines
|
|
55
|
+
|
|
56
|
+
satgate doesn't just serve humans with `curl`. It's designed for AI agents that pay for their own resources.
|
|
57
|
+
|
|
58
|
+
Every satgate instance exposes three discovery endpoints — no auth required:
|
|
59
|
+
|
|
60
|
+
| Endpoint | Who reads it |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `/.well-known/l402` | Machines — pricing, models, payment methods as structured JSON |
|
|
63
|
+
| `/llms.txt` | AI agents — plain-text description of what you're selling |
|
|
64
|
+
| `/openapi.json` | Code generators — full OpenAPI spec |
|
|
65
|
+
|
|
66
|
+
Pair with [l402-mcp](https://github.com/TheCryptoDonkey/l402-mcp) and an AI agent can autonomously discover your endpoint, check your prices, pay from its own wallet, and start prompting — no human involved.
|
|
67
|
+
|
|
68
|
+
```mermaid
|
|
69
|
+
sequenceDiagram
|
|
70
|
+
participant A as AI Agent
|
|
71
|
+
participant M as l402-mcp
|
|
72
|
+
participant T as satgate
|
|
73
|
+
participant G as Your GPU
|
|
74
|
+
|
|
75
|
+
A->>M: "Use this inference endpoint"
|
|
76
|
+
M->>T: GET /.well-known/l402
|
|
77
|
+
T-->>M: Pricing, models, payment methods
|
|
78
|
+
M->>T: POST /v1/chat/completions
|
|
79
|
+
T-->>M: 402 + Lightning invoice
|
|
80
|
+
M->>M: Pay invoice from wallet
|
|
81
|
+
M->>T: Retry with L402 credential
|
|
82
|
+
T->>G: Proxy request
|
|
83
|
+
G-->>T: Stream response
|
|
84
|
+
T-->>M: Stream completion
|
|
85
|
+
M-->>A: Response
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## The secret
|
|
91
|
+
|
|
92
|
+
Everything you just saw — the payment gating, the multi-rail support, the credit system, the free tier, the macaroon credentials — that's not satgate. That's [toll-booth](https://github.com/TheCryptoDonkey/toll-booth).
|
|
93
|
+
|
|
94
|
+
satgate is ~400 lines of glue on top of toll-booth. It adds the AI-specific bits: token counting, model pricing, streaming reconciliation, capacity management. Everything else comes from the middleware.
|
|
95
|
+
|
|
96
|
+
**You could build your own satgate for your domain in an afternoon.**
|
|
97
|
+
|
|
98
|
+
Monetise a routing API. Gate a translation service. Sell weather data per request. toll-booth handles the payments — you just write the product logic.
|
|
99
|
+
|
|
100
|
+
→ [**See toll-booth**](https://github.com/TheCryptoDonkey/toll-booth)
|
|
101
|
+
|
|
102
|
+
```mermaid
|
|
103
|
+
graph TB
|
|
104
|
+
subgraph "satgate (~400 lines)"
|
|
105
|
+
TC[Token counting]
|
|
106
|
+
MP[Model pricing]
|
|
107
|
+
SR[Streaming reconciliation]
|
|
108
|
+
CM[Capacity management]
|
|
109
|
+
AD[Agent discovery]
|
|
110
|
+
end
|
|
111
|
+
subgraph "toll-booth"
|
|
112
|
+
L402[L402 protocol]
|
|
113
|
+
CR[Credit system]
|
|
114
|
+
FT[Free tier]
|
|
115
|
+
PR[Payment rails]
|
|
116
|
+
MA[Macaroon auth]
|
|
117
|
+
end
|
|
118
|
+
TC --> L402
|
|
119
|
+
MP --> CR
|
|
120
|
+
SR --> CR
|
|
121
|
+
CM --> L402
|
|
122
|
+
AD --> L402
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## What satgate adds
|
|
128
|
+
|
|
129
|
+
- **Pay-per-token** — actual token count from the response, not estimated. Streaming and buffered.
|
|
130
|
+
- **Model-specific pricing** — 1 sat/1k for Llama, 5 sats/1k for DeepSeek. You set the rates.
|
|
131
|
+
- **Streaming reconciliation** — estimated charge upfront, reconciled to actual usage after. Overpayments credited back.
|
|
132
|
+
- **Capacity management** — limit concurrent inference requests to protect your GPU.
|
|
133
|
+
- **Auto-detect models** — queries your upstream on startup. No manual model list.
|
|
134
|
+
- **Four payment rails** — Lightning, Cashu ecash, NWC, and x402 stablecoins. Operator picks what to accept.
|
|
135
|
+
- **Privacy by design** — no personal data collected or stored. No accounts, no cookies, no IP logging. GDPR-safe out of the box.
|
|
136
|
+
- **Instant public URL** — auto-spawns a Cloudflare tunnel. Your GPU is reachable from the internet in seconds.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## How it works
|
|
141
|
+
|
|
142
|
+
```mermaid
|
|
143
|
+
sequenceDiagram
|
|
144
|
+
participant C as Client
|
|
145
|
+
participant T as satgate
|
|
146
|
+
participant G as Your GPU
|
|
147
|
+
|
|
148
|
+
C->>T: POST /v1/chat/completions
|
|
149
|
+
T-->>C: 402 + Lightning invoice (estimated cost)
|
|
150
|
+
C->>C: Pay invoice
|
|
151
|
+
C->>T: Retry with L402 credential
|
|
152
|
+
T->>G: Proxy request
|
|
153
|
+
G-->>T: Stream response
|
|
154
|
+
T->>T: Count actual tokens
|
|
155
|
+
T-->>C: Stream completion
|
|
156
|
+
T->>T: Reconcile: credit back overpayment
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Charges are estimated upfront based on model pricing, then reconciled to actual token usage after the response completes. Operators are never short-changed — costs round up. Overpayments are credited to the client's balance for the next request.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Configuration
|
|
164
|
+
|
|
165
|
+
Zero config works (just `--upstream`). For production, create `satgate.yaml`:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
upstream: http://localhost:11434
|
|
169
|
+
port: 3000
|
|
170
|
+
pricing:
|
|
171
|
+
default: 1 # 1 sat per 1k tokens
|
|
172
|
+
models:
|
|
173
|
+
llama3: 1
|
|
174
|
+
deepseek-r1: 5
|
|
175
|
+
freeTier:
|
|
176
|
+
creditsPerDay: 250
|
|
177
|
+
capacity:
|
|
178
|
+
maxConcurrent: 4
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
CLI flags > environment variables > config file > defaults.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Get started
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# Monetise your local Ollama
|
|
189
|
+
npx satgate --upstream http://localhost:11434
|
|
190
|
+
|
|
191
|
+
# Or point at any OpenAI-compatible backend
|
|
192
|
+
npx satgate --upstream http://your-vllm-server:8000
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
→ [**toll-booth**](https://github.com/TheCryptoDonkey/toll-booth) — the middleware that powers all of this. Build your own.
|
|
196
|
+
→ [**l402-mcp**](https://github.com/TheCryptoDonkey/l402-mcp) — give AI agents a wallet. Let them pay for your GPU.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
Built by [@TheCryptoDonkey](https://github.com/TheCryptoDonkey).
|
|
201
|
+
|
|
202
|
+
- Lightning tips: `thedonkey@strike.me`
|
|
203
|
+
- Nostr: `npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"satgate.d.ts","sourceRoot":"","sources":["../../bin/satgate.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"satgate.js","sourceRoot":"","sources":["../../bin/satgate.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAA;AACpC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;IAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface AllowlistResult {
|
|
2
|
+
allowed: boolean;
|
|
3
|
+
identity?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface RequestContext {
|
|
6
|
+
url: string;
|
|
7
|
+
method: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Checks whether the Authorization header matches an entry in the allowlist.
|
|
11
|
+
*
|
|
12
|
+
* Identity types:
|
|
13
|
+
* - Bearer <secret> — matched against non-pubkey entries (strings that are
|
|
14
|
+
* NOT 64-char hex and NOT npub1-prefixed)
|
|
15
|
+
* - Nostr <base64-event> — NIP-98 verification against pubkey entries
|
|
16
|
+
*
|
|
17
|
+
* Security: hex pubkeys and npub entries are NEVER treated as Bearer secrets.
|
|
18
|
+
* Only entries that don't look like pubkeys are valid Bearer secrets.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* @param now - Optional Unix timestamp (seconds) for testing. Defaults to current time.
|
|
22
|
+
*/
|
|
23
|
+
export declare function checkAllowlist(authHeader: string | undefined, allowlist: string[], request: RequestContext, now?: number): AllowlistResult;
|
|
24
|
+
//# sourceMappingURL=allowlist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.d.ts","sourceRoot":"","sources":["../../../src/auth/allowlist.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;CACf;AA2DD;;;;;;;;;;GAUG;AACH;;GAEG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,SAAS,EAAE,MAAM,EAAE,EACnB,OAAO,EAAE,cAAc,EACvB,GAAG,CAAC,EAAE,MAAM,GACX,eAAe,CA4BjB"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
2
|
+
import { hexToBytes } from '@noble/curves/utils.js';
|
|
3
|
+
import { bech32 } from '@scure/base';
|
|
4
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
5
|
+
const HEX_PUBKEY_RE = /^[0-9a-f]{64}$/;
|
|
6
|
+
/**
|
|
7
|
+
* Decode an npub (bech32-encoded) to a hex pubkey.
|
|
8
|
+
*/
|
|
9
|
+
function npubToHex(npub) {
|
|
10
|
+
try {
|
|
11
|
+
const { prefix, words } = bech32.decode(npub);
|
|
12
|
+
if (prefix !== 'npub')
|
|
13
|
+
return null;
|
|
14
|
+
const bytes = bech32.fromWords(words);
|
|
15
|
+
return Buffer.from(bytes).toString('hex');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Normalise allowlist entries to hex pubkeys.
|
|
23
|
+
* Returns only the pubkey entries (npub or hex), not shared secrets.
|
|
24
|
+
*/
|
|
25
|
+
function extractPubkeys(allowlist) {
|
|
26
|
+
const pubkeys = [];
|
|
27
|
+
for (const entry of allowlist) {
|
|
28
|
+
if (entry.startsWith('npub1')) {
|
|
29
|
+
const hex = npubToHex(entry);
|
|
30
|
+
if (hex)
|
|
31
|
+
pubkeys.push(hex);
|
|
32
|
+
}
|
|
33
|
+
else if (HEX_PUBKEY_RE.test(entry)) {
|
|
34
|
+
pubkeys.push(entry);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return pubkeys;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Constant-time string comparison to prevent timing attacks on Bearer tokens.
|
|
41
|
+
*/
|
|
42
|
+
function constantTimeEqual(a, b) {
|
|
43
|
+
const bufA = Buffer.from(a);
|
|
44
|
+
const bufB = Buffer.from(b);
|
|
45
|
+
if (bufA.length !== bufB.length) {
|
|
46
|
+
// Compare against self to keep constant time even on length mismatch
|
|
47
|
+
timingSafeEqual(bufA, bufA);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return timingSafeEqual(bufA, bufB);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Checks whether the Authorization header matches an entry in the allowlist.
|
|
54
|
+
*
|
|
55
|
+
* Identity types:
|
|
56
|
+
* - Bearer <secret> — matched against non-pubkey entries (strings that are
|
|
57
|
+
* NOT 64-char hex and NOT npub1-prefixed)
|
|
58
|
+
* - Nostr <base64-event> — NIP-98 verification against pubkey entries
|
|
59
|
+
*
|
|
60
|
+
* Security: hex pubkeys and npub entries are NEVER treated as Bearer secrets.
|
|
61
|
+
* Only entries that don't look like pubkeys are valid Bearer secrets.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* @param now - Optional Unix timestamp (seconds) for testing. Defaults to current time.
|
|
65
|
+
*/
|
|
66
|
+
export function checkAllowlist(authHeader, allowlist, request, now) {
|
|
67
|
+
if (!authHeader || allowlist.length === 0) {
|
|
68
|
+
return { allowed: false };
|
|
69
|
+
}
|
|
70
|
+
const spaceIdx = authHeader.indexOf(' ');
|
|
71
|
+
if (spaceIdx === -1)
|
|
72
|
+
return { allowed: false };
|
|
73
|
+
const scheme = authHeader.slice(0, spaceIdx);
|
|
74
|
+
const credential = authHeader.slice(spaceIdx + 1);
|
|
75
|
+
if (scheme === 'Bearer') {
|
|
76
|
+
// Only match entries that are NOT pubkeys (hex or npub)
|
|
77
|
+
const secrets = allowlist.filter(entry => !entry.startsWith('npub1') && !HEX_PUBKEY_RE.test(entry));
|
|
78
|
+
const match = secrets.find(s => constantTimeEqual(s, credential));
|
|
79
|
+
if (match) {
|
|
80
|
+
return { allowed: true, identity: credential.slice(0, 8) + '...' };
|
|
81
|
+
}
|
|
82
|
+
return { allowed: false };
|
|
83
|
+
}
|
|
84
|
+
if (scheme === 'Nostr') {
|
|
85
|
+
return verifyNip98(credential, allowlist, request, now);
|
|
86
|
+
}
|
|
87
|
+
return { allowed: false };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Verify a NIP-98 HTTP Auth event against the allowlist of pubkeys.
|
|
91
|
+
*/
|
|
92
|
+
function verifyNip98(base64Event, allowlist, request, nowOverride) {
|
|
93
|
+
try {
|
|
94
|
+
const json = Buffer.from(base64Event, 'base64').toString('utf-8');
|
|
95
|
+
const event = JSON.parse(json);
|
|
96
|
+
// Must be kind 27235 (NIP-98 HTTP Auth)
|
|
97
|
+
if (event.kind !== 27235)
|
|
98
|
+
return { allowed: false };
|
|
99
|
+
// Check created_at is within 60 seconds (injectable for testing)
|
|
100
|
+
const now = nowOverride ?? Math.floor(Date.now() / 1000);
|
|
101
|
+
if (Math.abs(now - event.created_at) > 60)
|
|
102
|
+
return { allowed: false };
|
|
103
|
+
// Validate URL and method tags match the actual request
|
|
104
|
+
const urlTag = event.tags.find(t => t[0] === 'u')?.[1];
|
|
105
|
+
const methodTag = event.tags.find(t => t[0] === 'method')?.[1];
|
|
106
|
+
if (urlTag !== request.url)
|
|
107
|
+
return { allowed: false };
|
|
108
|
+
if (methodTag?.toUpperCase() !== request.method.toUpperCase())
|
|
109
|
+
return { allowed: false };
|
|
110
|
+
// Verify event ID
|
|
111
|
+
const serialised = JSON.stringify([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]);
|
|
112
|
+
const expectedId = createHash('sha256').update(serialised).digest('hex');
|
|
113
|
+
if (event.id !== expectedId)
|
|
114
|
+
return { allowed: false };
|
|
115
|
+
// Verify schnorr signature
|
|
116
|
+
const valid = schnorr.verify(hexToBytes(event.sig), hexToBytes(event.id), hexToBytes(event.pubkey));
|
|
117
|
+
if (!valid)
|
|
118
|
+
return { allowed: false };
|
|
119
|
+
// Check pubkey against allowlist (normalise npub → hex)
|
|
120
|
+
const allowedPubkeys = extractPubkeys(allowlist);
|
|
121
|
+
if (allowedPubkeys.includes(event.pubkey)) {
|
|
122
|
+
return { allowed: true, identity: event.pubkey.slice(0, 8) + '...' };
|
|
123
|
+
}
|
|
124
|
+
return { allowed: false };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return { allowed: false };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=allowlist.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowlist.js","sourceRoot":"","sources":["../../../src/auth/allowlist.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
export interface AuthMiddlewareConfig {
|
|
3
|
+
authMode: 'open' | 'lightning' | 'allowlist';
|
|
4
|
+
allowlist: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Creates Hono middleware that handles auth based on the configured mode.
|
|
8
|
+
*
|
|
9
|
+
* - open: pass through (no checks)
|
|
10
|
+
* - allowlist: check Authorization header against allowlist
|
|
11
|
+
* - lightning: pass through — toll-booth's authMiddleware is mounted separately
|
|
12
|
+
* in server.ts for the lightning path
|
|
13
|
+
*/
|
|
14
|
+
export declare function createAuthMiddleware(config: AuthMiddlewareConfig): MiddlewareHandler;
|
|
15
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/auth/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAA;AAG7C,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,WAAW,CAAA;IAC5C,SAAS,EAAE,MAAM,EAAE,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,iBAAiB,CAmBpF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { checkAllowlist } from './allowlist.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates Hono middleware that handles auth based on the configured mode.
|
|
4
|
+
*
|
|
5
|
+
* - open: pass through (no checks)
|
|
6
|
+
* - allowlist: check Authorization header against allowlist
|
|
7
|
+
* - lightning: pass through — toll-booth's authMiddleware is mounted separately
|
|
8
|
+
* in server.ts for the lightning path
|
|
9
|
+
*/
|
|
10
|
+
export function createAuthMiddleware(config) {
|
|
11
|
+
if (config.authMode === 'allowlist') {
|
|
12
|
+
return async (c, next) => {
|
|
13
|
+
const authHeader = c.req.header('Authorization');
|
|
14
|
+
const requestUrl = c.req.url;
|
|
15
|
+
const requestMethod = c.req.method;
|
|
16
|
+
const result = checkAllowlist(authHeader, config.allowlist, {
|
|
17
|
+
url: requestUrl,
|
|
18
|
+
method: requestMethod,
|
|
19
|
+
});
|
|
20
|
+
if (!result.allowed) {
|
|
21
|
+
return c.json({ error: 'Forbidden' }, 403);
|
|
22
|
+
}
|
|
23
|
+
await next();
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// open mode and lightning mode: pass through
|
|
27
|
+
return async (_c, next) => { await next(); };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../src/auth/middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAO/C;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAA4B;IAC/D,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,OAAO,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE;YACvB,MAAM,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;YAChD,MAAM,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAA;YAC5B,MAAM,aAAa,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAA;YAClC,MAAM,MAAM,GAAG,cAAc,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,EAAE;gBAC1D,GAAG,EAAE,UAAU;gBACf,MAAM,EAAE,aAAa;aACtB,CAAC,CAAA;YACF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,GAAG,CAAC,CAAA;YAC5C,CAAC;YACD,MAAM,IAAI,EAAE,CAAA;QACd,CAAC,CAAA;IACH,CAAC;IAED,6CAA6C;IAC7C,OAAO,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,GAAG,MAAM,IAAI,EAAE,CAAA,CAAC,CAAC,CAAA;AAC7C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAqHA,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqHvE"}
|