@craigruks/x402-server-guard 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/SECURITY.md +54 -0
- package/dist/cache.d.ts +36 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +31 -0
- package/dist/cache.js.map +1 -0
- package/dist/canonical.d.ts +31 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +38 -0
- package/dist/canonical.js.map +1 -0
- package/dist/cloudflare/durable-object.d.ts +52 -0
- package/dist/cloudflare/durable-object.d.ts.map +1 -0
- package/dist/cloudflare/durable-object.js +83 -0
- package/dist/cloudflare/durable-object.js.map +1 -0
- package/dist/error.d.ts +22 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +17 -0
- package/dist/error.js.map +1 -0
- package/dist/guard.d.ts +46 -0
- package/dist/guard.d.ts.map +1 -0
- package/dist/guard.js +88 -0
- package/dist/guard.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/nonce-store.d.ts +41 -0
- package/dist/nonce-store.d.ts.map +1 -0
- package/dist/nonce-store.js +103 -0
- package/dist/nonce-store.js.map +1 -0
- package/dist/protect.d.ts +45 -0
- package/dist/protect.d.ts.map +1 -0
- package/dist/protect.js +54 -0
- package/dist/protect.js.map +1 -0
- package/dist/result.d.ts +37 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +54 -0
- package/dist/result.js.map +1 -0
- package/dist/store-types.d.ts +66 -0
- package/dist/store-types.d.ts.map +1 -0
- package/dist/store-types.js +2 -0
- package/dist/store-types.js.map +1 -0
- package/package.json +90 -0
- package/src/cache.ts +48 -0
- package/src/canonical.ts +40 -0
- package/src/cloudflare/durable-object.ts +128 -0
- package/src/error.ts +31 -0
- package/src/guard.ts +153 -0
- package/src/index.ts +49 -0
- package/src/nonce-store.ts +149 -0
- package/src/protect.ts +96 -0
- package/src/result.ts +60 -0
- package/src/store-types.ts +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Craig Ruks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# x402-server-guard
|
|
2
|
+
|
|
3
|
+
**Server-side hardening middleware for [x402](https://github.com/coinbase/x402)
|
|
4
|
+
payment endpoints.**
|
|
5
|
+
|
|
6
|
+
[](https://github.com/craigruks/x402-server-guard/actions/workflows/ci.yml)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
Documentation: https://craigruks.github.io/x402-server-guard/
|
|
10
|
+
|
|
11
|
+
x402 lets a server charge for a resource by returning `402 Payment Required` and
|
|
12
|
+
verifying a signed payment. Published research has shown that a naïve resource
|
|
13
|
+
server is exploitable in several ways: a payment can be replayed, reused against
|
|
14
|
+
a different resource, raced to duplicate delivery before settlement confirms, or
|
|
15
|
+
leaked to unpaid clients through a shared cache. This library is the enforcement
|
|
16
|
+
layer a merchant wraps their endpoint in to close those gaps.
|
|
17
|
+
|
|
18
|
+
> [!WARNING]
|
|
19
|
+
> **Status: early, pre-1.0.** All four enumerated attack classes below have a
|
|
20
|
+
> mitigation implemented. It is **not audited** and is **not a security
|
|
21
|
+
> guarantee**; it mitigates these specific classes only and cannot make an insecure
|
|
22
|
+
> endpoint safe on its own. See the mitigation table for scope.
|
|
23
|
+
|
|
24
|
+
## Security disclaimer
|
|
25
|
+
|
|
26
|
+
This software is provided **"AS IS"**, without warranty of any kind. It is **not
|
|
27
|
+
audited** and is **not a security guarantee**. It mitigates specific, enumerated
|
|
28
|
+
attack classes only. It cannot make an insecure payment endpoint safe on its own.
|
|
29
|
+
**The authors accept no liability for any loss of funds or damages.** See
|
|
30
|
+
[SECURITY.md](./SECURITY.md) and the [LICENSE](./LICENSE).
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install @craigruks/x402-server-guard
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Node ≥ 22. Zero runtime dependencies. Ships ESM with type declarations; a
|
|
39
|
+
TypeScript or JavaScript consumer both import the same build.
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
Reserve a payment's nonce through the guard before you grant the resource. The
|
|
44
|
+
first request for a nonce wins; a replay or a concurrent race is denied. The
|
|
45
|
+
decision is a value, never a throw, so a stray `try/catch` cannot turn a deny
|
|
46
|
+
into an accidental grant.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createGuard } from "@craigruks/x402-server-guard";
|
|
50
|
+
|
|
51
|
+
const guard = createGuard();
|
|
52
|
+
|
|
53
|
+
// Inside your paid handler, after the facilitator verifies the payment:
|
|
54
|
+
const reservation = await guard.reserve({
|
|
55
|
+
nonce: authorization.nonce, // the EIP-3009 nonce
|
|
56
|
+
resource: request.url, // which resource this payment is for
|
|
57
|
+
expiresAt: Number(authorization.validBefore), // unix seconds
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!reservation.reserved) {
|
|
61
|
+
return deny(reservation.reason.code); // e.g. "nonce-already-reserved"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Settle before granting: a payment that fails to settle yields no resource.
|
|
65
|
+
const settled = await facilitator.settle(payload, requirements);
|
|
66
|
+
if (!settled.success) {
|
|
67
|
+
return deny("settle failed");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return grant(resource);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> [!WARNING]
|
|
74
|
+
> `createGuard()` with no store uses an in-memory store that protects **one process
|
|
75
|
+
> only**. On Cloudflare Workers, Vercel, AWS Lambda, or any autoscaled fleet, each
|
|
76
|
+
> isolate holds its own map, so replay and race protection do not hold across
|
|
77
|
+
> instances. For those deploys, pass a store backed by an atomic compare-and-set: a
|
|
78
|
+
> Durable Object, Redis `SET NX`, or a database unique constraint (not a plain
|
|
79
|
+
> get-then-put store like Workers KV, which reopens the race). Any backend that
|
|
80
|
+
> implements the `NonceStore` contract works. One is built start to finish in the box, a
|
|
81
|
+
> Cloudflare Durable Object adapter:
|
|
82
|
+
> `import { createDurableObjectNonceStore } from "@craigruks/x402-server-guard/cloudflare"`
|
|
83
|
+
> (see [the deployment guide](https://craigruks.github.io/x402-server-guard/deployment/cloudflare-durable-objects/)).
|
|
84
|
+
> The atomic compare-and-set contract for other backends is in
|
|
85
|
+
> [`docs/hardening.md`](./docs/hardening.md).
|
|
86
|
+
|
|
87
|
+
### One call: `protect`
|
|
88
|
+
|
|
89
|
+
`protect` runs the whole secure flow (`reserve → settle → (confirm) → deliver`) and
|
|
90
|
+
returns the cache directives on grant, releasing the reservation if the settle
|
|
91
|
+
fails or finality is not reached. It has no runtime dependencies and takes plain
|
|
92
|
+
callbacks, so it drops into any framework:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { protect } from "@craigruks/x402-server-guard";
|
|
96
|
+
|
|
97
|
+
// After the facilitator verifies the payment:
|
|
98
|
+
const decision = await protect(
|
|
99
|
+
guard,
|
|
100
|
+
{ nonce, resource: request.url, expiresAt: Number(authorization.validBefore) },
|
|
101
|
+
{
|
|
102
|
+
// settle resolves true only when the payment actually settled.
|
|
103
|
+
settle: async () => (await facilitator.settle(payload, requirements)).success,
|
|
104
|
+
deliver: () => resource,
|
|
105
|
+
// Grant on settle success (finality rests with the facilitator and the chain).
|
|
106
|
+
// Use `finality: "confirm"` with a `confirm()` callback to hold for k confirmations.
|
|
107
|
+
finality: "facilitator",
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
if (!decision.granted) return deny(decision.reason.code);
|
|
111
|
+
response.headers.set("Cache-Control", decision.cacheControl);
|
|
112
|
+
return grant(decision.resource);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Two runnable examples: [`examples/secure-flow.ts`](./examples/secure-flow.ts)
|
|
116
|
+
(the concurrent race, blocked) and [`examples/hono-server.ts`](./examples/hono-server.ts)
|
|
117
|
+
(a Hono route protected end to end). Bind the nonce to the **served** route (the
|
|
118
|
+
request URL), not the payload's claimed resource, which is why the binding lives at
|
|
119
|
+
the framework layer.
|
|
120
|
+
|
|
121
|
+
All four enumerated attack classes are covered; see the table below.
|
|
122
|
+
|
|
123
|
+
## Design principles
|
|
124
|
+
|
|
125
|
+
- **Zero runtime dependencies.** The core uses only the Web Platform `crypto` global
|
|
126
|
+
(`crypto.randomUUID`), present on Node 22+, Cloudflare Workers, and Deno, so the guard
|
|
127
|
+
runs in any modern runtime without a polyfill. Every dependency is attack surface; a
|
|
128
|
+
hardening library should have as little of it as possible. Independent signature
|
|
129
|
+
verification would need cryptographic primitives, so it stays out of the core path by
|
|
130
|
+
design; the facilitator verifies payments, and the guard hardens the flow around that.
|
|
131
|
+
- **Installs clean under npm v12's hardened defaults**: no lifecycle scripts, no
|
|
132
|
+
`npm approve-scripts` step, nothing to allow.
|
|
133
|
+
- **Small enough to read.** Source files are capped so the whole library can be
|
|
134
|
+
audited in an afternoon. Built with plain `tsc` so the published output maps
|
|
135
|
+
one-to-one to the source you can see.
|
|
136
|
+
- **Framework-agnostic core.** `protect` takes plain callbacks, so the same guard
|
|
137
|
+
drops into Hono, Express, Next, or Fastify. A Hono binding is shown in the
|
|
138
|
+
examples; an `@x402/core`-hook convenience wrapper is a thin layer over `protect`.
|
|
139
|
+
|
|
140
|
+
## Mitigations
|
|
141
|
+
|
|
142
|
+
Each ships with a paired test proving the attack against a vanilla server and
|
|
143
|
+
proving it blocked by the guard. Every class is mapped to its research, mechanism,
|
|
144
|
+
and proving test in [`docs/coverage-map.md`](./docs/coverage-map.md); the rationale
|
|
145
|
+
is in [`docs/hardening.md`](./docs/hardening.md). The hardest questions about scope
|
|
146
|
+
and honesty (is this a strawman, does the reference actually have these gaps, what
|
|
147
|
+
this does not do) are answered in
|
|
148
|
+
[`docs/objection-handling.md`](./docs/objection-handling.md).
|
|
149
|
+
|
|
150
|
+
| Attack class | Status |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| Duplicate-settlement race | done |
|
|
153
|
+
| Payment replay | done (same nonce reservation) |
|
|
154
|
+
| Cross-resource substitution | done (same nonce reservation, distinct reason) |
|
|
155
|
+
| Grant-before-finality (k-confirmations) | done |
|
|
156
|
+
| Cache leakage of paid content | done |
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
The local toolchain is pinned with [mise](https://mise.jdx.dev):
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
mise install # installs Node (Active LTS) + just from mise.toml
|
|
164
|
+
npm ci # installs the dev toolchain, exact-pinned
|
|
165
|
+
just # lists every repo command
|
|
166
|
+
just check # full local gate: typecheck + lint + file-length + tests
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Node 24 (Active LTS) is used locally; the published package supports Node ≥22,
|
|
170
|
+
and CI tests both. `npm run build` emits the package with plain `tsc`.
|
|
171
|
+
|
|
172
|
+
Dev tooling lives in `package.json` scripts (`npm run …`); the [`justfile`](./justfile)
|
|
173
|
+
is the discoverable index for repo operations that aren't npm: supply-chain
|
|
174
|
+
checks, CI, release prep. Run `just` to see them all.
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
[MIT](./LICENSE) © Craig Ruks
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Disclaimer
|
|
4
|
+
|
|
5
|
+
This software is provided **"AS IS"**, without warranty of any kind, express or
|
|
6
|
+
implied. It is **not audited** and is **not a security guarantee**. It mitigates
|
|
7
|
+
specific, enumerated attack classes against x402 payment endpoints. It cannot
|
|
8
|
+
make an insecure endpoint safe on its own, and it does not remove your
|
|
9
|
+
responsibility to secure your own payment path.
|
|
10
|
+
|
|
11
|
+
**The authors accept no liability for any loss of funds or damages** arising from
|
|
12
|
+
the use of this software. See the [LICENSE](./LICENSE) (MIT) for the full terms.
|
|
13
|
+
|
|
14
|
+
How this code is reviewed before merge, and the limits of that review, are described
|
|
15
|
+
in [docs/review.md](./docs/review.md).
|
|
16
|
+
|
|
17
|
+
An independent audit is welcome. If you review security code professionally, the
|
|
18
|
+
reporting channel below works for audit findings too; we would rather have this
|
|
19
|
+
checked than ask anyone to take our word for it.
|
|
20
|
+
|
|
21
|
+
## Scope
|
|
22
|
+
|
|
23
|
+
This library targets server-side ("resource server" / merchant) hardening for the
|
|
24
|
+
x402 protocol. It mitigates five listed line items across four distinct attack
|
|
25
|
+
classes, drawn from published research
|
|
26
|
+
([arXiv:2605.11781](https://arxiv.org/abs/2605.11781),
|
|
27
|
+
[arXiv:2605.30998](https://arxiv.org/abs/2605.30998)):
|
|
28
|
+
the duplicate-settlement race, payment replay, cross-resource substitution,
|
|
29
|
+
grant-before-finality, and cache leakage of paid content. Each is mapped to its
|
|
30
|
+
mechanism and the test that proves it in
|
|
31
|
+
[docs/coverage-map.md](./docs/coverage-map.md), with the rationale in
|
|
32
|
+
[docs/hardening.md](./docs/hardening.md). Anything not listed is out of scope and
|
|
33
|
+
should not be assumed covered.
|
|
34
|
+
|
|
35
|
+
## Supported versions
|
|
36
|
+
|
|
37
|
+
Pre-1.0: only the latest published version receives security fixes. The `latest`
|
|
38
|
+
dist-tag will never point at code that has not passed review; unreviewed cuts are
|
|
39
|
+
published under a separate tag.
|
|
40
|
+
|
|
41
|
+
| Version | Supported |
|
|
42
|
+
| ------- | --------- |
|
|
43
|
+
| latest | ✅ |
|
|
44
|
+
| < latest | ❌ |
|
|
45
|
+
|
|
46
|
+
## Reporting a vulnerability
|
|
47
|
+
|
|
48
|
+
Please report suspected vulnerabilities **privately** via GitHub Security
|
|
49
|
+
Advisories ("Report a vulnerability" on the repository's Security tab), not
|
|
50
|
+
through public issues.
|
|
51
|
+
|
|
52
|
+
Include a description, affected version, and a reproduction if possible. Expect an
|
|
53
|
+
acknowledgement within 72 hours. Please allow a reasonable window for a fix before
|
|
54
|
+
public disclosure.
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache directives for paid responses.
|
|
3
|
+
*
|
|
4
|
+
* A shared cache (CDN or reverse proxy) keys on the URL and knows nothing about
|
|
5
|
+
* payment, so a cacheable paid 200 can be stored and served to the next caller, paid
|
|
6
|
+
* or not. The reference x402 adapters set no cache directive, leaving that door open.
|
|
7
|
+
*
|
|
8
|
+
* The mitigation is a response directive, not a decision about a nonce, so it lives
|
|
9
|
+
* here as a small pure helper the server or adapter applies to every paid response.
|
|
10
|
+
* `no-store` is load-bearing (no conforming cache stores it); `private` and `Vary` on
|
|
11
|
+
* the payment header are defense in depth.
|
|
12
|
+
*/
|
|
13
|
+
/** HTTP header directives to attach to a paid response so no cache serves it onward. */
|
|
14
|
+
export interface CacheDirectives {
|
|
15
|
+
/** `Cache-Control` value. */
|
|
16
|
+
readonly cacheControl: string;
|
|
17
|
+
/** `Vary` value: the request headers a stored response must key on. */
|
|
18
|
+
readonly vary: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CacheDirectivesOptions {
|
|
21
|
+
/**
|
|
22
|
+
* The request header the payment travels in, added to `Vary` so a cache that
|
|
23
|
+
* stores despite `no-store` still keys by payer. Defaults to `X-PAYMENT`.
|
|
24
|
+
*/
|
|
25
|
+
readonly paymentHeader?: string;
|
|
26
|
+
}
|
|
27
|
+
/** The cache directives to attach to a paid (200) response. */
|
|
28
|
+
export declare function paidResponseCacheDirectives(options?: CacheDirectivesOptions): CacheDirectives;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a shared (CDN / reverse-proxy) cache may store a response with this
|
|
31
|
+
* `Cache-Control`. A conforming shared cache refuses to store a `no-store` or
|
|
32
|
+
* `private` response. Used by the caching model in the tests. A CDN in force-cache
|
|
33
|
+
* mode ignores `Cache-Control`; this models a cache that honors it.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isStorableBySharedCache(cacheControl: string | undefined): boolean;
|
|
36
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,wFAAwF;AACxF,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,+DAA+D;AAC/D,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAGjG;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMjF"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache directives for paid responses.
|
|
3
|
+
*
|
|
4
|
+
* A shared cache (CDN or reverse proxy) keys on the URL and knows nothing about
|
|
5
|
+
* payment, so a cacheable paid 200 can be stored and served to the next caller, paid
|
|
6
|
+
* or not. The reference x402 adapters set no cache directive, leaving that door open.
|
|
7
|
+
*
|
|
8
|
+
* The mitigation is a response directive, not a decision about a nonce, so it lives
|
|
9
|
+
* here as a small pure helper the server or adapter applies to every paid response.
|
|
10
|
+
* `no-store` is load-bearing (no conforming cache stores it); `private` and `Vary` on
|
|
11
|
+
* the payment header are defense in depth.
|
|
12
|
+
*/
|
|
13
|
+
/** The cache directives to attach to a paid (200) response. */
|
|
14
|
+
export function paidResponseCacheDirectives(options = {}) {
|
|
15
|
+
const paymentHeader = options.paymentHeader ?? "X-PAYMENT";
|
|
16
|
+
return { cacheControl: "no-store, private", vary: paymentHeader };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether a shared (CDN / reverse-proxy) cache may store a response with this
|
|
20
|
+
* `Cache-Control`. A conforming shared cache refuses to store a `no-store` or
|
|
21
|
+
* `private` response. Used by the caching model in the tests. A CDN in force-cache
|
|
22
|
+
* mode ignores `Cache-Control`; this models a cache that honors it.
|
|
23
|
+
*/
|
|
24
|
+
export function isStorableBySharedCache(cacheControl) {
|
|
25
|
+
if (cacheControl === undefined || cacheControl === "") {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const normalized = cacheControl.toLowerCase();
|
|
29
|
+
return !normalized.includes("no-store") && !normalized.includes("private");
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAkBH,+DAA+D;AAC/D,MAAM,UAAU,2BAA2B,CAAC,OAAO,GAA2B,EAAE;IAC9E,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,WAAW,CAAC;IAC3D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;AACpE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,YAAgC;IACtE,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,EAAE,EAAE,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,UAAU,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;IAC9C,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical keys for the nonce and the resource.
|
|
3
|
+
*
|
|
4
|
+
* The guard uses the nonce and resource strings directly as map keys. If one nonce (or
|
|
5
|
+
* resource) reaches the guard in two encodings, the two forms become two keys for one
|
|
6
|
+
* payment: the string-encoding cousin of signature malleability. `createGuard` folds
|
|
7
|
+
* these encodings by default; a caller with a case- or prefix-sensitive scope can
|
|
8
|
+
* override or opt out (see GuardOptions).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Canonicalize a payment nonce: lowercase it and drop a leading `0x`, so `0xABCD`,
|
|
12
|
+
* `0xabcd`, and `abcd` are one key. Hex case and the `0x` prefix are not part of the
|
|
13
|
+
* value (the same bytes either way), so folding them cannot merge two distinct nonces.
|
|
14
|
+
* A caller whose nonce scope is case- or prefix-sensitive should pass their own function.
|
|
15
|
+
*/
|
|
16
|
+
export declare function canonicalNonce(nonce: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Canonicalize a resource key. When it is an absolute URL, return the WHATWG-URL
|
|
19
|
+
* canonical form: scheme and host case-folded, the default port dropped, and
|
|
20
|
+
* dot-segments resolved. It does NOT decode percent-encoded octets (`%41` stays
|
|
21
|
+
* `%41`, not `A`), so this is not full RFC 3986 equivalence; two RFC-equivalent paths
|
|
22
|
+
* that differ only in percent-encoding get different keys. Path, query, and fragment
|
|
23
|
+
* CASE is preserved, so `/Foo` and `/foo` stay distinct and merging cannot open a
|
|
24
|
+
* substitution hole; normalize trailing slashes and query order yourself if a resource
|
|
25
|
+
* needs it. A non-URL key (a bare path or an opaque string) is returned unchanged.
|
|
26
|
+
* Note: the URL parser's IDN/punycode output can vary with the runtime's ICU, so a
|
|
27
|
+
* persistent distributed store should pin a stable resource form rather than rely on
|
|
28
|
+
* this across engine upgrades.
|
|
29
|
+
*/
|
|
30
|
+
export declare function canonicalResource(resource: string): string;
|
|
31
|
+
//# sourceMappingURL=canonical.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG1D"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical keys for the nonce and the resource.
|
|
3
|
+
*
|
|
4
|
+
* The guard uses the nonce and resource strings directly as map keys. If one nonce (or
|
|
5
|
+
* resource) reaches the guard in two encodings, the two forms become two keys for one
|
|
6
|
+
* payment: the string-encoding cousin of signature malleability. `createGuard` folds
|
|
7
|
+
* these encodings by default; a caller with a case- or prefix-sensitive scope can
|
|
8
|
+
* override or opt out (see GuardOptions).
|
|
9
|
+
*/
|
|
10
|
+
import { tryCatch } from "./result.js";
|
|
11
|
+
/**
|
|
12
|
+
* Canonicalize a payment nonce: lowercase it and drop a leading `0x`, so `0xABCD`,
|
|
13
|
+
* `0xabcd`, and `abcd` are one key. Hex case and the `0x` prefix are not part of the
|
|
14
|
+
* value (the same bytes either way), so folding them cannot merge two distinct nonces.
|
|
15
|
+
* A caller whose nonce scope is case- or prefix-sensitive should pass their own function.
|
|
16
|
+
*/
|
|
17
|
+
export function canonicalNonce(nonce) {
|
|
18
|
+
const lower = nonce.toLowerCase();
|
|
19
|
+
return lower.startsWith("0x") ? lower.slice(2) : lower;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Canonicalize a resource key. When it is an absolute URL, return the WHATWG-URL
|
|
23
|
+
* canonical form: scheme and host case-folded, the default port dropped, and
|
|
24
|
+
* dot-segments resolved. It does NOT decode percent-encoded octets (`%41` stays
|
|
25
|
+
* `%41`, not `A`), so this is not full RFC 3986 equivalence; two RFC-equivalent paths
|
|
26
|
+
* that differ only in percent-encoding get different keys. Path, query, and fragment
|
|
27
|
+
* CASE is preserved, so `/Foo` and `/foo` stay distinct and merging cannot open a
|
|
28
|
+
* substitution hole; normalize trailing slashes and query order yourself if a resource
|
|
29
|
+
* needs it. A non-URL key (a bare path or an opaque string) is returned unchanged.
|
|
30
|
+
* Note: the URL parser's IDN/punycode output can vary with the runtime's ICU, so a
|
|
31
|
+
* persistent distributed store should pin a stable resource form rather than rely on
|
|
32
|
+
* this across engine upgrades.
|
|
33
|
+
*/
|
|
34
|
+
export function canonicalResource(resource) {
|
|
35
|
+
const parsed = tryCatch(() => new URL(resource));
|
|
36
|
+
return parsed.ok ? parsed.value.href : resource;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=canonical.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.js","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAClC,OAAO,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACzD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;IACjD,OAAO,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Durable Object nonce store: atomic compare-and-set across isolates.
|
|
3
|
+
*
|
|
4
|
+
* The default in-memory store protects one process; on Workers each isolate has its
|
|
5
|
+
* own Map, so N isolates would each grant one nonce N times. This routes every nonce
|
|
6
|
+
* to its own Durable Object (`idFromName(nonce)`). A Durable Object serves one request
|
|
7
|
+
* at a time and holds delivery of other events while a storage op to the same object
|
|
8
|
+
* is in flight (input gating), so the reserve check-and-set runs atomically with no
|
|
9
|
+
* lock. That is the compare-and-set the store contract requires. See the Cloudflare
|
|
10
|
+
* store docs for the wrangler binding and deployment shape.
|
|
11
|
+
*/
|
|
12
|
+
import { DurableObject } from "cloudflare:workers";
|
|
13
|
+
import type { NonceStore, ReleaseOutcome, ReserveOutcome } from "../store-types.js";
|
|
14
|
+
/**
|
|
15
|
+
* The Durable Object the guard reserves against. Bind it in wrangler and export it
|
|
16
|
+
* from your Worker entry; the adapter routes each nonce to its own instance, so the
|
|
17
|
+
* single stored entry per object is the reservation for that one nonce.
|
|
18
|
+
*/
|
|
19
|
+
export declare class NonceReservationDO extends DurableObject {
|
|
20
|
+
/** Atomic reserve: input gating serializes concurrent calls to this same object. */
|
|
21
|
+
reserve(resource: string, expiresAt: number): Promise<ReserveOutcome>;
|
|
22
|
+
/** Fenced release: free the nonce only for the holder of the matching token. */
|
|
23
|
+
release(token: string): Promise<ReleaseOutcome>;
|
|
24
|
+
/** Cleanup: drop the entry once its window has closed. */
|
|
25
|
+
alarm(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
/** An opaque Durable Object id. A real `DurableObjectId` satisfies this. */
|
|
28
|
+
interface NonceReservationId {
|
|
29
|
+
toString(): string;
|
|
30
|
+
}
|
|
31
|
+
/** The RPC surface the adapter calls on a {@link NonceReservationDO} stub. */
|
|
32
|
+
interface NonceReservationStub {
|
|
33
|
+
reserve(resource: string, expiresAt: number): Promise<ReserveOutcome>;
|
|
34
|
+
release(token: string): Promise<ReleaseOutcome>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The Durable Object namespace binding the adapter needs. A real
|
|
38
|
+
* `DurableObjectNamespace<NonceReservationDO>` satisfies this structurally; type your
|
|
39
|
+
* binding with that generic so the stub carries the reserve/release RPC methods.
|
|
40
|
+
*/
|
|
41
|
+
export interface NonceReservationNamespace {
|
|
42
|
+
idFromName(name: string): NonceReservationId;
|
|
43
|
+
get(id: NonceReservationId): NonceReservationStub;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build a {@link NonceStore} backed by the Durable Object namespace. Pass it to
|
|
47
|
+
* `createGuard({ store })` on any multi-isolate deploy. A transport failure (the object
|
|
48
|
+
* is unreachable) becomes a `store-unavailable` value, so the guard fails closed.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createDurableObjectNonceStore(namespace: NonceReservationNamespace): NonceStore;
|
|
51
|
+
export {};
|
|
52
|
+
//# sourceMappingURL=durable-object.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-object.d.ts","sourceRoot":"","sources":["../../src/cloudflare/durable-object.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EAEd,cAAc,EAGf,MAAM,mBAAmB,CAAC;AAU3B;;;;GAIG;AACH,qBAAa,kBAAmB,SAAQ,aAAa;IACnD,oFAAoF;IAC9E,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAiB1E;IAED,gFAAgF;IAC1E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAOpD;IAED,0DAA0D;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAKpC;CACF;AAMD,4EAA4E;AAC5E,UAAU,kBAAkB;IAC1B,QAAQ,IAAI,MAAM,CAAC;CACpB;AAED,8EAA8E;AAC9E,UAAU,oBAAoB;IAC5B,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACtE,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACjD;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAAC;IAC7C,GAAG,CAAC,EAAE,EAAE,kBAAkB,GAAG,oBAAoB,CAAC;CACnD;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,yBAAyB,GAAG,UAAU,CAqB9F"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Durable Object nonce store: atomic compare-and-set across isolates.
|
|
3
|
+
*
|
|
4
|
+
* The default in-memory store protects one process; on Workers each isolate has its
|
|
5
|
+
* own Map, so N isolates would each grant one nonce N times. This routes every nonce
|
|
6
|
+
* to its own Durable Object (`idFromName(nonce)`). A Durable Object serves one request
|
|
7
|
+
* at a time and holds delivery of other events while a storage op to the same object
|
|
8
|
+
* is in flight (input gating), so the reserve check-and-set runs atomically with no
|
|
9
|
+
* lock. That is the compare-and-set the store contract requires. See the Cloudflare
|
|
10
|
+
* store docs for the wrangler binding and deployment shape.
|
|
11
|
+
*/
|
|
12
|
+
import { DurableObject } from "cloudflare:workers";
|
|
13
|
+
import { guardError } from "../error.js";
|
|
14
|
+
import { err, ok, tryCatchAsync } from "../result.js";
|
|
15
|
+
const ENTRY_KEY = "entry";
|
|
16
|
+
/**
|
|
17
|
+
* The Durable Object the guard reserves against. Bind it in wrangler and export it
|
|
18
|
+
* from your Worker entry; the adapter routes each nonce to its own instance, so the
|
|
19
|
+
* single stored entry per object is the reservation for that one nonce.
|
|
20
|
+
*/
|
|
21
|
+
export class NonceReservationDO extends DurableObject {
|
|
22
|
+
/** Atomic reserve: input gating serializes concurrent calls to this same object. */
|
|
23
|
+
async reserve(resource, expiresAt) {
|
|
24
|
+
const now = nowSeconds();
|
|
25
|
+
// Refuse an already-closed window before touching storage (matches the memory store).
|
|
26
|
+
if (expiresAt <= now) {
|
|
27
|
+
return { status: "expired" };
|
|
28
|
+
}
|
|
29
|
+
const existing = await this.ctx.storage.get(ENTRY_KEY);
|
|
30
|
+
// No `await` on anything but storage between the get and the put, so input gating
|
|
31
|
+
// keeps this read-modify-write atomic against a concurrent reserve of the same nonce.
|
|
32
|
+
if (existing !== undefined && existing.expiresAt > now) {
|
|
33
|
+
return { status: "already-reserved", boundResource: existing.resource };
|
|
34
|
+
}
|
|
35
|
+
const token = crypto.randomUUID();
|
|
36
|
+
await this.ctx.storage.put(ENTRY_KEY, { resource, expiresAt, token });
|
|
37
|
+
// Evict once the window closes: past `validBefore` the nonce is unreplayable on-chain.
|
|
38
|
+
await this.ctx.storage.setAlarm(expiresAt * 1000);
|
|
39
|
+
return { status: "reserved", token };
|
|
40
|
+
}
|
|
41
|
+
/** Fenced release: free the nonce only for the holder of the matching token. */
|
|
42
|
+
async release(token) {
|
|
43
|
+
const entry = await this.ctx.storage.get(ENTRY_KEY);
|
|
44
|
+
if (entry === undefined || entry.token !== token) {
|
|
45
|
+
return { status: "not-held" };
|
|
46
|
+
}
|
|
47
|
+
await this.ctx.storage.deleteAll();
|
|
48
|
+
return { status: "released" };
|
|
49
|
+
}
|
|
50
|
+
/** Cleanup: drop the entry once its window has closed. */
|
|
51
|
+
async alarm() {
|
|
52
|
+
const entry = await this.ctx.storage.get(ENTRY_KEY);
|
|
53
|
+
if (entry !== undefined && entry.expiresAt <= nowSeconds()) {
|
|
54
|
+
await this.ctx.storage.deleteAll();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function nowSeconds() {
|
|
59
|
+
return Math.floor(Date.now() / 1000);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Build a {@link NonceStore} backed by the Durable Object namespace. Pass it to
|
|
63
|
+
* `createGuard({ store })` on any multi-isolate deploy. A transport failure (the object
|
|
64
|
+
* is unreachable) becomes a `store-unavailable` value, so the guard fails closed.
|
|
65
|
+
*/
|
|
66
|
+
export function createDurableObjectNonceStore(namespace) {
|
|
67
|
+
const stubFor = (nonce) => namespace.get(namespace.idFromName(nonce));
|
|
68
|
+
return {
|
|
69
|
+
async reserve({ nonce, resource, expiresAt, }) {
|
|
70
|
+
const outcome = await tryCatchAsync(() => stubFor(nonce).reserve(resource, expiresAt));
|
|
71
|
+
return outcome.ok
|
|
72
|
+
? ok(outcome.value)
|
|
73
|
+
: err(guardError("store-unavailable", "durable object reserve failed", outcome.error));
|
|
74
|
+
},
|
|
75
|
+
async release(nonce, token) {
|
|
76
|
+
const outcome = await tryCatchAsync(() => stubFor(nonce).release(token));
|
|
77
|
+
return outcome.ok
|
|
78
|
+
? ok(outcome.value)
|
|
79
|
+
: err(guardError("store-unavailable", "durable object release failed", outcome.error));
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=durable-object.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-object.js","sourceRoot":"","sources":["../../src/cloudflare/durable-object.ts"],"names":[],"mappings":"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"}
|
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors: viem's error taste, adapted to errors-as-values.
|
|
3
|
+
*
|
|
4
|
+
* viem gives every failure a structured error you branch on by identity rather than by
|
|
5
|
+
* string-matching a message. We want that discipline, but viem THROWS and we carry
|
|
6
|
+
* errors as values (result.ts), so a `GuardError` is a plain typed object, not a class:
|
|
7
|
+
* it serializes, needs no `instanceof`, and drops into a discriminated union or a
|
|
8
|
+
* `Result`'s error slot. The load-bearing field is `code`, a stable branchable
|
|
9
|
+
* discriminant; `cause` preserves the original throw so wrapping never loses the chain.
|
|
10
|
+
*/
|
|
11
|
+
/** A typed error carried as a value. Branch on `code`, not on `message`. */
|
|
12
|
+
export interface GuardError<Code extends string = string> {
|
|
13
|
+
/** Stable, branchable discriminant. */
|
|
14
|
+
readonly code: Code;
|
|
15
|
+
/** Human-readable detail, for logs and messages, not for control flow. */
|
|
16
|
+
readonly message: string;
|
|
17
|
+
/** The originating error, when this wraps a throw. Preserves the chain. */
|
|
18
|
+
readonly cause?: unknown;
|
|
19
|
+
}
|
|
20
|
+
/** Construct a typed `GuardError`, omitting `cause` when there is none. */
|
|
21
|
+
export declare function guardError<const Code extends string>(code: Code, message: string, cause?: unknown): GuardError<Code>;
|
|
22
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,4EAA4E;AAC5E,MAAM,WAAW,UAAU,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACtD,uCAAuC;IACvC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAE3E,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,2EAA2E;AAC3E,wBAAgB,UAAU,CAAC,KAAK,CAAC,IAAI,SAAS,MAAM,EAClD,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EAEf,KAAK,CAAC,EAAE,OAAO,GACd,UAAU,CAAC,IAAI,CAAC,CAElB"}
|
package/dist/error.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors: viem's error taste, adapted to errors-as-values.
|
|
3
|
+
*
|
|
4
|
+
* viem gives every failure a structured error you branch on by identity rather than by
|
|
5
|
+
* string-matching a message. We want that discipline, but viem THROWS and we carry
|
|
6
|
+
* errors as values (result.ts), so a `GuardError` is a plain typed object, not a class:
|
|
7
|
+
* it serializes, needs no `instanceof`, and drops into a discriminated union or a
|
|
8
|
+
* `Result`'s error slot. The load-bearing field is `code`, a stable branchable
|
|
9
|
+
* discriminant; `cause` preserves the original throw so wrapping never loses the chain.
|
|
10
|
+
*/
|
|
11
|
+
/** Construct a typed `GuardError`, omitting `cause` when there is none. */
|
|
12
|
+
export function guardError(code, message,
|
|
13
|
+
// biome-ignore lint/plugin: a wrapped cause can be any thrown value
|
|
14
|
+
cause) {
|
|
15
|
+
return cause === undefined ? { code, message } : { code, message, cause };
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAaH,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CACxB,IAAU,EACV,OAAe;AACf,oEAAoE;AACpE,KAAe;IAEf,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;AAC5E,CAAC"}
|