@shunt-network/verify 0.0.0-stage → 1.0.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/README.md CHANGED
@@ -1,3 +1,275 @@
1
- # Temporary Holding Version
1
+ # @shunt-network/verify
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Verify that incoming requests to your API really came through the **Shunt gateway** and were paid for.
4
+
5
+ Shunt is a payment gateway and marketplace for AI agents on Solana. When an agent calls your tool, the gateway verifies the agent's USDC payment-channel voucher and then forwards the request to your API. Every forwarded request is **signed**. This package checks that signature, so nobody can skip the paywall by hitting your endpoint directly with `curl` or Postman.
6
+
7
+ - Zero runtime dependencies (only `node:crypto`)
8
+ - Works with Express, Next.js (App Router), Hono, Cloudflare Workers, Deno and Supabase Edge Functions
9
+ - Replay protection, timestamp window, constant-time comparison
10
+ - Supports two active secrets for painless rotation
11
+
12
+ ---
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @shunt-network/verify
18
+ ```
19
+
20
+ Deno / Supabase Edge Functions:
21
+
22
+ ```ts
23
+ import { verifyShunt } from "npm:@shunt-network/verify@0.1.0";
24
+ ```
25
+
26
+ ---
27
+
28
+ ## How it works
29
+
30
+ 1. You list your API on Shunt. Shunt gives you a **signing secret** (`shunt_whsec_...`), shown once in the dashboard.
31
+ 2. For every paid call, the gateway adds these headers to the forwarded request:
32
+
33
+ | Header | Meaning |
34
+ |---|---|
35
+ | `x-shunt-timestamp` | Unix time (seconds) when the gateway signed the request |
36
+ | `x-shunt-request-id` | Unique ID for this call |
37
+ | `x-shunt-signature` | `v1=<hex HMAC-SHA256>` |
38
+ | `x-shunt-agent` | Agent wallet address. **Informational only, not covered by the signature** |
39
+
40
+ 3. The signature is an HMAC-SHA256, using your secret, over:
41
+
42
+ ```
43
+ v1
44
+ {timestamp}
45
+ {request_id}
46
+ {METHOD}
47
+ {path_and_query}
48
+ {sha256_hex(raw_body)}
49
+ ```
50
+
51
+ (joined with `\n`). Because the method, path, query and body hash are all signed, a request can't be modified or replayed against a different endpoint.
52
+
53
+ 4. This package recomputes the signature and rejects the request if anything is missing, stale, or doesn't match.
54
+
55
+ ---
56
+
57
+ ## Quick start
58
+
59
+ ### Express
60
+
61
+ ```ts
62
+ import express from "express";
63
+ import { shuntExpress, shuntRawBody } from "@shunt-network/verify";
64
+
65
+ const app = express();
66
+
67
+ // Keep the raw bytes: the signature covers a hash of the exact body.
68
+ app.use(express.json({ verify: shuntRawBody }));
69
+
70
+ // Protect every paid route.
71
+ app.use("/api/paid", shuntExpress(process.env.SHUNT_SECRET!));
72
+
73
+ app.post("/api/paid/scrape", (req, res) => {
74
+ console.log(req.shunt); // { requestId, agent }
75
+ res.json({ ok: true });
76
+ });
77
+ ```
78
+
79
+ ### Next.js (App Router)
80
+
81
+ ```ts
82
+ // app/api/scrape/route.ts
83
+ import { withShunt } from "@shunt-network/verify";
84
+
85
+ export const POST = withShunt(process.env.SHUNT_SECRET!, async (req) => {
86
+ const body = await req.json();
87
+ return Response.json({ ok: true, body });
88
+ });
89
+ ```
90
+
91
+ ### Hono
92
+
93
+ ```ts
94
+ import { Hono } from "hono";
95
+ import { shuntHono } from "@shunt-network/verify";
96
+
97
+ const app = new Hono();
98
+ app.use("/paid/*", shuntHono(process.env.SHUNT_SECRET!));
99
+ app.post("/paid/scrape", (c) => c.json({ ok: true }));
100
+ ```
101
+
102
+ ### Supabase Edge Functions / Deno
103
+
104
+ Use the core function so you control the path that gets verified:
105
+
106
+ ```ts
107
+ import { verifyShunt } from "npm:@shunt-network/verify@0.1.0";
108
+
109
+ const SHUNT_SECRET = Deno.env.get("SHUNT_SECRET")!;
110
+ const PATH_PREFIX = "/functions/v1"; // stripped from req.url inside the runtime
111
+
112
+ Deno.serve(async (req) => {
113
+ const rawBody = new Uint8Array(await req.arrayBuffer()); // read ONCE
114
+ const url = new URL(req.url);
115
+
116
+ const result = verifyShunt({
117
+ secret: SHUNT_SECRET,
118
+ method: req.method,
119
+ pathAndQuery: PATH_PREFIX + url.pathname + url.search,
120
+ rawBody,
121
+ getHeader: (n) => req.headers.get(n),
122
+ });
123
+
124
+ if (!result.ok) {
125
+ return Response.json({ error: "Unauthorized: requests must come via Shunt" }, { status: 401 });
126
+ }
127
+
128
+ const body = JSON.parse(new TextDecoder().decode(rawBody));
129
+ return Response.json({ ok: true, body });
130
+ });
131
+ ```
132
+
133
+ Also disable Supabase's own JWT check for the function (`verify_jwt = false` in `config.toml`, or `--no-verify-jwt` on deploy). The gateway doesn't send a Supabase JWT, and your Shunt signature check replaces it.
134
+
135
+ ---
136
+
137
+ ## API
138
+
139
+ ### `verifyShunt(input): VerifyResult`
140
+
141
+ Framework-agnostic core. Everything else is a thin wrapper around it.
142
+
143
+ ```ts
144
+ interface VerifyInput {
145
+ secret: string | string[]; // array = accept any (rotation)
146
+ method: string; // "POST"
147
+ pathAndQuery: string; // exactly as received, e.g. "/v2/scrape?url=..."
148
+ rawBody: Buffer | Uint8Array | string;
149
+ getHeader: (name: string) => string | undefined | null;
150
+ maxSkewSec?: number; // default 300 (5 minutes)
151
+ replayCache?: ReplayCache; // optional duplicate-ID detection
152
+ }
153
+
154
+ type VerifyResult =
155
+ | { ok: true; requestId: string }
156
+ | { ok: false; reason: "missing_headers" | "stale_timestamp" | "bad_signature" | "replay" };
157
+ ```
158
+
159
+ ### `shuntExpress(secret, opts?)`
160
+
161
+ Express/Connect middleware. Responds `401` on failure and sets `req.shunt = { requestId, agent }` on success.
162
+
163
+ Options: `maxSkewSec?: number`, `replayProtection?: boolean`.
164
+
165
+ ### `shuntRawBody`
166
+
167
+ Pass to `express.json({ verify: shuntRawBody })` (or `express.raw`) so `req.rawBody` is available.
168
+
169
+ ### `withShunt(secret, handler)`
170
+
171
+ Wraps a Fetch-API route handler (Next.js App Router, Cloudflare Workers, etc.). Returns `401` JSON if verification fails, otherwise calls your handler.
172
+
173
+ ### `shuntHono(secret)`
174
+
175
+ Hono middleware.
176
+
177
+ ### `verifyShuntRequest(req, secret, opts?)`
178
+
179
+ Verify a standard `Request` object directly. Reads a clone of the body, so your handler can still read it.
180
+
181
+ ### `ReplayCache`
182
+
183
+ In-memory cache of seen request IDs (10 minute TTL by default). Request IDs are recorded only **after** the signature is valid, so attackers can't fill the cache with junk.
184
+
185
+ ```ts
186
+ import { ReplayCache, verifyShunt } from "@shunt-network/verify";
187
+ const replayCache = new ReplayCache();
188
+ ```
189
+
190
+ It's per-process. On serverless or edge platforms with short-lived isolates it isn't reliable, and the timestamp window already limits replay to a few minutes. Use a shared store (Redis, etc.) if you need strict guarantees.
191
+
192
+ ---
193
+
194
+ ## Secret rotation
195
+
196
+ Pass both the new and the old secret while you roll over, and drop the old one once traffic has switched:
197
+
198
+ ```ts
199
+ shuntExpress([process.env.SHUNT_SECRET!, process.env.SHUNT_SECRET_PREV!]);
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Common problems
205
+
206
+ **Everything returns 401 through the gateway**
207
+
208
+ 1. **Raw body.** The signature is over the exact bytes. If you hash a re-serialized `JSON.stringify(req.body)`, it won't match. Capture the raw bytes (`shuntRawBody` for Express).
209
+ 2. **Path mismatch.** `pathAndQuery` must equal what the gateway sent to your server, including any prefix from the `base_url` you registered. For example, a base URL of `https://api.example.com/v2` and a tool path of `/scrape` is signed as `/v2/scrape`. In Express, `req.originalUrl` is correct. If a reverse proxy or framework rewrites the path before your code sees it, pass the original path.
210
+ 3. **Wrong secret.** Each service has its own `shunt_whsec_...`. Check for trailing whitespace in your env var.
211
+ 4. **Clock skew.** Requests older than 5 minutes (or too far in the future) are rejected. Keep your server clock synced with NTP, or raise `maxSkewSec`.
212
+
213
+ **Direct calls still work.** Make sure the middleware runs before your handlers and covers every paid route.
214
+
215
+ ---
216
+
217
+ ## Best practices
218
+
219
+ - **Verify first.** Put the middleware at the top, before any parsing, database access, or expensive work.
220
+ - **Return real status codes.** The gateway automatically **refunds the agent** when your API returns a 5xx or certain 4xx errors (401, 403, 404, 408, 429). If your API fails but returns `200` with `{ "error": ... }`, the agent is still charged.
221
+ - **Treat `x-shunt-agent` as a hint.** It isn't signed. Don't use it for authorization.
222
+ - **Keep the secret out of source control.** Use an environment variable or secrets manager. If it leaks, regenerate it from the Shunt dashboard.
223
+ - **Selling elsewhere too?** This package verifies Shunt only. If you list your API on other marketplaces or accept other payment methods, mount those on separate routes or middleware.
224
+
225
+ ---
226
+
227
+ ## Testing locally
228
+
229
+ Simulate the gateway with `curl`:
230
+
231
+ ```bash
232
+ SECRET="shunt_whsec_your_secret"
233
+ URL="http://localhost:3000/api/paid/scrape"
234
+ SIGNED_PATH="/api/paid/scrape"
235
+ BODY='{"url":"https://example.com"}'
236
+
237
+ TS=$(date +%s)
238
+ RID=$(uuidgen)
239
+ BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')
240
+ PAYLOAD=$(printf 'v1\n%s\n%s\nPOST\n%s\n%s' "$TS" "$RID" "$SIGNED_PATH" "$BODY_HASH")
241
+ SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
242
+
243
+ curl -i -X POST "$URL" \
244
+ -H "Content-Type: application/json" \
245
+ -H "x-shunt-timestamp: $TS" \
246
+ -H "x-shunt-request-id: $RID" \
247
+ -H "x-shunt-signature: v1=$SIG" \
248
+ --data-binary "$BODY"
249
+ ```
250
+
251
+ Expected results:
252
+
253
+ | Request | Result |
254
+ |---|---|
255
+ | Direct call, no headers | `401` |
256
+ | Correctly signed | `200` |
257
+ | Signed, then body changed | `401` |
258
+
259
+ Use `--data-binary` rather than `-d` so curl sends exactly the bytes that were hashed.
260
+
261
+ ---
262
+
263
+ ## Security notes
264
+
265
+ - Signatures use HMAC-SHA256 and are compared in constant time.
266
+ - The signed payload binds method, path, query, body hash, timestamp and request ID.
267
+ - The gateway strips any client-supplied `x-shunt-*` headers before signing, so callers can't forge them.
268
+ - The gateway doesn't follow redirects from your API.
269
+ - Secrets are stored encrypted (AES-256-GCM) on the gateway side.
270
+
271
+ ---
272
+
273
+ ## License
274
+
275
+ MIT
package/dist/core.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ export type VerifyResult = {
2
+ ok: true;
3
+ requestId: string;
4
+ } | {
5
+ ok: false;
6
+ reason: "missing_headers" | "stale_timestamp" | "bad_signature" | "replay";
7
+ };
8
+ export interface VerifyInput {
9
+ secret: string | string[];
10
+ method: string;
11
+ pathAndQuery: string;
12
+ rawBody: Buffer | Uint8Array | string;
13
+ getHeader: (name: string) => string | undefined | null;
14
+ maxSkewSec?: number;
15
+ replayCache?: ReplayCache;
16
+ }
17
+ export declare class ReplayCache {
18
+ private ttlMs;
19
+ private seen;
20
+ constructor(ttlMs?: number);
21
+ /** returns true if the id was already seen */
22
+ check(id: string): boolean;
23
+ }
24
+ export declare function verifyShunt(i: VerifyInput): VerifyResult;
25
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,eAAe,GAAG,QAAQ,CAAA;CAAE,CAAC;AAE9F,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IACtC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,WAAW;IAEV,OAAO,CAAC,KAAK;IADzB,OAAO,CAAC,IAAI,CAA6B;IACzC,YAAoB,KAAK,SAAc,EAAI;IAC3C,8CAA8C;IAC9C,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAMzB;CACF;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,WAAW,GAAG,YAAY,CA4BxD"}
package/dist/core.js ADDED
@@ -0,0 +1,44 @@
1
+ import crypto from "node:crypto";
2
+ export class ReplayCache {
3
+ ttlMs;
4
+ seen = new Map();
5
+ constructor(ttlMs = 10 * 60_000) {
6
+ this.ttlMs = ttlMs;
7
+ }
8
+ /** returns true if the id was already seen */
9
+ check(id) {
10
+ const now = Date.now();
11
+ for (const [k, exp] of this.seen)
12
+ if (exp < now)
13
+ this.seen.delete(k);
14
+ if (this.seen.has(id))
15
+ return true;
16
+ this.seen.set(id, now + this.ttlMs);
17
+ return false;
18
+ }
19
+ }
20
+ export function verifyShunt(i) {
21
+ const ts = i.getHeader("x-shunt-timestamp");
22
+ const rid = i.getHeader("x-shunt-request-id");
23
+ const sig = (i.getHeader("x-shunt-signature") ?? "").replace(/^v1=/, "");
24
+ if (!ts || !rid || !sig)
25
+ return { ok: false, reason: "missing_headers" };
26
+ const skew = Math.abs(Date.now() / 1000 - Number(ts));
27
+ if (!Number.isFinite(skew) || skew > (i.maxSkewSec ?? 300))
28
+ return { ok: false, reason: "stale_timestamp" };
29
+ const bodyHash = crypto.createHash("sha256").update(i.rawBody).digest("hex");
30
+ const payload = `v1\n${ts}\n${rid}\n${i.method.toUpperCase()}\n${i.pathAndQuery}\n${bodyHash}`;
31
+ const secrets = Array.isArray(i.secret) ? i.secret : [i.secret];
32
+ const sigBuf = Buffer.from(sig, "utf8");
33
+ const valid = secrets.some((s) => {
34
+ const expected = Buffer.from(crypto.createHmac("sha256", s).update(payload).digest("hex"), "utf8");
35
+ return expected.length === sigBuf.length && crypto.timingSafeEqual(expected, sigBuf);
36
+ });
37
+ if (!valid)
38
+ return { ok: false, reason: "bad_signature" };
39
+ // only record the id AFTER the signature is valid, so attackers can't fill the cache
40
+ if (i.replayCache?.check(rid))
41
+ return { ok: false, reason: "replay" };
42
+ return { ok: true, requestId: rid };
43
+ }
44
+ //# sourceMappingURL=core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,aAAa,CAAC;AAgBjC,MAAM,OAAO,WAAW;IAEF,KAAK;IADjB,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,YAAoB,KAAK,GAAG,EAAE,GAAG,MAAM;qBAAnB,KAAK;IAAiB,CAAC;IAC3C,8CAA8C;IAC9C,KAAK,CAAC,EAAU;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI;YAAE,IAAI,GAAG,GAAG,GAAG;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACrE,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACpC,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAED,MAAM,UAAU,WAAW,CAAC,CAAc;IACxC,MAAM,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC;IAC5C,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;IAC9C,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACzE,IAAI,CAAC,EAAE,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAEzE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACtD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,UAAU,IAAI,GAAG,CAAC;QACxD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAElD,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,OAAO,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;IAE/F,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAChE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACxC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;QAC/B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAC1B,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC5D,MAAM,CACP,CAAC;QACF,OAAO,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACvF,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC;IAE1D,qFAAqF;IACrF,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IAEtE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AACtC,CAAC"}
@@ -0,0 +1,7 @@
1
+ /** Pass to express.json({ verify: shuntRawBody }) so the raw bytes are kept. */
2
+ export declare const shuntRawBody: (req: any, _res: any, buf: Buffer) => void;
3
+ export declare function shuntExpress(secret: string | string[], opts?: {
4
+ maxSkewSec?: number;
5
+ replayProtection?: boolean;
6
+ }): (req: any, res: any, next: any) => any;
7
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,eAAO,MAAM,YAAY,QAAS,GAAG,QAAQ,GAAG,OAAO,MAAM,SAE5D,CAAC;AAEF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EACzB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAO,SAGjD,GAAG,OAAO,GAAG,QAAQ,GAAG,SAgBtC"}
@@ -0,0 +1,25 @@
1
+ import { verifyShunt, ReplayCache } from "./core.js";
2
+ /** Pass to express.json({ verify: shuntRawBody }) so the raw bytes are kept. */
3
+ export const shuntRawBody = (req, _res, buf) => {
4
+ req.rawBody = buf;
5
+ };
6
+ export function shuntExpress(secret, opts = {}) {
7
+ const replayCache = opts.replayProtection ? new ReplayCache() : undefined;
8
+ return (req, res, next) => {
9
+ const r = verifyShunt({
10
+ secret,
11
+ method: req.method,
12
+ pathAndQuery: req.originalUrl,
13
+ rawBody: req.rawBody ?? Buffer.alloc(0),
14
+ getHeader: (n) => req.get(n),
15
+ maxSkewSec: opts.maxSkewSec,
16
+ replayCache,
17
+ });
18
+ if (!r.ok) {
19
+ return res.status(401).json({ error: "Unauthorized: requests must come via Shunt" });
20
+ }
21
+ req.shunt = { requestId: r.requestId, agent: req.get("x-shunt-agent") };
22
+ next();
23
+ };
24
+ }
25
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAErD,gFAAgF;AAChF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAQ,EAAE,IAAS,EAAE,GAAW,EAAE,EAAE;IAC/D,GAAG,CAAC,OAAO,GAAG,GAAG,CAAC;AACpB,CAAC,CAAC;AAEF,MAAM,UAAU,YAAY,CAC1B,MAAyB,EACzB,IAAI,GAAwD,EAAE;IAE9D,MAAM,WAAW,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IAC1E,OAAO,CAAC,GAAQ,EAAE,GAAQ,EAAE,IAAS,EAAE,EAAE;QACvC,MAAM,CAAC,GAAG,WAAW,CAAC;YACpB,MAAM;YACN,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,YAAY,EAAE,GAAG,CAAC,WAAW;YAC7B,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACvC,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YAC5B,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,WAAW;SACZ,CAAC,CAAC;QACH,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,4CAA4C,EAAE,CAAC,CAAC;QACvF,CAAC;QACD,GAAG,CAAC,KAAK,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;QACxE,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from "./core.js";
2
+ export * from "./express.js";
3
+ export * from "./web.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./core.js";
2
+ export * from "./express.js";
3
+ export * from "./web.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC"}
package/dist/web.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { ReplayCache } from "./core.js";
2
+ export declare function verifyShuntRequest(req: Request, secret: string | string[], opts?: {
3
+ maxSkewSec?: number;
4
+ replayCache?: ReplayCache;
5
+ }): Promise<import("./core.js").VerifyResult>;
6
+ /** Next.js App Router: export const POST = withShunt(secret, async (req) => ...) */
7
+ export declare function withShunt<T extends (req: Request, ...a: any[]) => Promise<Response> | Response>(secret: string | string[], handler: T): T;
8
+ /** Hono: app.use("/paid/*", shuntHono(secret)) */
9
+ export declare const shuntHono: (secret: string | string[]) => (c: any, next: any) => Promise<any>;
10
+ //# sourceMappingURL=web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,WAAW,EAAE,MAAM,WAAW,CAAC;AAErD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EACzB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,WAAW,CAAA;CAAO,6CAY9D;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAC7F,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EACzB,OAAO,EAAE,CAAC,GACT,CAAC,CAQH;AAED,kDAAkD;AAClD,eAAO,MAAM,SAAS,WAAY,MAAM,GAAG,MAAM,EAAE,SAAe,GAAG,QAAQ,GAAG,iBAI/E,CAAC"}
package/dist/web.js ADDED
@@ -0,0 +1,31 @@
1
+ import { verifyShunt } from "./core.js";
2
+ export async function verifyShuntRequest(req, secret, opts = {}) {
3
+ const url = new URL(req.url);
4
+ const rawBody = new Uint8Array(await req.clone().arrayBuffer());
5
+ return verifyShunt({
6
+ secret,
7
+ method: req.method,
8
+ pathAndQuery: url.pathname + url.search,
9
+ rawBody,
10
+ getHeader: (n) => req.headers.get(n),
11
+ ...opts,
12
+ });
13
+ }
14
+ /** Next.js App Router: export const POST = withShunt(secret, async (req) => ...) */
15
+ export function withShunt(secret, handler) {
16
+ return (async (req, ...args) => {
17
+ const r = await verifyShuntRequest(req, secret);
18
+ if (!r.ok) {
19
+ return Response.json({ error: "Unauthorized: requests must come via Shunt" }, { status: 401 });
20
+ }
21
+ return handler(req, ...args);
22
+ });
23
+ }
24
+ /** Hono: app.use("/paid/*", shuntHono(secret)) */
25
+ export const shuntHono = (secret) => async (c, next) => {
26
+ const r = await verifyShuntRequest(c.req.raw, secret);
27
+ if (!r.ok)
28
+ return c.json({ error: "Unauthorized: requests must come via Shunt" }, 401);
29
+ await next();
30
+ };
31
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,WAAW,CAAC;AAErD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,GAAY,EACZ,MAAyB,EACzB,IAAI,GAAuD,EAAE;IAE7D,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IAChE,OAAO,WAAW,CAAC;QACjB,MAAM;QACN,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,YAAY,EAAE,GAAG,CAAC,QAAQ,GAAG,GAAG,CAAC,MAAM;QACvC,OAAO;QACP,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QACpC,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,SAAS,CACvB,MAAyB,EACzB,OAAU;IAEV,OAAO,CAAC,KAAK,EAAE,GAAY,EAAE,GAAG,IAAW,EAAE,EAAE;QAC7C,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAChD,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;YACV,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,4CAA4C,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QACjG,CAAC;QACD,OAAO,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAM,CAAC;AACV,CAAC;AAED,kDAAkD;AAClD,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,MAAyB,EAAE,EAAE,CAAC,KAAK,EAAE,CAAM,EAAE,IAAS,EAAE,EAAE;IAClF,MAAM,CAAC,GAAG,MAAM,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACtD,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,4CAA4C,EAAE,EAAE,GAAG,CAAC,CAAC;IACvF,MAAM,IAAI,EAAE,CAAC;AACf,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,21 @@
1
1
  {
2
2
  "name": "@shunt-network/verify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "license": "ISC",
6
+ "author": "Aditya Patwa",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^26.6.5",
19
+ "typescript": "^7.0.2"
20
+ }
21
+ }