@dvmkit/sdk 0.0.0 → 0.1.0-rc.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/NOTICE +2 -0
- package/README.md +38 -2
- package/dist/chunk-27V2ILSR.js +291 -0
- package/dist/chunk-5GFED3GJ.js +955 -0
- package/dist/chunk-6JZIX5WW.js +1155 -0
- package/dist/chunk-7IH5SG2A.js +1038 -0
- package/dist/chunk-AT6V3SY7.js +102 -0
- package/dist/chunk-DCNT4PJS.js +733 -0
- package/dist/chunk-DMNLFNTW.js +135 -0
- package/dist/chunk-FROTD5XQ.js +70 -0
- package/dist/chunk-H25M54MI.js +149 -0
- package/dist/chunk-KQAJVVZT.js +712 -0
- package/dist/chunk-KXWROQGK.js +74 -0
- package/dist/chunk-L4OYF4DQ.js +67 -0
- package/dist/chunk-NTK5DJ6R.js +1256 -0
- package/dist/chunk-RPXHKMYE.js +3808 -0
- package/dist/chunk-S3XAHZQY.js +63 -0
- package/dist/chunk-YG7G4DPZ.js +25 -0
- package/dist/credit-ledger-EDMEZSA2.js +28 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +303 -0
- package/dist/job-store-C5n6bhap.d.ts +5090 -0
- package/dist/memory-credit-ledger-7TTZDSRS.js +9 -0
- package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
- package/dist/mpp-setup-MOBWGTWJ.js +30 -0
- package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
- package/dist/postgres-job-store-J5F4GUWU.js +7 -0
- package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
- package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
- package/dist/pricing-4CEB34RM.js +48 -0
- package/dist/processed-payment-store-HAA4SFNK.js +11 -0
- package/dist/revenue-reporter-M35KP6V7.js +435 -0
- package/dist/server/index.d.ts +4108 -0
- package/dist/server/index.js +22538 -0
- package/dist/ssrf-BdHsrrIb.d.ts +325 -0
- package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
- package/dist/tempo-session-store-FTEEGZXA.js +467 -0
- package/dist/testing/index.d.ts +135 -0
- package/dist/testing/index.js +151 -0
- package/dist/x402-35VLYFKZ.js +1272 -0
- package/package.json +89 -6
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { C as Currency } from './job-store-C5n6bhap.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Quote-time fx snapshot. Embedded in scribe's `lockedQuote` for within-job
|
|
5
|
+
* consistency between upfront and mid-job charges (see scribe's
|
|
6
|
+
* `runTranscribeJob`). Within-call lock = "rate cached at first fetch and
|
|
7
|
+
* reused across the duration calculation"; across-call lock degrades to
|
|
8
|
+
* "fxFetcher {@link FX_CACHE_TTL_MS} cache window" — see
|
|
9
|
+
* `public design rationale`.
|
|
10
|
+
*
|
|
11
|
+
* `rates` maps an ISO-4217 lowercase currency code to that currency's
|
|
12
|
+
* value-per-BTC at `as_of`. The set of currencies present matches the
|
|
13
|
+
* fetcher's configured set (see `createFxFetcher.currencies`).
|
|
14
|
+
*/
|
|
15
|
+
interface FxRateSnapshot {
|
|
16
|
+
rates: Record<Currency, number>;
|
|
17
|
+
as_of: string;
|
|
18
|
+
/**
|
|
19
|
+
* Configured currencies whose rate here came from an *earlier* fetch, because
|
|
20
|
+
* the source's latest body omitted them (internal-review). The rate is the best one
|
|
21
|
+
* this process holds; `as_of` describes the rest of the snapshot, so a
|
|
22
|
+
* consumer pricing one of these is degrading and should say so — the mid-job
|
|
23
|
+
* legs report it as `credit_topup_stale_rate` / `dvm.ask_pin_stale_rate`.
|
|
24
|
+
* Absent on the normal complete-body path.
|
|
25
|
+
*/
|
|
26
|
+
carried_forward?: readonly Currency[];
|
|
27
|
+
/**
|
|
28
|
+
* Configured currencies absent from `rates` altogether — the source omitted
|
|
29
|
+
* them and this process has never held one to carry forward. {@link fxRateFor}
|
|
30
|
+
* raises `fx_rate_unavailable` rather than `unsupported_currency` for these:
|
|
31
|
+
* the DVM advertises the currency, so this is a rate outage narrowed to one
|
|
32
|
+
* code, and a caller's agent must read it on the same retryable envelope as
|
|
33
|
+
* any other (internal-review). Absent on the normal complete-body path.
|
|
34
|
+
*/
|
|
35
|
+
unavailable?: readonly Currency[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Default fiat set the bundled fx fetcher resolves against BTC. Add a currency
|
|
39
|
+
* here and it flows through `DEFAULT_FX_RATE_SOURCE`, the
|
|
40
|
+
* `UnsupportedCurrencyError` supported-set string, and every fiat-form
|
|
41
|
+
* `requestPayment` call site — no shape change at the SDK or caller surface.
|
|
42
|
+
*/
|
|
43
|
+
declare const DEFAULT_FX_CURRENCIES: readonly Currency[];
|
|
44
|
+
/**
|
|
45
|
+
* Default fx-rate source. Points at CoinGecko's free `simple/price` endpoint.
|
|
46
|
+
* Used for self-hosted DVMs and as the fallback when `DVMKIT_FX_SOURCE` is
|
|
47
|
+
* unset. Hosted DVMs hit the platform-served endpoint instead — see
|
|
48
|
+
* {@link PlatformFxSource} and {@link resolveFxSourceFromEnv}.
|
|
49
|
+
*/
|
|
50
|
+
declare const DEFAULT_FX_RATE_SOURCE: string;
|
|
51
|
+
/**
|
|
52
|
+
* Platform-served fx source. Hosted DVMs hit `${DVMKIT_PLATFORM_URL}/_internal/fx`
|
|
53
|
+
* authenticated with their per-DVM bearer token. The endpoint serves the
|
|
54
|
+
* canonical `fx_rates` snapshot the platform refreshes daily — one upstream
|
|
55
|
+
* fetch covers every hosted DVM. Self-hosted DVMs (no platform wiring) fall
|
|
56
|
+
* back to CoinGecko-direct.
|
|
57
|
+
*/
|
|
58
|
+
interface PlatformFxSource {
|
|
59
|
+
kind: "platform";
|
|
60
|
+
url: string;
|
|
61
|
+
token: string;
|
|
62
|
+
}
|
|
63
|
+
/** Agent-relayable copy for an {@link FxRateUnavailableError}. */
|
|
64
|
+
interface FxRateUnavailableCopy {
|
|
65
|
+
display?: string;
|
|
66
|
+
hint?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The platform's *deliberate* known-stale refusal, threaded through the fetch
|
|
70
|
+
* boundary from the `_internal/fx` 503 body (internal-review/internal-review). Present on an
|
|
71
|
+
* {@link FxRateUnavailableError} only when the platform decided the snapshot is
|
|
72
|
+
* too old to price against (`error.code === "fx_rate_unavailable"` with a
|
|
73
|
+
* `detail.stale_days`) — as opposed to a transient unreachability, which carries
|
|
74
|
+
* no `knownStale`. `resolveServerBtcUsdRate` fails closed on this rather than
|
|
75
|
+
* degrading to `lastKnown` (internal-review): the platform already refused to serve the
|
|
76
|
+
* rate, so pricing off a frozen local copy of it would defeat the refusal.
|
|
77
|
+
*/
|
|
78
|
+
interface FxKnownStale {
|
|
79
|
+
staleDays: number;
|
|
80
|
+
staleSource?: string;
|
|
81
|
+
thresholdDays?: number;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Thrown when the fx source can't be reached or returns an unparseable rate.
|
|
85
|
+
* `code`/`display`/`hint` mirror the structured-error envelope the SDK quote
|
|
86
|
+
* path renders to agents as a 503-style response — operators retry; agents
|
|
87
|
+
* pass `display` to humans.
|
|
88
|
+
*
|
|
89
|
+
* `copy` overrides the quote-flavoured defaults. The payment rails raise the
|
|
90
|
+
* same code from a different moment in the request (internal-review) — an agent that
|
|
91
|
+
* can't be quoted and an agent whose payment couldn't be priced retry the same
|
|
92
|
+
* way, so they share one code, but "nothing was charged" is only true of the
|
|
93
|
+
* latter and must not be said to the former.
|
|
94
|
+
*
|
|
95
|
+
* One code, two envelopes: the quote path renders `{ error: { code, display,
|
|
96
|
+
* hint } }`, the payment path the flat `{ error: "fx_rate_unavailable", display,
|
|
97
|
+
* hint }` that every other payment error uses (`buildPaymentErrorResponse`). So
|
|
98
|
+
* an agent has one retry rule but still two shapes to parse the code out of —
|
|
99
|
+
* that split is the response envelopes', not this error's, and unifying it is
|
|
100
|
+
* internal-review.
|
|
101
|
+
*/
|
|
102
|
+
declare class FxRateUnavailableError extends Error {
|
|
103
|
+
readonly code = "fx_rate_unavailable";
|
|
104
|
+
readonly display: string;
|
|
105
|
+
readonly hint: string;
|
|
106
|
+
/**
|
|
107
|
+
* Set only when the platform explicitly refused a *known-stale* snapshot
|
|
108
|
+
* (internal-review). Absent means transient/unreachable — a distinction the payment
|
|
109
|
+
* rail's amount computation branches on: refuse on `knownStale`, degrade to
|
|
110
|
+
* the last-known rate without it. See {@link FxKnownStale}.
|
|
111
|
+
*/
|
|
112
|
+
readonly knownStale?: FxKnownStale;
|
|
113
|
+
constructor(message?: string, copy?: FxRateUnavailableCopy, knownStale?: FxKnownStale);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Snapshot fetcher for BTC against one or more fiat currencies. Caches
|
|
117
|
+
* in-process for {@link FX_CACHE_TTL_MS} so concurrent quotes amortise the
|
|
118
|
+
* rate-limit budget, and a within-window quote → job pair settles at the same
|
|
119
|
+
* "locked" snapshot without an explicit cross-call lock. `supportedCurrencies()`
|
|
120
|
+
* returns the configured set synchronously so callers can preflight an
|
|
121
|
+
* unsupported currency without forcing a network hit.
|
|
122
|
+
*/
|
|
123
|
+
interface FxFetcher {
|
|
124
|
+
fetch(): Promise<FxRateSnapshot>;
|
|
125
|
+
supportedCurrencies(): readonly Currency[];
|
|
126
|
+
/**
|
|
127
|
+
* The last snapshot this fetcher successfully retrieved, paired with the Unix
|
|
128
|
+
* ms it was cached, or `null` if none has landed yet. Ignores the freshness
|
|
129
|
+
* TTL — the cache is never cleared, so this survives an upstream outage and is
|
|
130
|
+
* the offline degradation source for the payment rail (internal-review): when
|
|
131
|
+
* {@link fetch} throws, callers serve this rather than fail a priced request.
|
|
132
|
+
* Optional so lightweight test fakes need not implement it.
|
|
133
|
+
*/
|
|
134
|
+
lastKnown?(): {
|
|
135
|
+
snapshot: FxRateSnapshot;
|
|
136
|
+
cachedAt: number;
|
|
137
|
+
} | null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Options for `createFxFetcher`. `source` accepts either:
|
|
141
|
+
* - a URL string targeting a CoinGecko-shaped endpoint
|
|
142
|
+
* (`{ bitcoin: { <currency>: number, ... } }`), or
|
|
143
|
+
* - a {@link PlatformFxSource} object pointing at the dvmkit platform's
|
|
144
|
+
* `_internal/fx` endpoint (the canonical `fx_rates` snapshot, served with
|
|
145
|
+
* bearer auth).
|
|
146
|
+
*
|
|
147
|
+
* `undefined` falls back to {@link DEFAULT_FX_RATE_SOURCE} (CoinGecko-direct).
|
|
148
|
+
*/
|
|
149
|
+
interface CreateFxFetcherOpts {
|
|
150
|
+
source?: string | PlatformFxSource;
|
|
151
|
+
/** Currencies the fetcher advertises support for. Defaults to {@link DEFAULT_FX_CURRENCIES}. */
|
|
152
|
+
currencies?: readonly Currency[];
|
|
153
|
+
fetchFn?: typeof fetch;
|
|
154
|
+
now?: () => number;
|
|
155
|
+
cacheTtlMs?: number;
|
|
156
|
+
retries?: number;
|
|
157
|
+
/**
|
|
158
|
+
* Base backoff between retry attempts, doubling each time. Defaults to
|
|
159
|
+
* {@link FX_RETRY_BASE_DELAY_MS}; pass `0` in tests so the retry loop stays
|
|
160
|
+
* instant.
|
|
161
|
+
*/
|
|
162
|
+
retryDelayMs?: number;
|
|
163
|
+
}
|
|
164
|
+
/** Build the default fx fetcher backed by `DVMKIT_FX_SOURCE` (or CoinGecko). */
|
|
165
|
+
declare function createFxFetcher(opts?: CreateFxFetcherOpts): FxFetcher;
|
|
166
|
+
/**
|
|
167
|
+
* Look up `currency`'s rate in `snapshot`.
|
|
168
|
+
*
|
|
169
|
+
* Two ways it can be missing, and they are not the same failure (internal-review):
|
|
170
|
+
*
|
|
171
|
+
* - The snapshot lists it in {@link FxRateSnapshot.unavailable} — the DVM
|
|
172
|
+
* advertises the currency, but its rate source omitted it and this process
|
|
173
|
+
* holds no earlier one. That is a rate outage narrowed to one code, so it
|
|
174
|
+
* raises `fx_rate_unavailable`, retryable, the same envelope every other rate
|
|
175
|
+
* failure reaches the caller's agent on (internal-review). Answering
|
|
176
|
+
* `unsupported_currency` here would reach the quote path as a `400
|
|
177
|
+
* invalid_quote_result` — telling an agent the DVM's own quote is malformed,
|
|
178
|
+
* and not to retry, over a transient omission upstream.
|
|
179
|
+
* - Anything else — a currency neither configured nor fetched, typically an
|
|
180
|
+
* operator's `DVMKIT_FX_SOURCE` override that doesn't carry it. Raises
|
|
181
|
+
* `UnsupportedCurrencyError` naming the keys actually present, so the message
|
|
182
|
+
* stays truthful about what this snapshot can price.
|
|
183
|
+
*/
|
|
184
|
+
declare function fxRateFor(snapshot: FxRateSnapshot, currency: Currency): number;
|
|
185
|
+
/**
|
|
186
|
+
* Resolve the fx source from a DVM process's env. Used by first-party DVMs to
|
|
187
|
+
* pick between platform-served and CoinGecko-direct fetchers based on whether
|
|
188
|
+
* the platform wired this DVM up at deploy-time. Returns `undefined` when no
|
|
189
|
+
* env override is set — the default CoinGecko URL kicks in inside
|
|
190
|
+
* {@link createFxFetcher}.
|
|
191
|
+
*
|
|
192
|
+
* - `DVMKIT_FX_SOURCE === "platform"` + `DVMKIT_PLATFORM_URL` +
|
|
193
|
+
* `DVMKIT_PLATFORM_TOKEN` → {@link PlatformFxSource} pointing at
|
|
194
|
+
* `${DVMKIT_PLATFORM_URL}/_internal/fx`. The platform's container deploy
|
|
195
|
+
* pipeline injects all three together (see `applyPlatformTokenInjection`).
|
|
196
|
+
* - `DVMKIT_FX_SOURCE` starts with `http(s)://` → URL passed through verbatim
|
|
197
|
+
* (CoinGecko-shaped override, mostly used by tests + self-hosted operators
|
|
198
|
+
* pointing at a private mirror).
|
|
199
|
+
* - Otherwise → `undefined` (CoinGecko-direct default).
|
|
200
|
+
*
|
|
201
|
+
* The magic string `"platform"` and the URL fallback are intentionally
|
|
202
|
+
* non-overlapping shapes — operators who want a CoinGecko-style URL pass a
|
|
203
|
+
* URL, and platform-injected DVMs get the string flag plus the two address
|
|
204
|
+
* env vars.
|
|
205
|
+
*/
|
|
206
|
+
declare function resolveFxSourceFromEnv(env: Record<string, string | undefined>): string | PlatformFxSource | undefined;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Why `assertSafeUrl` refused this URL. Carried on `SSRFError` so callers can
|
|
210
|
+
* branch without string-matching `err.message` — DNS failures (NXDOMAIN, empty
|
|
211
|
+
* resolution), unparseable URLs, and a non-https scheme each have distinct
|
|
212
|
+
* recovery paths and shouldn't collapse together.
|
|
213
|
+
*/
|
|
214
|
+
type SSRFReason = "invalid_url" | "insecure_scheme" | "private_address" | "dns_failed";
|
|
215
|
+
/**
|
|
216
|
+
* Thrown when an input URL points at internal infrastructure we refuse to
|
|
217
|
+
* fetch from inside the cluster. Carries a `reason` discriminator so the
|
|
218
|
+
* caller can map cleanly onto its public taxonomy instead of inspecting the
|
|
219
|
+
* message string.
|
|
220
|
+
*/
|
|
221
|
+
declare class SSRFError extends Error {
|
|
222
|
+
readonly code: "ssrf_blocked";
|
|
223
|
+
readonly reason: SSRFReason;
|
|
224
|
+
constructor(reason: SSRFReason, message: string);
|
|
225
|
+
}
|
|
226
|
+
/** Pluggable DNS resolver. Defaults to `node:dns/promises` `lookup(host, { all: true })`. */
|
|
227
|
+
type SSRFResolver = (host: string) => Promise<{
|
|
228
|
+
address: string;
|
|
229
|
+
family: number;
|
|
230
|
+
}[]>;
|
|
231
|
+
/**
|
|
232
|
+
* Configurable hooks for `assertSafeUrl`. Defaults preserve the dvmkit
|
|
233
|
+
* baseline denylist; extra entries compose with (never replace) the defaults.
|
|
234
|
+
*/
|
|
235
|
+
interface SSRFGuardOpts {
|
|
236
|
+
/** Custom DNS resolver — typically a test stub. */
|
|
237
|
+
resolver?: SSRFResolver;
|
|
238
|
+
/** Additional exact hostnames to deny on top of the defaults (lowercased on input). */
|
|
239
|
+
extraHostnamesExact?: Iterable<string>;
|
|
240
|
+
/**
|
|
241
|
+
* Additional hostname suffixes to deny on top of the defaults. Each entry
|
|
242
|
+
* should include the leading dot (e.g. `.example.invalid`) — the same shape
|
|
243
|
+
* the defaults use.
|
|
244
|
+
*/
|
|
245
|
+
extraHostnameSuffixes?: Iterable<string>;
|
|
246
|
+
/** Additional IPv4 predicate. Returns true for an unsafe address. */
|
|
247
|
+
extraUnsafeIPv4?: (ipv4: string) => boolean;
|
|
248
|
+
/** Additional IPv6 predicate. Returns true for an unsafe address. */
|
|
249
|
+
extraUnsafeIPv6?: (ipv6: string) => boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Underlying fetch implementation `createPinnedFetch` wraps. Defaults to
|
|
252
|
+
* `globalThis.fetch`. Pass an instrumented wrapper (e.g. scribe's
|
|
253
|
+
* `ctx.fetch` from `createInstrumentedFetch`) to keep `dvm.fetch` spans
|
|
254
|
+
* around outbound HEAD / GET — the dispatcher pin for every safe redirect
|
|
255
|
+
* hop is preserved because `init.dispatcher` flows through any thin fetch
|
|
256
|
+
* wrapper unchanged.
|
|
257
|
+
*/
|
|
258
|
+
baseFetch?: typeof fetch;
|
|
259
|
+
/**
|
|
260
|
+
* Called after a redirect response is accepted and before the target is
|
|
261
|
+
* resolved or connected. Lets an outer transport refuse to replay an
|
|
262
|
+
* original request once its source has demonstrably responded.
|
|
263
|
+
*/
|
|
264
|
+
onRedirectFollowed?: () => void;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Reject URLs whose host points at internal infrastructure — private /
|
|
268
|
+
* link-local IP ranges, the AWS / GCP metadata IP, Fly's `*.internal`
|
|
269
|
+
* service-discovery names, and a small denylist of obviously-internal
|
|
270
|
+
* hostnames. Defence-in-depth — full mitigation belongs at the egress firewall
|
|
271
|
+
* and at fetch-time IP pinning (`createPinnedFetch`, internal-review).
|
|
272
|
+
*
|
|
273
|
+
* Throws `SSRFError` with a `reason` discriminator so callers can branch onto
|
|
274
|
+
* their own typed error taxonomy.
|
|
275
|
+
*/
|
|
276
|
+
declare function assertSafeUrl(url: string, opts?: SSRFGuardOpts): Promise<void>;
|
|
277
|
+
/**
|
|
278
|
+
* A `fetch`-compatible function pinned to a single pre-validated IP address.
|
|
279
|
+
*
|
|
280
|
+
* Returned by {@link createPinnedFetch}. Closes the DNS-rebinding window in
|
|
281
|
+
* which `assertSafeUrl` validates an address but the subsequent `fetch()`
|
|
282
|
+
* re-resolves DNS and lands on a different (private) address.
|
|
283
|
+
*
|
|
284
|
+
* Callers MUST `await close()` when done with the pin so undici's connection
|
|
285
|
+
* pool is released.
|
|
286
|
+
*/
|
|
287
|
+
interface PinnedFetch {
|
|
288
|
+
/** Fetch whose initial request and every followed redirect are pinned to their pre-validated address sets. */
|
|
289
|
+
fetch: typeof fetch;
|
|
290
|
+
/** Primary (first) validated address of the initial URL. */
|
|
291
|
+
address: string;
|
|
292
|
+
/** IP family (4 or 6) of {@link address} (the primary). */
|
|
293
|
+
family: number;
|
|
294
|
+
/** Release the undici connection pool backing this pinned fetch. */
|
|
295
|
+
close(): Promise<void>;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Resolve the URL's host once, validate every answer against the same
|
|
299
|
+
* denylist `assertSafeUrl` applies, then return a `fetch` whose `connect.lookup`
|
|
300
|
+
* replays that frozen, pre-validated address set (see {@link buildPinnedLookup}).
|
|
301
|
+
* Redirects are followed manually: each target URL is independently required
|
|
302
|
+
* to be HTTPS, resolved once, denylist-checked, and pinned before it can be
|
|
303
|
+
* requested. That prevents an automatic redirect from reusing the source
|
|
304
|
+
* host's dispatcher for a different hostname, while keeping a redirect from
|
|
305
|
+
* opening a DNS-rebinding window.
|
|
306
|
+
*
|
|
307
|
+
* Throws `SSRFError` with the same `reason` discriminator as `assertSafeUrl`,
|
|
308
|
+
* so error-mapping paths (`mapSsrfError` in cast/scribe) work unchanged.
|
|
309
|
+
*
|
|
310
|
+
* For IP-literal URLs no dispatcher is needed, but redirects still pass
|
|
311
|
+
* through the same validation loop.
|
|
312
|
+
*
|
|
313
|
+
* Replaying the full validated set (not just `addresses[0]`) preserves Node's
|
|
314
|
+
* happy-eyeballs fallback across the answers — a host whose first address is an
|
|
315
|
+
* unroutable family still connects via the others — while keeping the rebind
|
|
316
|
+
* window closed, since every replayed address was already denylist-checked.
|
|
317
|
+
*
|
|
318
|
+
* Pass `opts.baseFetch` to wrap an instrumented fetch (e.g. scribe's
|
|
319
|
+
* `ctx.fetch` from `createInstrumentedFetch`) — `dvm.fetch` spans flow
|
|
320
|
+
* through because `init.dispatcher` is forwarded to the underlying
|
|
321
|
+
* `globalThis.fetch` unchanged.
|
|
322
|
+
*/
|
|
323
|
+
declare function createPinnedFetch(url: string, opts?: SSRFGuardOpts): Promise<PinnedFetch>;
|
|
324
|
+
|
|
325
|
+
export { type CreateFxFetcherOpts as C, DEFAULT_FX_CURRENCIES as D, type FxFetcher as F, type PinnedFetch as P, SSRFError as S, DEFAULT_FX_RATE_SOURCE as a, type FxRateSnapshot as b, FxRateUnavailableError as c, type PlatformFxSource as d, type SSRFGuardOpts as e, type SSRFReason as f, type SSRFResolver as g, assertSafeUrl as h, createFxFetcher as i, createPinnedFetch as j, fxRateFor as k, resolveFxSourceFromEnv as r };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseStoreValue,
|
|
3
|
+
stringifyStoreValue
|
|
4
|
+
} from "./chunk-YG7G4DPZ.js";
|
|
5
|
+
import {
|
|
6
|
+
withSdkInitLock
|
|
7
|
+
} from "./chunk-S3XAHZQY.js";
|
|
8
|
+
|
|
9
|
+
// src/sdk/server/tempo-charge-store.ts
|
|
10
|
+
var DEFAULT_TABLE_NAME = "mpp_tempo_charge_replays";
|
|
11
|
+
var DEFAULT_RETENTION_MS = 60 * 60 * 1e3;
|
|
12
|
+
var DEFAULT_GC_SAMPLE_RATE = 0.01;
|
|
13
|
+
var VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
14
|
+
var PostgresTempoChargeStore = class {
|
|
15
|
+
pool;
|
|
16
|
+
tableName;
|
|
17
|
+
retentionMs;
|
|
18
|
+
gcSampleRate;
|
|
19
|
+
now;
|
|
20
|
+
constructor(pool, opts) {
|
|
21
|
+
const tableName = opts?.tableName ?? DEFAULT_TABLE_NAME;
|
|
22
|
+
if (!VALID_IDENTIFIER.test(tableName)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`PostgresTempoChargeStore: tableName must match ${VALID_IDENTIFIER.source}, got "${tableName}"`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
this.pool = pool;
|
|
28
|
+
this.tableName = tableName;
|
|
29
|
+
this.retentionMs = opts?.retentionMs ?? DEFAULT_RETENTION_MS;
|
|
30
|
+
this.gcSampleRate = opts?.gcSampleRate ?? DEFAULT_GC_SAMPLE_RATE;
|
|
31
|
+
this.now = opts?.now ?? (() => Date.now());
|
|
32
|
+
this.get = this.get.bind(this);
|
|
33
|
+
this.put = this.put.bind(this);
|
|
34
|
+
this.delete = this.delete.bind(this);
|
|
35
|
+
this.update = this.update.bind(this);
|
|
36
|
+
}
|
|
37
|
+
/** Run the CREATE TABLE migration. Idempotent — safe to call repeatedly. */
|
|
38
|
+
async init() {
|
|
39
|
+
await withSdkInitLock(this.pool, () => this.createTables());
|
|
40
|
+
}
|
|
41
|
+
/** Read one stored value, or `null` when absent — mppx's miss sentinel. */
|
|
42
|
+
async get(key) {
|
|
43
|
+
const { rows } = await this.pool.query(
|
|
44
|
+
`SELECT value_json FROM ${this.tableName} WHERE store_key = $1`,
|
|
45
|
+
[key]
|
|
46
|
+
);
|
|
47
|
+
return rows[0] ? parseStoreValue(rows[0].value_json) : null;
|
|
48
|
+
}
|
|
49
|
+
/** Replace one stored value. */
|
|
50
|
+
async put(key, value) {
|
|
51
|
+
await this.update(key, () => ({ op: "set", value, result: void 0 }));
|
|
52
|
+
}
|
|
53
|
+
/** Delete one stored value — mppx's compensating release on a failed broadcast. */
|
|
54
|
+
async delete(key) {
|
|
55
|
+
await this.update(key, () => ({ op: "delete", result: void 0 }));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Atomic read-modify-write, linearized across every machine sharing the pool.
|
|
59
|
+
*
|
|
60
|
+
* The advisory lock is taken before the row lock because a key's *first*
|
|
61
|
+
* write is the one that matters here: `FOR UPDATE` locks nothing when no row
|
|
62
|
+
* exists, so two machines marking one hash for the first time would both read
|
|
63
|
+
* `null` and both believe they won.
|
|
64
|
+
*
|
|
65
|
+
* Replay keys are unique per credential, so they never contend. The contended
|
|
66
|
+
* key is the fee-sponsor budget — one per `(chain, sponsor)`, read-modified
|
|
67
|
+
* several times per sponsored charge — and serializing it across machines is
|
|
68
|
+
* the point of sharing the store at all. The critical section is a SELECT and
|
|
69
|
+
* an upsert, so a queue of waiters clears in tens of milliseconds; a pooled
|
|
70
|
+
* connection is held for exactly that long and never across a chain call.
|
|
71
|
+
*/
|
|
72
|
+
async update(key, fn) {
|
|
73
|
+
const client = await this.pool.connect();
|
|
74
|
+
try {
|
|
75
|
+
await client.query("BEGIN");
|
|
76
|
+
await client.query(`SELECT pg_advisory_xact_lock(hashtextextended($1, 2036))`, [key]);
|
|
77
|
+
const { rows } = await client.query(
|
|
78
|
+
`SELECT value_json FROM ${this.tableName} WHERE store_key = $1 FOR UPDATE`,
|
|
79
|
+
[key]
|
|
80
|
+
);
|
|
81
|
+
const current = rows[0] ? parseStoreValue(rows[0].value_json) : null;
|
|
82
|
+
const change = fn(current);
|
|
83
|
+
const now = this.now();
|
|
84
|
+
if (change.op === "set") {
|
|
85
|
+
await client.query(
|
|
86
|
+
`INSERT INTO ${this.tableName} (store_key, value_json, expires_at)
|
|
87
|
+
VALUES ($1, $2, $3)
|
|
88
|
+
ON CONFLICT (store_key) DO UPDATE
|
|
89
|
+
SET value_json = EXCLUDED.value_json, expires_at = EXCLUDED.expires_at`,
|
|
90
|
+
[
|
|
91
|
+
key,
|
|
92
|
+
stringifyStoreValue(change.value, "Tempo charge store value"),
|
|
93
|
+
now + this.retentionMs
|
|
94
|
+
]
|
|
95
|
+
);
|
|
96
|
+
} else if (change.op === "delete") {
|
|
97
|
+
await client.query(`DELETE FROM ${this.tableName} WHERE store_key = $1`, [key]);
|
|
98
|
+
}
|
|
99
|
+
await client.query("COMMIT");
|
|
100
|
+
if (change.op !== "noop") this.maybeSweep(now);
|
|
101
|
+
return change.result;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
104
|
+
throw error;
|
|
105
|
+
} finally {
|
|
106
|
+
client.release();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
|
|
110
|
+
async createTables() {
|
|
111
|
+
await this.pool.query(`
|
|
112
|
+
CREATE TABLE IF NOT EXISTS ${this.tableName} (
|
|
113
|
+
store_key TEXT PRIMARY KEY,
|
|
114
|
+
value_json TEXT NOT NULL,
|
|
115
|
+
expires_at BIGINT NOT NULL
|
|
116
|
+
);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS ${this.tableName}_expires_at_idx
|
|
118
|
+
ON ${this.tableName} (expires_at);
|
|
119
|
+
`);
|
|
120
|
+
}
|
|
121
|
+
maybeSweep(now) {
|
|
122
|
+
if (this.gcSampleRate <= 0 || Math.random() >= this.gcSampleRate) return;
|
|
123
|
+
this.pool.query(`DELETE FROM ${this.tableName} WHERE expires_at < $1`, [now]).catch((err) => {
|
|
124
|
+
console.error("PostgresTempoChargeStore GC sweep failed:", err);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
export {
|
|
129
|
+
PostgresTempoChargeStore
|
|
130
|
+
};
|