@dexterai/x402 3.9.0 → 3.10.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/README.md +6 -2
- package/dist/adapters/index.cjs +1 -1
- package/dist/adapters/index.d.cts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +1 -1
- package/dist/batch-settlement/index.cjs +1 -1
- package/dist/batch-settlement/index.js +1 -1
- package/dist/client/index.cjs +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/{constants-qU-4U3L-.d.cts → constants-D41hDAG6.d.cts} +13 -1
- package/dist/{constants-qU-4U3L-.d.ts → constants-D41hDAG6.d.ts} +13 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.js +1 -1
- package/dist/server/index.cjs +3 -3
- package/dist/server/index.d.cts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +3 -3
- package/dist/tab/adapters/solana/index.cjs +1 -0
- package/dist/tab/adapters/solana/index.d.cts +124 -0
- package/dist/tab/adapters/solana/index.d.ts +124 -0
- package/dist/tab/adapters/solana/index.js +1 -0
- package/dist/tab/index.cjs +6 -0
- package/dist/tab/index.d.cts +31 -0
- package/dist/tab/index.d.ts +31 -0
- package/dist/tab/index.js +6 -0
- package/dist/tab/seller/index.cjs +6 -0
- package/dist/tab/seller/index.d.cts +291 -0
- package/dist/tab/seller/index.d.ts +291 -0
- package/dist/tab/seller/index.js +6 -0
- package/dist/types-DIrmhiD-.d.cts +234 -0
- package/dist/types-DIrmhiD-.d.ts +234 -0
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.d.cts +10 -1
- package/dist/utils/index.d.ts +10 -1
- package/dist/utils/index.js +1 -1
- package/package.json +18 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { c as TabNetworkId, H as HumanAmount, e as SignedVoucher, A as AtomicAmount } from '../../types-DIrmhiD-.cjs';
|
|
2
|
+
import { RequestHandler, Request, Response } from 'express';
|
|
3
|
+
import { Connection, PublicKey } from '@solana/web3.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @dexterai/x402/tab/seller — types for the seller side of OTS tab streaming.
|
|
7
|
+
*
|
|
8
|
+
* The seller middleware verifies vouchers locally (microsecond latency, no
|
|
9
|
+
* chain calls) and demands a fresh session-signed voucher before delivering
|
|
10
|
+
* each chunk. Voucher accumulation is internal; the seller's mental model
|
|
11
|
+
* is "charge for what I serve."
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Persistent store for the seller's per-tab voucher state. The middleware
|
|
16
|
+
* writes the latest accepted voucher after every chunk so a process crash
|
|
17
|
+
* loses at most the last in-flight voucher's worth of revenue. Pluggable to
|
|
18
|
+
* match `batch-settlement/store`'s ChannelStore pattern.
|
|
19
|
+
*/
|
|
20
|
+
interface VoucherStore {
|
|
21
|
+
get(channelId: string): Promise<SignedVoucher | null>;
|
|
22
|
+
set(channelId: string, voucher: SignedVoucher): Promise<void>;
|
|
23
|
+
delete(channelId: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Tab handle injected by `tabMiddleware` onto the Express request. The route
|
|
27
|
+
* handler reads it to drive a metered stream.
|
|
28
|
+
*/
|
|
29
|
+
interface SellerTab {
|
|
30
|
+
readonly channelId: string;
|
|
31
|
+
readonly network: TabNetworkId;
|
|
32
|
+
/** The buyer's session pubkey for this tab (set by the first voucher). */
|
|
33
|
+
readonly sessionPublicKey: Uint8Array | null;
|
|
34
|
+
/** Cumulative human amount already accepted via vouchers. */
|
|
35
|
+
cumulative(): HumanAmount;
|
|
36
|
+
/**
|
|
37
|
+
* Accept a fresh voucher from the buyer that bumps the cumulative amount
|
|
38
|
+
* by `incrementHuman`. Throws if the voucher signature, scope, or
|
|
39
|
+
* monotonicity check fails. The middleware persists on success.
|
|
40
|
+
*/
|
|
41
|
+
charge(incrementHuman: HumanAmount): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/** Options for `tabMiddleware`. */
|
|
44
|
+
interface TabMiddlewareOptions {
|
|
45
|
+
/** Charge unit denomination (human amount per delivered unit). */
|
|
46
|
+
perUnit: HumanAmount;
|
|
47
|
+
/** Which network the seller accepts. */
|
|
48
|
+
network: TabNetworkId;
|
|
49
|
+
/** When to settle on chain: at tab close (the common case) vs periodically. */
|
|
50
|
+
settle: 'on-close' | 'periodic';
|
|
51
|
+
/** Facilitator base URL. Default: https://facilitator.dexter.cash. */
|
|
52
|
+
facilitatorUrl?: string;
|
|
53
|
+
/** Voucher persistence. Default: file-backed. */
|
|
54
|
+
store?: VoucherStore;
|
|
55
|
+
/**
|
|
56
|
+
* Hard cap on a single voucher's incremental amount. Protects the seller's
|
|
57
|
+
* middleware from accepting a buyer trying to slip in a giant single
|
|
58
|
+
* voucher. Default: 100x `perUnit`.
|
|
59
|
+
*/
|
|
60
|
+
maxPerVoucherAtomic?: AtomicAmount;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Options for `openSse` — the Express response → SSE stream helper. Returns
|
|
64
|
+
* a meter the route handler drives.
|
|
65
|
+
*/
|
|
66
|
+
interface OpenSseOptions {
|
|
67
|
+
tab: SellerTab;
|
|
68
|
+
/** Per-chunk human amount; default = the middleware's perUnit. */
|
|
69
|
+
perUnit?: HumanAmount;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Meter returned by `openSse`. The route handler calls `charge()` before
|
|
73
|
+
* delivering each chunk and `send()` to actually push the chunk; `end()`
|
|
74
|
+
* closes the SSE stream without settling.
|
|
75
|
+
*/
|
|
76
|
+
interface SseMeter {
|
|
77
|
+
charge(units?: number): Promise<void>;
|
|
78
|
+
send(chunk: string | Uint8Array): void;
|
|
79
|
+
end(): void;
|
|
80
|
+
}
|
|
81
|
+
/** Errors thrown by the seller middleware on bad vouchers. */
|
|
82
|
+
declare class InvalidVoucherError extends Error {
|
|
83
|
+
readonly reason: 'signature_invalid' | 'registration_invalid' | 'cap_exceeded' | 'session_expired' | 'wrong_counterparty' | 'non_monotonic';
|
|
84
|
+
constructor(reason: 'signature_invalid' | 'registration_invalid' | 'cap_exceeded' | 'session_expired' | 'wrong_counterparty' | 'non_monotonic', detail?: string);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Express middleware for accepting OTS tab vouchers.
|
|
89
|
+
*
|
|
90
|
+
* Wire shape:
|
|
91
|
+
* - Buyer sends each paid request with header `X-Tab-Voucher: <base64-json>`
|
|
92
|
+
* - The voucher JSON is the SignedVoucher shape (payload + session pubkey +
|
|
93
|
+
* registration + signature)
|
|
94
|
+
* - On the FIRST voucher of a session, the middleware parses the
|
|
95
|
+
* registration, verifies it against the on-chain vault (one RPC call),
|
|
96
|
+
* and caches the result
|
|
97
|
+
* - On EVERY voucher, the middleware verifies the session-key signature
|
|
98
|
+
* and enforces scope (cap, expiry, counterparty, monotonicity)
|
|
99
|
+
* - The route handler reads `req.tab` and either runs a stream against it
|
|
100
|
+
* or rejects with 402 Payment Required
|
|
101
|
+
*
|
|
102
|
+
* The middleware never blocks on chain in the per-voucher hot path. The
|
|
103
|
+
* one-time on-chain read is amortized across the entire session.
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
declare module 'express-serve-static-core' {
|
|
107
|
+
interface Request {
|
|
108
|
+
tab?: SellerTab;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
interface TabMiddlewareConfig extends TabMiddlewareOptions {
|
|
112
|
+
/** RPC connection used for the one-time on-chain registration read. */
|
|
113
|
+
connection: Connection;
|
|
114
|
+
/** The seller's pubkey — used as allowed_counterparty for scope check. */
|
|
115
|
+
sellerPubkey: string | PublicKey;
|
|
116
|
+
}
|
|
117
|
+
/** Header the buyer sends with each paid request. base64-encoded JSON of SignedVoucher. */
|
|
118
|
+
declare const TAB_VOUCHER_HEADER = "x-tab-voucher";
|
|
119
|
+
declare function tabMiddleware(config: TabMiddlewareConfig): RequestHandler;
|
|
120
|
+
/** Pull the SellerTab off a request. Throws if the middleware didn't run. */
|
|
121
|
+
declare function requireTab(req: Request): SellerTab;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* SSE meter: turn an Express response into a Server-Sent-Events stream
|
|
125
|
+
* tied to a tab. The route handler calls `charge()` before each chunk
|
|
126
|
+
* (which demands a fresh voucher from the buyer) and `send()` to push
|
|
127
|
+
* the chunk down the wire.
|
|
128
|
+
*
|
|
129
|
+
* The streaming pattern this enables:
|
|
130
|
+
*
|
|
131
|
+
* app.post('/inference', tabMiddleware({...}), async (req, res) => {
|
|
132
|
+
* const tab = requireTab(req);
|
|
133
|
+
* const meter = openSse(res, { tab, perUnit: '0.00003' });
|
|
134
|
+
* for await (const token of llm(req.body.prompt)) {
|
|
135
|
+
* await meter.charge(); // demand voucher; throws if cap exceeded
|
|
136
|
+
* meter.send(token); // emit SSE event with the token
|
|
137
|
+
* }
|
|
138
|
+
* meter.end();
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* NOTE on voucher cadence: this implementation treats EACH `charge()` as
|
|
142
|
+
* "the buyer already presented a voucher covering this chunk via the
|
|
143
|
+
* inbound request header" — i.e. the request's voucher header bounds the
|
|
144
|
+
* cumulative the seller can deliver under. For true per-chunk voucher
|
|
145
|
+
* exchange mid-stream (the buyer presenting fresh vouchers WITHIN one
|
|
146
|
+
* HTTP request), the seller needs to read vouchers off the response
|
|
147
|
+
* stream's reverse direction or via WebSocket. That's an advanced mode
|
|
148
|
+
* left for Phase 4+; the v3 meter ships the simpler "one voucher bounds
|
|
149
|
+
* the whole request" model, which is correct for any reasonable chunk
|
|
150
|
+
* count under a single per-request increment.
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
declare function openSse(res: Response, options: OpenSseOptions): SseMeter;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Voucher persistence for the seller side.
|
|
157
|
+
*
|
|
158
|
+
* The seller's middleware accepts a fresh voucher per chunk and overwrites
|
|
159
|
+
* the previous one — only the latest cumulative voucher matters for
|
|
160
|
+
* settlement. Persistence exists so a process crash mid-stream doesn't
|
|
161
|
+
* lose the last few seconds of accrued revenue.
|
|
162
|
+
*
|
|
163
|
+
* Two implementations:
|
|
164
|
+
* - InMemoryVoucherStore — zero-config default. Loses state on restart.
|
|
165
|
+
* - FileVoucherStore — writes one JSON file per channel id. Survives
|
|
166
|
+
* restarts; cheap enough for low-concurrency sellers.
|
|
167
|
+
*
|
|
168
|
+
* Production sellers expecting high concurrency or atomic restart-recovery
|
|
169
|
+
* can implement VoucherStore themselves (Redis, Postgres, etc) and pass it
|
|
170
|
+
* into tabMiddleware. The interface is intentionally minimal.
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
declare class InMemoryVoucherStore implements VoucherStore {
|
|
174
|
+
private map;
|
|
175
|
+
get(channelId: string): Promise<SignedVoucher | null>;
|
|
176
|
+
set(channelId: string, voucher: SignedVoucher): Promise<void>;
|
|
177
|
+
delete(channelId: string): Promise<void>;
|
|
178
|
+
}
|
|
179
|
+
declare class FileVoucherStore implements VoucherStore {
|
|
180
|
+
private readonly dir;
|
|
181
|
+
constructor(dir: string);
|
|
182
|
+
private pathFor;
|
|
183
|
+
get(channelId: string): Promise<SignedVoucher | null>;
|
|
184
|
+
set(channelId: string, voucher: SignedVoucher): Promise<void>;
|
|
185
|
+
delete(channelId: string): Promise<void>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Local voucher verification for the seller side of OTS tab streaming.
|
|
190
|
+
*
|
|
191
|
+
* Two-layer verification:
|
|
192
|
+
*
|
|
193
|
+
* 1. parseRegistration(registrationBytes)
|
|
194
|
+
* Parses the 180-byte registration message into a scope. Synchronous,
|
|
195
|
+
* no I/O. This gives the seller everything they need to enforce limits
|
|
196
|
+
* LOCALLY (cap, expiry, counterparty) and to know which vault to read
|
|
197
|
+
* for passkey verification.
|
|
198
|
+
*
|
|
199
|
+
* 2. verifyRegistrationOnChain(connection, registration, programId)
|
|
200
|
+
* Reads the vault account on chain ONCE per session and verifies that
|
|
201
|
+
* the buyer's passkey would have produced the registration's signature.
|
|
202
|
+
* Cached after the first call; subsequent vouchers in the same session
|
|
203
|
+
* reuse the cached result.
|
|
204
|
+
*
|
|
205
|
+
* 3. verifyVoucherSignature(voucher, sessionPublicKey, channelIdBytes)
|
|
206
|
+
* Verifies the session-key signature over the 44-byte voucher payload.
|
|
207
|
+
* Synchronous, no I/O, microsecond latency. This is what runs PER
|
|
208
|
+
* CHUNK during streaming.
|
|
209
|
+
*
|
|
210
|
+
* The seller's per-chunk hot path is (3) only. (1) and (2) run once per
|
|
211
|
+
* session.
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
interface ParsedRegistration {
|
|
215
|
+
programId: PublicKey;
|
|
216
|
+
vaultPda: PublicKey;
|
|
217
|
+
sessionPubkey: Uint8Array;
|
|
218
|
+
maxAmount: bigint;
|
|
219
|
+
expiresAt: bigint;
|
|
220
|
+
allowedCounterparty: PublicKey;
|
|
221
|
+
nonce: number;
|
|
222
|
+
}
|
|
223
|
+
declare class InvalidRegistrationError extends Error {
|
|
224
|
+
readonly reason: 'wrong_length' | 'wrong_domain' | 'wrong_program' | 'expiry_in_past' | 'cap_zero';
|
|
225
|
+
constructor(reason: 'wrong_length' | 'wrong_domain' | 'wrong_program' | 'expiry_in_past' | 'cap_zero', detail?: string);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Parse the raw registration bytes the buyer presents with their first
|
|
229
|
+
* voucher. Synchronous, pure. Validates structural correctness only — the
|
|
230
|
+
* passkey signature check is a separate on-chain step.
|
|
231
|
+
*/
|
|
232
|
+
declare function parseRegistration(registration: Uint8Array): ParsedRegistration;
|
|
233
|
+
interface OnChainVaultState {
|
|
234
|
+
passkeyPubkey: Uint8Array;
|
|
235
|
+
activeSessionPubkey: Uint8Array | null;
|
|
236
|
+
}
|
|
237
|
+
declare class OnChainVerificationError extends Error {
|
|
238
|
+
readonly reason: 'vault_not_found' | 'session_not_active' | 'session_pubkey_mismatch' | 'wrong_program';
|
|
239
|
+
constructor(reason: 'vault_not_found' | 'session_not_active' | 'session_pubkey_mismatch' | 'wrong_program', detail?: string);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Read the vault account and extract passkey + active session.
|
|
243
|
+
*
|
|
244
|
+
* Reads at `finalized` commitment to avoid the read-replica race that
|
|
245
|
+
* shows up when the buyer just confirmed register_session_key at
|
|
246
|
+
* `confirmed` and the seller's RPC replica hasn't propagated the write
|
|
247
|
+
* yet. This is the same lesson as the dexter-vault test suite — see
|
|
248
|
+
* reference_anchor_test_commitment in repo memory.
|
|
249
|
+
*/
|
|
250
|
+
declare function readVaultState(connection: Connection, vaultPda: PublicKey): Promise<OnChainVaultState>;
|
|
251
|
+
/**
|
|
252
|
+
* Verify a registration against on-chain state. Returns the vault's
|
|
253
|
+
* passkey pubkey (caller can cache it). Throws on any mismatch.
|
|
254
|
+
*
|
|
255
|
+
* The "verification" here is structural: the active_session on chain MUST
|
|
256
|
+
* carry the same session pubkey the registration claims. If the program
|
|
257
|
+
* accepted the register_session_key tx (which is what set active_session
|
|
258
|
+
* in the first place), then the passkey signature was verified by the
|
|
259
|
+
* secp256r1 precompile inside that tx. The seller doesn't need to redo
|
|
260
|
+
* that work; they just need to confirm the on-chain witness still holds.
|
|
261
|
+
*/
|
|
262
|
+
declare function verifyRegistrationOnChain(connection: Connection, registration: ParsedRegistration): Promise<{
|
|
263
|
+
passkeyPubkey: Uint8Array;
|
|
264
|
+
}>;
|
|
265
|
+
declare class InvalidVoucherSignatureError extends Error {
|
|
266
|
+
constructor(detail?: string);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Verify the session-key signature on a voucher. This is the hot-path
|
|
270
|
+
* check, called on every chunk during streaming. Pure ed25519
|
|
271
|
+
* verification, microsecond latency.
|
|
272
|
+
*
|
|
273
|
+
* The channelIdBytes must be the canonical 32-byte channel id the buyer
|
|
274
|
+
* derived (typically sha256(vault_pda || seller_url || nonce)). The
|
|
275
|
+
* caller is responsible for either deriving it the same way or accepting
|
|
276
|
+
* whatever the buyer presents on the first voucher (treating it as the
|
|
277
|
+
* channel handle for the session).
|
|
278
|
+
*/
|
|
279
|
+
declare function verifyVoucherSignature(voucher: SignedVoucher, channelIdBytes: Uint8Array): void;
|
|
280
|
+
declare class ScopeViolationError extends Error {
|
|
281
|
+
readonly reason: 'cumulative_exceeds_cap' | 'session_expired' | 'wrong_counterparty' | 'non_monotonic';
|
|
282
|
+
constructor(reason: 'cumulative_exceeds_cap' | 'session_expired' | 'wrong_counterparty' | 'non_monotonic', detail?: string);
|
|
283
|
+
}
|
|
284
|
+
declare function enforceScope(args: {
|
|
285
|
+
registration: ParsedRegistration;
|
|
286
|
+
voucher: SignedVoucher;
|
|
287
|
+
expectedCounterparty: PublicKey;
|
|
288
|
+
previousCumulativeAtomic?: AtomicAmount;
|
|
289
|
+
}): void;
|
|
290
|
+
|
|
291
|
+
export { FileVoucherStore, InMemoryVoucherStore, InvalidRegistrationError, InvalidVoucherError, InvalidVoucherSignatureError, type OnChainVaultState, OnChainVerificationError, type OpenSseOptions, type ParsedRegistration, ScopeViolationError, type SellerTab, type SseMeter, TAB_VOUCHER_HEADER, type TabMiddlewareConfig, type TabMiddlewareOptions, type VoucherStore, enforceScope, openSse, parseRegistration, readVaultState, requireTab, tabMiddleware, verifyRegistrationOnChain, verifyVoucherSignature };
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { c as TabNetworkId, H as HumanAmount, e as SignedVoucher, A as AtomicAmount } from '../../types-DIrmhiD-.js';
|
|
2
|
+
import { RequestHandler, Request, Response } from 'express';
|
|
3
|
+
import { Connection, PublicKey } from '@solana/web3.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @dexterai/x402/tab/seller — types for the seller side of OTS tab streaming.
|
|
7
|
+
*
|
|
8
|
+
* The seller middleware verifies vouchers locally (microsecond latency, no
|
|
9
|
+
* chain calls) and demands a fresh session-signed voucher before delivering
|
|
10
|
+
* each chunk. Voucher accumulation is internal; the seller's mental model
|
|
11
|
+
* is "charge for what I serve."
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Persistent store for the seller's per-tab voucher state. The middleware
|
|
16
|
+
* writes the latest accepted voucher after every chunk so a process crash
|
|
17
|
+
* loses at most the last in-flight voucher's worth of revenue. Pluggable to
|
|
18
|
+
* match `batch-settlement/store`'s ChannelStore pattern.
|
|
19
|
+
*/
|
|
20
|
+
interface VoucherStore {
|
|
21
|
+
get(channelId: string): Promise<SignedVoucher | null>;
|
|
22
|
+
set(channelId: string, voucher: SignedVoucher): Promise<void>;
|
|
23
|
+
delete(channelId: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Tab handle injected by `tabMiddleware` onto the Express request. The route
|
|
27
|
+
* handler reads it to drive a metered stream.
|
|
28
|
+
*/
|
|
29
|
+
interface SellerTab {
|
|
30
|
+
readonly channelId: string;
|
|
31
|
+
readonly network: TabNetworkId;
|
|
32
|
+
/** The buyer's session pubkey for this tab (set by the first voucher). */
|
|
33
|
+
readonly sessionPublicKey: Uint8Array | null;
|
|
34
|
+
/** Cumulative human amount already accepted via vouchers. */
|
|
35
|
+
cumulative(): HumanAmount;
|
|
36
|
+
/**
|
|
37
|
+
* Accept a fresh voucher from the buyer that bumps the cumulative amount
|
|
38
|
+
* by `incrementHuman`. Throws if the voucher signature, scope, or
|
|
39
|
+
* monotonicity check fails. The middleware persists on success.
|
|
40
|
+
*/
|
|
41
|
+
charge(incrementHuman: HumanAmount): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/** Options for `tabMiddleware`. */
|
|
44
|
+
interface TabMiddlewareOptions {
|
|
45
|
+
/** Charge unit denomination (human amount per delivered unit). */
|
|
46
|
+
perUnit: HumanAmount;
|
|
47
|
+
/** Which network the seller accepts. */
|
|
48
|
+
network: TabNetworkId;
|
|
49
|
+
/** When to settle on chain: at tab close (the common case) vs periodically. */
|
|
50
|
+
settle: 'on-close' | 'periodic';
|
|
51
|
+
/** Facilitator base URL. Default: https://facilitator.dexter.cash. */
|
|
52
|
+
facilitatorUrl?: string;
|
|
53
|
+
/** Voucher persistence. Default: file-backed. */
|
|
54
|
+
store?: VoucherStore;
|
|
55
|
+
/**
|
|
56
|
+
* Hard cap on a single voucher's incremental amount. Protects the seller's
|
|
57
|
+
* middleware from accepting a buyer trying to slip in a giant single
|
|
58
|
+
* voucher. Default: 100x `perUnit`.
|
|
59
|
+
*/
|
|
60
|
+
maxPerVoucherAtomic?: AtomicAmount;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Options for `openSse` — the Express response → SSE stream helper. Returns
|
|
64
|
+
* a meter the route handler drives.
|
|
65
|
+
*/
|
|
66
|
+
interface OpenSseOptions {
|
|
67
|
+
tab: SellerTab;
|
|
68
|
+
/** Per-chunk human amount; default = the middleware's perUnit. */
|
|
69
|
+
perUnit?: HumanAmount;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Meter returned by `openSse`. The route handler calls `charge()` before
|
|
73
|
+
* delivering each chunk and `send()` to actually push the chunk; `end()`
|
|
74
|
+
* closes the SSE stream without settling.
|
|
75
|
+
*/
|
|
76
|
+
interface SseMeter {
|
|
77
|
+
charge(units?: number): Promise<void>;
|
|
78
|
+
send(chunk: string | Uint8Array): void;
|
|
79
|
+
end(): void;
|
|
80
|
+
}
|
|
81
|
+
/** Errors thrown by the seller middleware on bad vouchers. */
|
|
82
|
+
declare class InvalidVoucherError extends Error {
|
|
83
|
+
readonly reason: 'signature_invalid' | 'registration_invalid' | 'cap_exceeded' | 'session_expired' | 'wrong_counterparty' | 'non_monotonic';
|
|
84
|
+
constructor(reason: 'signature_invalid' | 'registration_invalid' | 'cap_exceeded' | 'session_expired' | 'wrong_counterparty' | 'non_monotonic', detail?: string);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Express middleware for accepting OTS tab vouchers.
|
|
89
|
+
*
|
|
90
|
+
* Wire shape:
|
|
91
|
+
* - Buyer sends each paid request with header `X-Tab-Voucher: <base64-json>`
|
|
92
|
+
* - The voucher JSON is the SignedVoucher shape (payload + session pubkey +
|
|
93
|
+
* registration + signature)
|
|
94
|
+
* - On the FIRST voucher of a session, the middleware parses the
|
|
95
|
+
* registration, verifies it against the on-chain vault (one RPC call),
|
|
96
|
+
* and caches the result
|
|
97
|
+
* - On EVERY voucher, the middleware verifies the session-key signature
|
|
98
|
+
* and enforces scope (cap, expiry, counterparty, monotonicity)
|
|
99
|
+
* - The route handler reads `req.tab` and either runs a stream against it
|
|
100
|
+
* or rejects with 402 Payment Required
|
|
101
|
+
*
|
|
102
|
+
* The middleware never blocks on chain in the per-voucher hot path. The
|
|
103
|
+
* one-time on-chain read is amortized across the entire session.
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
declare module 'express-serve-static-core' {
|
|
107
|
+
interface Request {
|
|
108
|
+
tab?: SellerTab;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
interface TabMiddlewareConfig extends TabMiddlewareOptions {
|
|
112
|
+
/** RPC connection used for the one-time on-chain registration read. */
|
|
113
|
+
connection: Connection;
|
|
114
|
+
/** The seller's pubkey — used as allowed_counterparty for scope check. */
|
|
115
|
+
sellerPubkey: string | PublicKey;
|
|
116
|
+
}
|
|
117
|
+
/** Header the buyer sends with each paid request. base64-encoded JSON of SignedVoucher. */
|
|
118
|
+
declare const TAB_VOUCHER_HEADER = "x-tab-voucher";
|
|
119
|
+
declare function tabMiddleware(config: TabMiddlewareConfig): RequestHandler;
|
|
120
|
+
/** Pull the SellerTab off a request. Throws if the middleware didn't run. */
|
|
121
|
+
declare function requireTab(req: Request): SellerTab;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* SSE meter: turn an Express response into a Server-Sent-Events stream
|
|
125
|
+
* tied to a tab. The route handler calls `charge()` before each chunk
|
|
126
|
+
* (which demands a fresh voucher from the buyer) and `send()` to push
|
|
127
|
+
* the chunk down the wire.
|
|
128
|
+
*
|
|
129
|
+
* The streaming pattern this enables:
|
|
130
|
+
*
|
|
131
|
+
* app.post('/inference', tabMiddleware({...}), async (req, res) => {
|
|
132
|
+
* const tab = requireTab(req);
|
|
133
|
+
* const meter = openSse(res, { tab, perUnit: '0.00003' });
|
|
134
|
+
* for await (const token of llm(req.body.prompt)) {
|
|
135
|
+
* await meter.charge(); // demand voucher; throws if cap exceeded
|
|
136
|
+
* meter.send(token); // emit SSE event with the token
|
|
137
|
+
* }
|
|
138
|
+
* meter.end();
|
|
139
|
+
* });
|
|
140
|
+
*
|
|
141
|
+
* NOTE on voucher cadence: this implementation treats EACH `charge()` as
|
|
142
|
+
* "the buyer already presented a voucher covering this chunk via the
|
|
143
|
+
* inbound request header" — i.e. the request's voucher header bounds the
|
|
144
|
+
* cumulative the seller can deliver under. For true per-chunk voucher
|
|
145
|
+
* exchange mid-stream (the buyer presenting fresh vouchers WITHIN one
|
|
146
|
+
* HTTP request), the seller needs to read vouchers off the response
|
|
147
|
+
* stream's reverse direction or via WebSocket. That's an advanced mode
|
|
148
|
+
* left for Phase 4+; the v3 meter ships the simpler "one voucher bounds
|
|
149
|
+
* the whole request" model, which is correct for any reasonable chunk
|
|
150
|
+
* count under a single per-request increment.
|
|
151
|
+
*/
|
|
152
|
+
|
|
153
|
+
declare function openSse(res: Response, options: OpenSseOptions): SseMeter;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Voucher persistence for the seller side.
|
|
157
|
+
*
|
|
158
|
+
* The seller's middleware accepts a fresh voucher per chunk and overwrites
|
|
159
|
+
* the previous one — only the latest cumulative voucher matters for
|
|
160
|
+
* settlement. Persistence exists so a process crash mid-stream doesn't
|
|
161
|
+
* lose the last few seconds of accrued revenue.
|
|
162
|
+
*
|
|
163
|
+
* Two implementations:
|
|
164
|
+
* - InMemoryVoucherStore — zero-config default. Loses state on restart.
|
|
165
|
+
* - FileVoucherStore — writes one JSON file per channel id. Survives
|
|
166
|
+
* restarts; cheap enough for low-concurrency sellers.
|
|
167
|
+
*
|
|
168
|
+
* Production sellers expecting high concurrency or atomic restart-recovery
|
|
169
|
+
* can implement VoucherStore themselves (Redis, Postgres, etc) and pass it
|
|
170
|
+
* into tabMiddleware. The interface is intentionally minimal.
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
declare class InMemoryVoucherStore implements VoucherStore {
|
|
174
|
+
private map;
|
|
175
|
+
get(channelId: string): Promise<SignedVoucher | null>;
|
|
176
|
+
set(channelId: string, voucher: SignedVoucher): Promise<void>;
|
|
177
|
+
delete(channelId: string): Promise<void>;
|
|
178
|
+
}
|
|
179
|
+
declare class FileVoucherStore implements VoucherStore {
|
|
180
|
+
private readonly dir;
|
|
181
|
+
constructor(dir: string);
|
|
182
|
+
private pathFor;
|
|
183
|
+
get(channelId: string): Promise<SignedVoucher | null>;
|
|
184
|
+
set(channelId: string, voucher: SignedVoucher): Promise<void>;
|
|
185
|
+
delete(channelId: string): Promise<void>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Local voucher verification for the seller side of OTS tab streaming.
|
|
190
|
+
*
|
|
191
|
+
* Two-layer verification:
|
|
192
|
+
*
|
|
193
|
+
* 1. parseRegistration(registrationBytes)
|
|
194
|
+
* Parses the 180-byte registration message into a scope. Synchronous,
|
|
195
|
+
* no I/O. This gives the seller everything they need to enforce limits
|
|
196
|
+
* LOCALLY (cap, expiry, counterparty) and to know which vault to read
|
|
197
|
+
* for passkey verification.
|
|
198
|
+
*
|
|
199
|
+
* 2. verifyRegistrationOnChain(connection, registration, programId)
|
|
200
|
+
* Reads the vault account on chain ONCE per session and verifies that
|
|
201
|
+
* the buyer's passkey would have produced the registration's signature.
|
|
202
|
+
* Cached after the first call; subsequent vouchers in the same session
|
|
203
|
+
* reuse the cached result.
|
|
204
|
+
*
|
|
205
|
+
* 3. verifyVoucherSignature(voucher, sessionPublicKey, channelIdBytes)
|
|
206
|
+
* Verifies the session-key signature over the 44-byte voucher payload.
|
|
207
|
+
* Synchronous, no I/O, microsecond latency. This is what runs PER
|
|
208
|
+
* CHUNK during streaming.
|
|
209
|
+
*
|
|
210
|
+
* The seller's per-chunk hot path is (3) only. (1) and (2) run once per
|
|
211
|
+
* session.
|
|
212
|
+
*/
|
|
213
|
+
|
|
214
|
+
interface ParsedRegistration {
|
|
215
|
+
programId: PublicKey;
|
|
216
|
+
vaultPda: PublicKey;
|
|
217
|
+
sessionPubkey: Uint8Array;
|
|
218
|
+
maxAmount: bigint;
|
|
219
|
+
expiresAt: bigint;
|
|
220
|
+
allowedCounterparty: PublicKey;
|
|
221
|
+
nonce: number;
|
|
222
|
+
}
|
|
223
|
+
declare class InvalidRegistrationError extends Error {
|
|
224
|
+
readonly reason: 'wrong_length' | 'wrong_domain' | 'wrong_program' | 'expiry_in_past' | 'cap_zero';
|
|
225
|
+
constructor(reason: 'wrong_length' | 'wrong_domain' | 'wrong_program' | 'expiry_in_past' | 'cap_zero', detail?: string);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Parse the raw registration bytes the buyer presents with their first
|
|
229
|
+
* voucher. Synchronous, pure. Validates structural correctness only — the
|
|
230
|
+
* passkey signature check is a separate on-chain step.
|
|
231
|
+
*/
|
|
232
|
+
declare function parseRegistration(registration: Uint8Array): ParsedRegistration;
|
|
233
|
+
interface OnChainVaultState {
|
|
234
|
+
passkeyPubkey: Uint8Array;
|
|
235
|
+
activeSessionPubkey: Uint8Array | null;
|
|
236
|
+
}
|
|
237
|
+
declare class OnChainVerificationError extends Error {
|
|
238
|
+
readonly reason: 'vault_not_found' | 'session_not_active' | 'session_pubkey_mismatch' | 'wrong_program';
|
|
239
|
+
constructor(reason: 'vault_not_found' | 'session_not_active' | 'session_pubkey_mismatch' | 'wrong_program', detail?: string);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Read the vault account and extract passkey + active session.
|
|
243
|
+
*
|
|
244
|
+
* Reads at `finalized` commitment to avoid the read-replica race that
|
|
245
|
+
* shows up when the buyer just confirmed register_session_key at
|
|
246
|
+
* `confirmed` and the seller's RPC replica hasn't propagated the write
|
|
247
|
+
* yet. This is the same lesson as the dexter-vault test suite — see
|
|
248
|
+
* reference_anchor_test_commitment in repo memory.
|
|
249
|
+
*/
|
|
250
|
+
declare function readVaultState(connection: Connection, vaultPda: PublicKey): Promise<OnChainVaultState>;
|
|
251
|
+
/**
|
|
252
|
+
* Verify a registration against on-chain state. Returns the vault's
|
|
253
|
+
* passkey pubkey (caller can cache it). Throws on any mismatch.
|
|
254
|
+
*
|
|
255
|
+
* The "verification" here is structural: the active_session on chain MUST
|
|
256
|
+
* carry the same session pubkey the registration claims. If the program
|
|
257
|
+
* accepted the register_session_key tx (which is what set active_session
|
|
258
|
+
* in the first place), then the passkey signature was verified by the
|
|
259
|
+
* secp256r1 precompile inside that tx. The seller doesn't need to redo
|
|
260
|
+
* that work; they just need to confirm the on-chain witness still holds.
|
|
261
|
+
*/
|
|
262
|
+
declare function verifyRegistrationOnChain(connection: Connection, registration: ParsedRegistration): Promise<{
|
|
263
|
+
passkeyPubkey: Uint8Array;
|
|
264
|
+
}>;
|
|
265
|
+
declare class InvalidVoucherSignatureError extends Error {
|
|
266
|
+
constructor(detail?: string);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Verify the session-key signature on a voucher. This is the hot-path
|
|
270
|
+
* check, called on every chunk during streaming. Pure ed25519
|
|
271
|
+
* verification, microsecond latency.
|
|
272
|
+
*
|
|
273
|
+
* The channelIdBytes must be the canonical 32-byte channel id the buyer
|
|
274
|
+
* derived (typically sha256(vault_pda || seller_url || nonce)). The
|
|
275
|
+
* caller is responsible for either deriving it the same way or accepting
|
|
276
|
+
* whatever the buyer presents on the first voucher (treating it as the
|
|
277
|
+
* channel handle for the session).
|
|
278
|
+
*/
|
|
279
|
+
declare function verifyVoucherSignature(voucher: SignedVoucher, channelIdBytes: Uint8Array): void;
|
|
280
|
+
declare class ScopeViolationError extends Error {
|
|
281
|
+
readonly reason: 'cumulative_exceeds_cap' | 'session_expired' | 'wrong_counterparty' | 'non_monotonic';
|
|
282
|
+
constructor(reason: 'cumulative_exceeds_cap' | 'session_expired' | 'wrong_counterparty' | 'non_monotonic', detail?: string);
|
|
283
|
+
}
|
|
284
|
+
declare function enforceScope(args: {
|
|
285
|
+
registration: ParsedRegistration;
|
|
286
|
+
voucher: SignedVoucher;
|
|
287
|
+
expectedCounterparty: PublicKey;
|
|
288
|
+
previousCumulativeAtomic?: AtomicAmount;
|
|
289
|
+
}): void;
|
|
290
|
+
|
|
291
|
+
export { FileVoucherStore, InMemoryVoucherStore, InvalidRegistrationError, InvalidVoucherError, InvalidVoucherSignatureError, type OnChainVaultState, OnChainVerificationError, type OpenSseOptions, type ParsedRegistration, ScopeViolationError, type SellerTab, type SseMeter, TAB_VOUCHER_HEADER, type TabMiddlewareConfig, type TabMiddlewareOptions, type VoucherStore, enforceScope, openSse, parseRegistration, readVaultState, requireTab, tabMiddleware, verifyRegistrationOnChain, verifyVoucherSignature };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
var d=class extends Error{constructor(n,r){super(`Invalid voucher: ${n}${r?` (${r})`:""}`);this.reason=n;this.name="InvalidVoucherError"}};import{PublicKey as X}from"@solana/web3.js";import J from"tweetnacl";import{sha256 as fe}from"@noble/hashes/sha256";import{p256 as be}from"@noble/curves/p256";import{PublicKey as E}from"@solana/web3.js";var oe=(()=>{let e=new Uint8Array(32);return e.set(new TextEncoder().encode("OTS_SESSION_REGISTER_V1"),0),e})(),se=(()=>{let e=new Uint8Array(32);return e.set(new TextEncoder().encode("OTS_SESSION_REVOKE_V1"),0),e})();function U(e){if(e.channelId.length!==32)throw new Error(`channelId must be 32 bytes, got ${e.channelId.length}`);let t=new Uint8Array(44),n=new DataView(t.buffer),r=0;if(t.set(e.channelId,r),r+=32,n.setBigUint64(r,e.cumulativeAmount,!0),r+=8,n.setUint32(r,e.sequenceNumber>>>0,!0),r+=4,r!==44)throw new Error(`internal: voucher payload wrong length ${r}, expected 44`);return t}import{PublicKey as I,TransactionInstruction as ue}from"@solana/web3.js";var v=new I("Hg3wRaydFtJhYrdvYrKECacpJYDsC9Px7yKmpncj2fhc"),le=new I("Secp256r1SigVerify1111111111111111111111111"),de=new I("Sysvar1nstructions1111111111111111111111111"),me=new Uint8Array([69,94,60,44,49,199,183,233]),pe=new Uint8Array([81,192,32,110,104,116,144,151]);var T="OTS_SESSION_REGISTER_V1",m=class extends Error{constructor(n,r){super(`Invalid registration: ${n}${r?` (${r})`:""}`);this.reason=n;this.name="InvalidRegistrationError"}};function k(e){if(e.length!==180)throw new m("wrong_length",`expected 180, got ${e.length}`);let t=new TextDecoder().decode(e.slice(0,T.length));if(t!==T)throw new m("wrong_domain",`got "${t}"`);for(let s=T.length;s<32;s++)if(e[s]!==0)throw new m("wrong_domain",`non-NUL padding at byte ${s}`);let n=new DataView(e.buffer,e.byteOffset,e.byteLength),r=new E(e.slice(32,64)),o=new E(e.slice(64,96)),c=e.slice(96,128),p=n.getBigUint64(128,!0),g=n.getBigInt64(136,!0),i=new E(e.slice(144,176)),a=n.getUint32(176,!0);if(!r.equals(v))throw new m("wrong_program",`${r.toBase58()} is not ${v.toBase58()}`);if(p===0n)throw new m("cap_zero");let l=BigInt(Math.floor(Date.now()/1e3));if(g<=l)throw new m("expiry_in_past",`expires_at=${g}, now=${l}`);return{programId:r,vaultPda:o,sessionPubkey:new Uint8Array(c),maxAmount:p,expiresAt:g,allowedCounterparty:i,nonce:a}}var M=10,f=class extends Error{constructor(n,r){super(`On-chain verification failed: ${n}${r?` (${r})`:""}`);this.reason=n;this.name="OnChainVerificationError"}};async function L(e,t){let n=await e.getAccountInfo(t,"finalized");if(!n)throw new f("vault_not_found",t.toBase58());if(!n.owner.equals(v))throw new f("wrong_program",`owner ${n.owner.toBase58()} is not the vault program`);let r=n.data,o=new Uint8Array(r.slice(M,M+33)),a=84+(r[83]===1?48:0)+32+32;if(r[a]!==1)return{passkeyPubkey:o,activeSessionPubkey:null};let s=a+1,y=new Uint8Array(r.slice(s,s+32));return{passkeyPubkey:o,activeSessionPubkey:y}}async function _(e,t){let n=await L(e,t.vaultPda);if(n.activeSessionPubkey===null)throw new f("session_not_active","vault has no active_session \u2014 was it revoked?");if(!j(n.activeSessionPubkey,t.sessionPubkey))throw new f("session_pubkey_mismatch",`on-chain ${q(n.activeSessionPubkey)} != registration ${q(t.sessionPubkey)}`);return{passkeyPubkey:n.passkeyPubkey}}var h=class extends Error{constructor(t){super(`Invalid voucher signature${t?`: ${t}`:""}`),this.name="InvalidVoucherSignatureError"}};function R(e,t){if(t.length!==32)throw new h(`channelIdBytes must be 32 bytes, got ${t.length}`);if(e.sessionPublicKey.length!==32)throw new h(`sessionPublicKey must be 32 bytes, got ${e.sessionPublicKey.length}`);if(e.sessionSignature.length!==64)throw new h(`sessionSignature must be 64 bytes, got ${e.sessionSignature.length}`);let n=U({channelId:t,cumulativeAmount:BigInt(e.payload.cumulativeAmount),sequenceNumber:e.payload.sequenceNumber});if(!J.sign.detached.verify(n,e.sessionSignature,e.sessionPublicKey))throw new h("ed25519 verify rejected")}var u=class extends Error{constructor(n,r){super(`Scope violation: ${n}${r?` (${r})`:""}`);this.reason=n;this.name="ScopeViolationError"}};function V(e){let t=BigInt(e.voucher.payload.cumulativeAmount);if(t>e.registration.maxAmount)throw new u("cumulative_exceeds_cap",`${t} > ${e.registration.maxAmount}`);let n=BigInt(Math.floor(Date.now()/1e3));if(n>=e.registration.expiresAt)throw new u("session_expired",`now=${n} >= expiresAt=${e.registration.expiresAt}`);if(!e.registration.allowedCounterparty.equals(e.expectedCounterparty))throw new u("wrong_counterparty",`${e.registration.allowedCounterparty.toBase58()} != ${e.expectedCounterparty.toBase58()}`);if(e.previousCumulativeAtomic!==void 0){let r=BigInt(e.previousCumulativeAtomic);if(t<=r)throw new u("non_monotonic",`cumulative=${t} not > previous=${r}`)}}function j(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function q(e){let t="";for(let n of e)t+=n.toString(16).padStart(2,"0");return t}import{promises as w}from"fs";import{join as G,dirname as Y}from"path";function W(e){return{payload:e.payload,sessionPublicKey:$(e.sessionPublicKey),sessionRegistration:$(e.sessionRegistration),sessionSignature:$(e.sessionSignature)}}function Z(e){return{payload:e.payload,sessionPublicKey:C(e.sessionPublicKey),sessionRegistration:C(e.sessionRegistration),sessionSignature:C(e.sessionSignature)}}function $(e){let t="";for(let n of e)t+=n.toString(16).padStart(2,"0");return t}function C(e){if(e.length%2!==0)throw new Error(`hex length must be even, got ${e.length}`);let t=new Uint8Array(e.length/2);for(let n=0;n<t.length;n++)t[n]=parseInt(e.substr(n*2,2),16);return t}var b=class{map=new Map;async get(t){return this.map.get(t)??null}async set(t,n){this.map.set(t,n)}async delete(t){this.map.delete(t)}},K=class{constructor(t){this.dir=t}pathFor(t){if(!/^[a-z0-9_-]+$/i.test(t))throw new Error(`unsafe channelId for filesystem: ${t}`);return G(this.dir,`${t}.json`)}async get(t){try{let n=await w.readFile(this.pathFor(t),"utf8");return Z(JSON.parse(n))}catch(n){if(n?.code==="ENOENT")return null;throw n}}async set(t,n){let r=this.pathFor(t);await w.mkdir(Y(r),{recursive:!0});let o=`${r}.tmp`;await w.writeFile(o,JSON.stringify(W(n))),await w.rename(o,r)}async delete(t){try{await w.unlink(this.pathFor(t))}catch(n){if(n?.code!=="ENOENT")throw n}}};import{PublicKey as He}from"@solana/web3.js";import{bytesToHex as qe}from"@noble/hashes/utils";import _e from"tweetnacl";import{sha256 as $e}from"@noble/hashes/sha256";var z=6;function A(e,t=z){if(!/^\d+(\.\d+)?$/.test(e))throw new Error(`amount must be a non-negative decimal string, got "${e}"`);let[n,r=""]=e.split(".");if(r.length>t)throw new Error(`amount "${e}" has more than ${t} decimals`);let o=r.padEnd(t,"0"),c=`${n}${o}`.replace(/^0+(?=\d)/,"");return c===""?"0":c}function S(e,t=z){if(!/^\d+$/.test(e))throw new Error(`atomic must be a non-negative integer string, got "${e}"`);let n=e.padStart(t+1,"0"),r=n.slice(0,-t).replace(/^0+(?=\d)/,"")||"0",o=n.slice(-t).replace(/0+$/,"");return o?`${r}.${o}`:r}var B="x-tab-voucher",O=class{map=new Map;get(t){return this.map.get(t)}set(t,n){this.map.set(t,n)}update(t,n){let r=this.map.get(t);r&&(r.lastCumulativeAtomic=n)}delete(t){this.map.delete(t)}},N=class{constructor(t,n,r,o){this.chargeImpl=o;this.channelId=t,this.network=n,this.cumulativeAtomic=r}channelId;network;sessionPublicKey=null;cumulativeAtomic;cumulative(){return S(this.cumulativeAtomic.toString())}bumpCumulative(t){this.cumulativeAtomic=t}setSessionPublicKey(t){this.sessionPublicKey=t}async charge(t){return this.chargeImpl(t)}};function Q(e){if(typeof e!="string"||e.length===0)throw new d("signature_invalid",`missing ${B} header`);let t;try{t=Buffer.from(e,"base64").toString("utf8")}catch{throw new d("signature_invalid","malformed base64")}let n;try{n=JSON.parse(t)}catch{throw new d("signature_invalid","malformed JSON")}if(!n||typeof n!="object"||!n.payload||!n.sessionPublicKey)throw new d("signature_invalid","missing required fields");return{payload:n.payload,sessionPublicKey:x(n.sessionPublicKey),sessionRegistration:x(n.sessionRegistration),sessionSignature:x(n.sessionSignature)}}function x(e){if(typeof e!="string"||e.length%2!==0)throw new d("signature_invalid",`bad hex: ${typeof e}`);let t=new Uint8Array(e.length/2);for(let n=0;n<t.length;n++)t[n]=parseInt(e.substr(n*2,2),16);return t}function ee(e){if(!/^[0-9a-f]{64}$/i.test(e))throw new d("signature_invalid",`channelId must be 64-char hex, got "${e}"`);return x(e)}function te(e){let t=e.store??new b,n=new O,r=typeof e.sellerPubkey=="string"?new X(e.sellerPubkey):e.sellerPubkey,o=e.maxPerVoucherAtomic?BigInt(e.maxPerVoucherAtomic):BigInt(A(e.perUnit))*100n;return async(c,p,g)=>{try{let i=Q(c.headers[B]),a=i.payload.channelId,l=ee(a),s=n.get(a);if(!s){let P=k(i.sessionRegistration);await _(e.connection,P),s={registration:P,lastCumulativeAtomic:"0"},n.set(a,s)}R(i,l),V({registration:s.registration,voucher:i,expectedCounterparty:r,previousCumulativeAtomic:s.lastCumulativeAtomic});let y=BigInt(i.payload.cumulativeAmount),F=BigInt(s.lastCumulativeAtomic),D=y-F;if(D>o)throw new u("cumulative_exceeds_cap",`single voucher increment ${D} exceeds maxPerVoucherAtomic ${o}`);await t.set(a,i),n.update(a,i.payload.cumulativeAmount);let H=new N(a,e.network,y,async P=>{throw new Error("SellerTab.charge() is not driven by the route handler; the buyer presents a fresh voucher per chunk. Use openSse(res, tab) for the metered-stream pattern.")});H.setSessionPublicKey(i.sessionPublicKey),c.tab=H,g()}catch(i){if(i instanceof d||i instanceof m||i instanceof f||i instanceof h||i instanceof u){p.status(402).json({error:"invalid_voucher",reason:i.reason??"unknown",detail:i.message});return}g(i)}}}function ne(e){if(!e.tab)throw new Error("req.tab is missing \u2014 did tabMiddleware run on this route?");return e.tab}function re(e,t){if(!t.tab)throw new Error("openSse requires options.tab");e.headersSent||(e.setHeader("Content-Type","text/event-stream"),e.setHeader("Cache-Control","no-cache"),e.setHeader("Connection","keep-alive"),typeof e.flushHeaders=="function"&&e.flushHeaders());let n=t.tab,r=BigInt(A(n.cumulative())),o=t.perUnit?BigInt(A(t.perUnit)):null,c=0n,p=!1;function g(l=1){if(p)return Promise.reject(new Error("meter ended"));if(o===null)return Promise.reject(new Error("charge() needs options.perUnit"));let s=o*BigInt(l),y=c+s;return y>r?Promise.reject(new u("cumulative_exceeds_cap",`chunk would push request total to ${S(y.toString())} beyond voucher-authorized budget ${S(r.toString())}`)):(c=y,Promise.resolve())}function i(l){if(p)throw new Error("meter ended");let y=(typeof l=="string"?l:Buffer.from(l).toString("utf8")).replace(/\n/g,"\\n");e.write(`data: ${y}
|
|
2
|
+
|
|
3
|
+
`)}function a(){p||(p=!0,e.write(`event: end
|
|
4
|
+
data: {"chargedAtomic":"${c}"}
|
|
5
|
+
|
|
6
|
+
`),e.end())}return{charge:g,send:i,end:a}}export{K as FileVoucherStore,b as InMemoryVoucherStore,m as InvalidRegistrationError,d as InvalidVoucherError,h as InvalidVoucherSignatureError,f as OnChainVerificationError,u as ScopeViolationError,B as TAB_VOUCHER_HEADER,V as enforceScope,re as openSse,k as parseRegistration,L as readVaultState,ne as requireTab,te as tabMiddleware,_ as verifyRegistrationOnChain,R as verifyVoucherSignature};
|