@crvouga/mockingbird-service-stripe 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/README.md +254 -0
- package/dist/account.d.ts +8 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +13 -0
- package/dist/account.js.map +1 -0
- package/dist/charges.d.ts +4 -0
- package/dist/charges.d.ts.map +1 -0
- package/dist/charges.js +89 -0
- package/dist/charges.js.map +1 -0
- package/dist/checkout-sessions.d.ts +5 -0
- package/dist/checkout-sessions.d.ts.map +1 -0
- package/dist/checkout-sessions.js +303 -0
- package/dist/checkout-sessions.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +3 -0
- package/dist/constants.js.map +1 -0
- package/dist/coupons.d.ts +4 -0
- package/dist/coupons.d.ts.map +1 -0
- package/dist/coupons.js +158 -0
- package/dist/coupons.js.map +1 -0
- package/dist/customers.d.ts +4 -0
- package/dist/customers.d.ts.map +1 -0
- package/dist/customers.js +218 -0
- package/dist/customers.js.map +1 -0
- package/dist/disputes.d.ts +4 -0
- package/dist/disputes.d.ts.map +1 -0
- package/dist/disputes.js +36 -0
- package/dist/disputes.js.map +1 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +74 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +4 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +76 -0
- package/dist/events.js.map +1 -0
- package/dist/expand.d.ts +11 -0
- package/dist/expand.d.ts.map +1 -0
- package/dist/expand.js +45 -0
- package/dist/expand.js.map +1 -0
- package/dist/fields.d.ts +25 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/fields.js +117 -0
- package/dist/fields.js.map +1 -0
- package/dist/generated/openapi.d.ts +7 -0
- package/dist/generated/openapi.d.ts.map +1 -0
- package/dist/generated/openapi.js +5 -0
- package/dist/generated/openapi.js.map +1 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +172 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +134 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +460 -0
- package/dist/internal.js.map +1 -0
- package/dist/invoice-items.d.ts +4 -0
- package/dist/invoice-items.d.ts.map +1 -0
- package/dist/invoice-items.js +179 -0
- package/dist/invoice-items.js.map +1 -0
- package/dist/invoices.d.ts +4 -0
- package/dist/invoices.d.ts.map +1 -0
- package/dist/invoices.js +312 -0
- package/dist/invoices.js.map +1 -0
- package/dist/list.d.ts +24 -0
- package/dist/list.d.ts.map +1 -0
- package/dist/list.js +68 -0
- package/dist/list.js.map +1 -0
- package/dist/params.d.ts +29 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/params.js +165 -0
- package/dist/params.js.map +1 -0
- package/dist/payment-intents.d.ts +4 -0
- package/dist/payment-intents.d.ts.map +1 -0
- package/dist/payment-intents.js +232 -0
- package/dist/payment-intents.js.map +1 -0
- package/dist/payment-methods.d.ts +4 -0
- package/dist/payment-methods.d.ts.map +1 -0
- package/dist/payment-methods.js +99 -0
- package/dist/payment-methods.js.map +1 -0
- package/dist/prices.d.ts +4 -0
- package/dist/prices.d.ts.map +1 -0
- package/dist/prices.js +176 -0
- package/dist/prices.js.map +1 -0
- package/dist/products.d.ts +6 -0
- package/dist/products.d.ts.map +1 -0
- package/dist/products.js +202 -0
- package/dist/products.js.map +1 -0
- package/dist/promotion-codes.d.ts +4 -0
- package/dist/promotion-codes.d.ts.map +1 -0
- package/dist/promotion-codes.js +193 -0
- package/dist/promotion-codes.js.map +1 -0
- package/dist/qa-corpus.d.ts +26 -0
- package/dist/qa-corpus.d.ts.map +1 -0
- package/dist/qa-corpus.js +42 -0
- package/dist/qa-corpus.js.map +1 -0
- package/dist/refunds.d.ts +4 -0
- package/dist/refunds.d.ts.map +1 -0
- package/dist/refunds.js +125 -0
- package/dist/refunds.js.map +1 -0
- package/dist/render.d.ts +60 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +426 -0
- package/dist/render.js.map +1 -0
- package/dist/reshape-qa.d.ts +8 -0
- package/dist/reshape-qa.d.ts.map +1 -0
- package/dist/reshape-qa.js +102 -0
- package/dist/reshape-qa.js.map +1 -0
- package/dist/search.d.ts +32 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +64 -0
- package/dist/search.js.map +1 -0
- package/dist/setup-intents.d.ts +4 -0
- package/dist/setup-intents.d.ts.map +1 -0
- package/dist/setup-intents.js +129 -0
- package/dist/setup-intents.js.map +1 -0
- package/dist/state.d.ts +485 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +143 -0
- package/dist/state.js.map +1 -0
- package/dist/subscription-schedules.d.ts +4 -0
- package/dist/subscription-schedules.d.ts.map +1 -0
- package/dist/subscription-schedules.js +122 -0
- package/dist/subscription-schedules.js.map +1 -0
- package/dist/subscriptions.d.ts +23 -0
- package/dist/subscriptions.d.ts.map +1 -0
- package/dist/subscriptions.js +260 -0
- package/dist/subscriptions.js.map +1 -0
- package/dist/test-tokens.d.ts +27 -0
- package/dist/test-tokens.d.ts.map +1 -0
- package/dist/test-tokens.js +61 -0
- package/dist/test-tokens.js.map +1 -0
- package/dist/url.d.ts +6 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +38 -0
- package/dist/url.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# @crvouga/mockingbird-service-stripe
|
|
2
|
+
|
|
3
|
+
Stateful, in-process mock of the [Stripe API](https://docs.stripe.com/api) for test suites. It is
|
|
4
|
+
driven by a vendored subset of Stripe's OpenAPI contract and verified by differential property tests
|
|
5
|
+
against Stripe test mode. Covered: customers (including search and balance transactions), payment
|
|
6
|
+
methods, payment and setup intents, charges, refunds, disputes (read-only), checkout sessions,
|
|
7
|
+
invoices and invoice items, subscriptions and subscription schedules, coupons and promotion codes,
|
|
8
|
+
products, prices, and an event ledger (`/v1/events`) with a webhook hook.
|
|
9
|
+
|
|
10
|
+
Use it when server-side code talks to Stripe through `fetch` or stripe-node and you want the suite
|
|
11
|
+
to run offline with no `api.stripe.com` egress. It does not serve Stripe.js or hosted checkout
|
|
12
|
+
(`js.stripe.com`, `checkout.stripe.com`); browser-driven checkout still needs real Stripe test mode.
|
|
13
|
+
|
|
14
|
+
- Operation coverage (88 of 108 operations in the vendored spec, with reasons for each gap):
|
|
15
|
+
[SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/stripe/SUPPORT.md)
|
|
16
|
+
- Scope and proof: [stripe-drop-in.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/stripe/docs/stripe-drop-in.md)
|
|
17
|
+
· Consumer wiring checklist: [qa-followon.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/stripe/docs/qa-followon.md)
|
|
18
|
+
· [QA coverage](https://github.com/crvouga/mockingbird/blob/main/packages/service/stripe/docs/qa-coverage.md)
|
|
19
|
+
- Stripe API reference: https://docs.stripe.com/api · Upstream OpenAPI: https://github.com/stripe/openapi
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -D @crvouga/mockingbird-service-stripe
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
ESM only. Requires Node >= 22 or Bun >= 1.2. No native dependencies: state lives in an in-memory
|
|
28
|
+
SQLite engine (`@crvouga/mockingbird-service-sqlite`, pure TypeScript). To serve it over HTTP also
|
|
29
|
+
install an adapter: `@crvouga/mockingbird-adapter-node` or `@crvouga/mockingbird-adapter-bun`.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Behaviour the examples rely on (all from the source):
|
|
34
|
+
|
|
35
|
+
- **Any host works.** Routing uses only the path (`/v1/...`), so `https://api.stripe.com`,
|
|
36
|
+
`http://127.0.0.1:<port>` or any made-up origin is fine.
|
|
37
|
+
- **Auth is required.** Every request needs `Authorization: Bearer <key>` where the key matches
|
|
38
|
+
`^(sk|rk)_test_[A-Za-z0-9]+$`. Missing header or any other shape (including `sk_live_...` and
|
|
39
|
+
keys with extra underscores such as `sk_test_my_key`) returns Stripe's 401 error body.
|
|
40
|
+
- **One key = one account.** State is partitioned by bearer key, so an object created with
|
|
41
|
+
`sk_test_a` is `resource_missing` (404) under `sk_test_b`.
|
|
42
|
+
- Request bodies are `application/x-www-form-urlencoded` with Stripe's bracket notation, exactly
|
|
43
|
+
as stripe-node sends them. Every response carries `request-id` and `stripe-version` headers.
|
|
44
|
+
- `Idempotency-Key` on POSTs is honoured: a replay returns the cached response, a replay with
|
|
45
|
+
different parameters returns 400.
|
|
46
|
+
|
|
47
|
+
### In-process (inject `fetch`)
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { StripeAPI } from "@crvouga/mockingbird-service-stripe"
|
|
51
|
+
|
|
52
|
+
const stripe = new StripeAPI({ now: () => Date.UTC(2026, 0, 1) })
|
|
53
|
+
|
|
54
|
+
const auth = { authorization: "Bearer sk_test_mockingbird" }
|
|
55
|
+
|
|
56
|
+
const created = await stripe.fetch(
|
|
57
|
+
new Request("https://api.stripe.com/v1/customers", {
|
|
58
|
+
method: "POST",
|
|
59
|
+
headers: { ...auth, "content-type": "application/x-www-form-urlencoded" },
|
|
60
|
+
body: new URLSearchParams({ email: "qa@example.com", "metadata[userId]": "1001" }),
|
|
61
|
+
}),
|
|
62
|
+
)
|
|
63
|
+
const customer = (await created.json()) as { id: string; created: number }
|
|
64
|
+
console.log(created.status, customer.id) // 200 "cus_..."
|
|
65
|
+
|
|
66
|
+
// Any code that accepts a fetch function can be pointed at the mock:
|
|
67
|
+
const mockFetch = (input: string | URL | Request, init?: RequestInit) =>
|
|
68
|
+
stripe.fetch(new Request(input, init))
|
|
69
|
+
const listed = await mockFetch("https://api.stripe.com/v1/customers?limit=10", { headers: auth })
|
|
70
|
+
console.log(((await listed.json()) as { data: unknown[] }).data.length) // 1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`now` drives `created`-style fields (Stripe returns seconds; `now` returns milliseconds).
|
|
74
|
+
|
|
75
|
+
### Over HTTP
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { serve } from "@crvouga/mockingbird-adapter-bun"
|
|
79
|
+
import { StripeAPI } from "@crvouga/mockingbird-service-stripe"
|
|
80
|
+
|
|
81
|
+
const stripe = new StripeAPI()
|
|
82
|
+
const server = serve(stripe, { port: 0, hostname: "127.0.0.1" }) // port 0 = ephemeral
|
|
83
|
+
const baseUrl = `http://127.0.0.1:${server.port}`
|
|
84
|
+
|
|
85
|
+
const response = await fetch(`${baseUrl}/v1/products?limit=3`, {
|
|
86
|
+
headers: { authorization: "Bearer sk_test_mockingbird" },
|
|
87
|
+
})
|
|
88
|
+
console.log(response.status) // 200
|
|
89
|
+
|
|
90
|
+
server.stop()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
On Node use `@crvouga/mockingbird-adapter-node`, whose `serve` is async and returns a
|
|
94
|
+
`node:http` server:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { serve } from "@crvouga/mockingbird-adapter-node"
|
|
98
|
+
const server = await serve(stripe, { port: 0, host: "127.0.0.1" })
|
|
99
|
+
const { port } = server.address()
|
|
100
|
+
// ... later: server.close()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Pointing stripe-node at it
|
|
104
|
+
|
|
105
|
+
stripe-node accepts `host`, `port` and `protocol`. This is the construction the package's own
|
|
106
|
+
client smoke test uses (stripe 16.x, `apiVersion: "2024-06-20"`):
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import Stripe from "stripe"
|
|
110
|
+
|
|
111
|
+
const client = new Stripe("sk_test_mockingbird", {
|
|
112
|
+
apiVersion: "2024-06-20",
|
|
113
|
+
host: "127.0.0.1",
|
|
114
|
+
port: server.port, // from serve() above
|
|
115
|
+
protocol: "http",
|
|
116
|
+
})
|
|
117
|
+
await client.customers.create({ email: "qa@example.com" })
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Add a base-URL override (e.g. `STRIPE_API_BASE_URL=http://127.0.0.1:12111`) at every place your app
|
|
121
|
+
constructs a Stripe client; a client built with `new Stripe(key)` and no options cannot be
|
|
122
|
+
redirected. Test payment methods and tokens such as `pm_card_visa`, `pm_card_authenticationRequired`
|
|
123
|
+
and `tok_chargeDeclinedInsufficientFunds` behave like their Stripe counterparts
|
|
124
|
+
(`QA_TEST_PAYMENT_METHODS` and `QA_TEST_CARD_TOKENS` list the ones the suites exercise).
|
|
125
|
+
|
|
126
|
+
### Webhooks
|
|
127
|
+
|
|
128
|
+
The mock records an event for every state change (readable through `GET /v1/events` and
|
|
129
|
+
`webhookEvents()`), and calls `onWebhook` with each one. Delivery and signing are up to you;
|
|
130
|
+
Stripe signs `"<t>.<body>"` with HMAC-SHA256 keyed by the `whsec_` secret verbatim, which
|
|
131
|
+
`stripe.webhooks.constructEvent` accepts:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { createHmac } from "node:crypto"
|
|
135
|
+
import { accountOfKey, StripeAPI } from "@crvouga/mockingbird-service-stripe"
|
|
136
|
+
|
|
137
|
+
const WEBHOOK_URL = "http://127.0.0.1:3100/webhooks/stripe"
|
|
138
|
+
const WEBHOOK_SECRET = "whsec_local_test"
|
|
139
|
+
const account = accountOfKey("sk_test_mockingbird")
|
|
140
|
+
|
|
141
|
+
const stripe = new StripeAPI({
|
|
142
|
+
onWebhook: (event) => {
|
|
143
|
+
if (event.account !== account) return
|
|
144
|
+
const t = Math.floor(Date.now() / 1000)
|
|
145
|
+
const v1 = createHmac("sha256", WEBHOOK_SECRET).update(`${t}.${event.body}`).digest("hex")
|
|
146
|
+
void fetch(WEBHOOK_URL, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "content-type": "application/json", "stripe-signature": `t=${t},v1=${v1}` },
|
|
149
|
+
body: event.body,
|
|
150
|
+
}).catch(() => undefined)
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Or assert on recorded events directly:
|
|
155
|
+
console.log(stripe.webhookEvents(account).map((event) => event.type))
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Resetting between tests
|
|
159
|
+
|
|
160
|
+
`reset()` clears every account's objects, the event ledger and the idempotency cache. Create one
|
|
161
|
+
instance per suite and reset it in `beforeEach`:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { beforeEach, expect, test } from "bun:test"
|
|
165
|
+
import { StripeAPI } from "@crvouga/mockingbird-service-stripe"
|
|
166
|
+
|
|
167
|
+
const stripe = new StripeAPI()
|
|
168
|
+
beforeEach(() => stripe.reset())
|
|
169
|
+
|
|
170
|
+
test("starts empty", async () => {
|
|
171
|
+
const response = await stripe.fetch(
|
|
172
|
+
new Request("https://api.stripe.com/v1/customers", {
|
|
173
|
+
headers: { authorization: "Bearer sk_test_mockingbird" },
|
|
174
|
+
}),
|
|
175
|
+
)
|
|
176
|
+
expect(((await response.json()) as { data: unknown[] }).data).toEqual([])
|
|
177
|
+
})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## API
|
|
181
|
+
|
|
182
|
+
`StripeAPI` is the main export; the rest supports account scoping, contract introspection and the
|
|
183
|
+
QA corpus used by the parity suites.
|
|
184
|
+
|
|
185
|
+
| Export | Description |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| `StripeAPI` | Class. `new StripeAPI(options?)`; implements `FetchAPI` (`fetch(request: Request): Promise<Response>`). |
|
|
188
|
+
| `accountOfKey` | `(key: string) => string` — the opaque `acct_...` partition id for an API key (use it to filter `webhookEvents`). |
|
|
189
|
+
| `accountOf` | `(request: Request) => string` — the partition id for a request's bearer key. |
|
|
190
|
+
| `STRIPE_NAMESPACE` | `"stripe"` — SQLite namespace holding every Stripe record when sharing a `sqlite` client. |
|
|
191
|
+
| `document` | The vendored Stripe OpenAPI document (Mockingbird subset) that drives routing and validation. |
|
|
192
|
+
| `operationIds` | Every `operationId` in `document` (108). |
|
|
193
|
+
| `supportedOperationIds` | The `operationId`s the mock implements (88); the rest return a Stripe-shaped error. |
|
|
194
|
+
| `QA_SURFACE_OPS` | Operations the QA suites exercise (same set as `supportedOperationIds`). |
|
|
195
|
+
| `QA_METADATA` | Pinned metadata values (`intent`, `source`, `userId`) the suites send. |
|
|
196
|
+
| `QA_AMOUNTS` | Pinned amounts in cents: `1000`, `15000`, `17999`. |
|
|
197
|
+
| `QA_CUSTOMER` | Pinned customer `email`, `name`, `phone`. |
|
|
198
|
+
| `QA_TEST_PAYMENT_METHODS` | Test payment method ids the suites attach (`pm_card_visa`, ...). |
|
|
199
|
+
| `QA_TEST_CARD_TOKENS` | Test card tokens the suites use (`tok_visa`, decline tokens, ...). |
|
|
200
|
+
| `QA_SEARCH_QUERIES` | Search queries the suites issue against `/v1/customers/search`. |
|
|
201
|
+
| `QA_COUPON_CODES` | Coupon / promotion codes used by the coupon flows. |
|
|
202
|
+
| `reshapeQaCommand` | Parity-walk hook that pins sampled commands onto QA corpus values (for `@crvouga/mockingbird-parity`). |
|
|
203
|
+
|
|
204
|
+
`StripeAPI` members:
|
|
205
|
+
|
|
206
|
+
| Member | Description |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| `fetch(request)` | Handle one Stripe REST request. |
|
|
209
|
+
| `reset()` | `Promise<void>` — clear all state, events and cached idempotent responses. |
|
|
210
|
+
| `webhookEvents(account?)` | `StripeWebhookEvent[]`, oldest first; `account` narrows to one `accountOfKey(...)` partition. |
|
|
211
|
+
| `webhookDeliveryAttempts(account?)` | Delivery attempts recorded for the event ledger. |
|
|
212
|
+
| `importStateFrom(source)` | Adopt another `StripeAPI` instance's state (used by seeded parity walks). |
|
|
213
|
+
| `app` | The underlying Hono app. |
|
|
214
|
+
| `sqlite` | The `SqliteClient` holding state. |
|
|
215
|
+
|
|
216
|
+
Options and types:
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
type StripeAPIOptions = {
|
|
220
|
+
sqlite?: SqliteClient // share one client across services; default: fresh in-memory DB
|
|
221
|
+
now?: () => number // clock in ms for created-style fields; default Date.now
|
|
222
|
+
onWebhook?: WebhookPublisher // called with every event the mock records
|
|
223
|
+
}
|
|
224
|
+
type StripeWebhookEvent = { type: string; account: string; body: string } // body is the JSON event
|
|
225
|
+
type WebhookPublisher = (event: StripeWebhookEvent) => void
|
|
226
|
+
type OperationId / SupportedOperationId // string unions of operationIds / supportedOperationIds
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`SqliteClient` is the port from `@crvouga/mockingbird-sqlite`; `Database` from
|
|
230
|
+
`@crvouga/mockingbird-service-sqlite` satisfies it, as do better-sqlite3 and wrapped `bun:sqlite`.
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
For contributors to the mockingbird repo only; these scripts are not shipped in the npm package.
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
bun test # self-parity, auth/idempotency and namespace suites (offline)
|
|
238
|
+
bun run mock:server # serve over HTTP on PORT (default 12111), GET /health for readiness
|
|
239
|
+
bun run client-parity # stripe-node smoke proof, including webhook signature verification
|
|
240
|
+
bun run parity # live differential parity against Stripe test mode
|
|
241
|
+
MOCKINGBIRD_STRIPE_SECRET_KEY=sk_test_... bun run parity -- --only GetPrices,GetProducts
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`mock:server` delivers webhooks to the targets in `MOCKINGBIRD_STRIPE_WEBHOOK_TARGETS` (a JSON array
|
|
245
|
+
of `{apiKey, url, secret}`, matched by the API key that produced the event) or to a single fallback
|
|
246
|
+
target from `MOCKINGBIRD_STRIPE_WEBHOOK_URL` + `MOCKINGBIRD_STRIPE_WEBHOOK_SECRET`:
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
PORT=12111 MOCKINGBIRD_STRIPE_WEBHOOK_TARGETS='[{"apiKey":"sk_test_mso","url":"http://127.0.0.1:3100/billing/webhooks/stripe/mso","secret":"whsec_..."}]' bun run mock:server
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Live parity needs a real `sk_test_` key and mutates a shared test account.
|
|
253
|
+
|
|
254
|
+
Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [`@crvouga/mockingbird`](https://github.com/crvouga/mockingbird/tree/main/packages/facade#readme).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real Stripe partitions objects by account; the mock partitions by the bearer key that created
|
|
3
|
+
* them, so two different test keys are two accounts and a PC object is `resource_missing` on the
|
|
4
|
+
* MSO account. The key itself is never stored — only this deterministic, opaque token.
|
|
5
|
+
*/
|
|
6
|
+
export declare const accountOfKey: (key: string) => string;
|
|
7
|
+
export declare const accountOf: (request: Request) => string;
|
|
8
|
+
//# sourceMappingURL=account.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,WAAkC,CAAA;AAE1E,eAAO,MAAM,SAAS,GAAI,SAAS,OAAO,KAAG,MAI5C,CAAA"}
|
package/dist/account.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { opaqueToken } from "@crvouga/mockingbird-service";
|
|
2
|
+
/**
|
|
3
|
+
* Real Stripe partitions objects by account; the mock partitions by the bearer key that created
|
|
4
|
+
* them, so two different test keys are two accounts and a PC object is `resource_missing` on the
|
|
5
|
+
* MSO account. The key itself is never stored — only this deterministic, opaque token.
|
|
6
|
+
*/
|
|
7
|
+
export const accountOfKey = (key) => `acct_${opaqueToken(key, 8)}`;
|
|
8
|
+
export const accountOf = (request) => {
|
|
9
|
+
const authorization = request.headers.get("authorization");
|
|
10
|
+
const match = authorization === null ? null : /^Bearer\s+(\S+)$/.exec(authorization);
|
|
11
|
+
return accountOfKey(match?.[1] ?? "");
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AAE1D;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,QAAQ,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAA;AAE1E,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAgB,EAAU,EAAE;IACpD,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAA;IAC1D,MAAM,KAAK,GAAG,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IACpF,OAAO,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;AACvC,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"charges.d.ts","sourceRoot":"","sources":["../src/charges.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAElF,OAAO,EAAkD,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAA;AA6E7F,eAAO,MAAM,cAAc,GAAI,UAAU,QAAQ,KAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAuBjF,CAAA"}
|
package/dist/charges.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { jsonResponse } from "@crvouga/mockingbird-service";
|
|
2
|
+
import { applyExpand } from "./expand.js";
|
|
3
|
+
import { requestScope, requireCharge } from "./internal.js";
|
|
4
|
+
import { matchesCreated, paginate } from "./list.js";
|
|
5
|
+
import { queryParams } from "./params.js";
|
|
6
|
+
import { renderCharge, renderCoupon, renderCustomer, renderInvoice, renderPaymentIntent, renderPaymentMethod, } from "./render.js";
|
|
7
|
+
const stringOf = (params, key) => {
|
|
8
|
+
const value = params[key];
|
|
9
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
10
|
+
};
|
|
11
|
+
const expanders = (scope) => {
|
|
12
|
+
const invoice = (id) => {
|
|
13
|
+
const record = scope.account.invoices.get(id);
|
|
14
|
+
return record ? renderInvoice(record, scope.account) : undefined;
|
|
15
|
+
};
|
|
16
|
+
const coupon = (id) => {
|
|
17
|
+
const record = scope.account.coupons.get(id);
|
|
18
|
+
return record ? renderCoupon(record) : undefined;
|
|
19
|
+
};
|
|
20
|
+
const paymentIntent = (id) => {
|
|
21
|
+
const record = scope.account.paymentIntents.get(id);
|
|
22
|
+
return record ? renderPaymentIntent(record) : undefined;
|
|
23
|
+
};
|
|
24
|
+
const customer = (id) => {
|
|
25
|
+
const entry = scope.account.customers.get(id);
|
|
26
|
+
return entry?.kind === "live" ? renderCustomer(entry.customer) : undefined;
|
|
27
|
+
};
|
|
28
|
+
const paymentMethod = (id) => {
|
|
29
|
+
const record = scope.account.paymentMethods.get(id);
|
|
30
|
+
return record ? renderPaymentMethod(record) : undefined;
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
invoice,
|
|
34
|
+
"invoice.discounts.coupon": coupon,
|
|
35
|
+
payment_intent: paymentIntent,
|
|
36
|
+
"payment_intent.invoice": invoice,
|
|
37
|
+
customer,
|
|
38
|
+
payment_method: paymentMethod,
|
|
39
|
+
"data.invoice": invoice,
|
|
40
|
+
"data.invoice.discounts.coupon": coupon,
|
|
41
|
+
"data.payment_intent": paymentIntent,
|
|
42
|
+
"data.payment_intent.invoice": invoice,
|
|
43
|
+
"data.customer": customer,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Stripe expands a path's ancestors along with it, and `invoice.discounts.coupon` only walks into
|
|
48
|
+
* an invoice once the shallower `invoice` pass has replaced the id with the rendered object, so
|
|
49
|
+
* every prefix is added and the list is applied shallowest first.
|
|
50
|
+
*/
|
|
51
|
+
const orderedPaths = (expand) => {
|
|
52
|
+
const requested = Array.isArray(expand)
|
|
53
|
+
? expand.filter((entry) => typeof entry === "string")
|
|
54
|
+
: typeof expand === "string" && expand !== ""
|
|
55
|
+
? [expand]
|
|
56
|
+
: [];
|
|
57
|
+
const paths = new Set();
|
|
58
|
+
for (const path of requested) {
|
|
59
|
+
const parts = path.split(".");
|
|
60
|
+
for (let depth = 1; depth <= parts.length; depth += 1)
|
|
61
|
+
paths.add(parts.slice(0, depth).join("."));
|
|
62
|
+
}
|
|
63
|
+
return [...paths].sort((a, b) => a.split(".").length - b.split(".").length);
|
|
64
|
+
};
|
|
65
|
+
const render = (scope, charge, expand) => applyExpand(renderCharge(charge), orderedPaths(expand), expanders(scope));
|
|
66
|
+
export const chargeHandlers = (services) => ({
|
|
67
|
+
GetCharges: async (context) => {
|
|
68
|
+
const scope = requestScope(services, context);
|
|
69
|
+
const params = queryParams(context);
|
|
70
|
+
const customer = stringOf(params, "customer");
|
|
71
|
+
const paymentIntent = stringOf(params, "payment_intent");
|
|
72
|
+
const page = await paginate(scope.account.charges, params, {
|
|
73
|
+
url: "/v1/charges",
|
|
74
|
+
kind: "charge",
|
|
75
|
+
where: (record) => matchesCreated(record.created, params.created) &&
|
|
76
|
+
(customer === null || record.customer === customer) &&
|
|
77
|
+
(paymentIntent === null || record.payment_intent === paymentIntent),
|
|
78
|
+
render: renderCharge,
|
|
79
|
+
});
|
|
80
|
+
return jsonResponse(200, applyExpand(page, orderedPaths(params.expand), expanders(scope)));
|
|
81
|
+
},
|
|
82
|
+
GetChargesCharge: async (context) => {
|
|
83
|
+
const scope = requestScope(services, context);
|
|
84
|
+
const params = queryParams(context);
|
|
85
|
+
const charge = requireCharge(scope, context.params.charge ?? "");
|
|
86
|
+
return jsonResponse(200, render(scope, charge, params.expand));
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=charges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"charges.js","sourceRoot":"","sources":["../src/charges.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAyB,MAAM,8BAA8B,CAAA;AAClF,OAAO,EAAE,WAAW,EAAwB,MAAM,aAAa,CAAA;AAC/D,OAAO,EAAqB,YAAY,EAAE,aAAa,EAAiB,MAAM,eAAe,CAAA;AAC7F,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AACpD,OAAO,EAAe,WAAW,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,aAAa,CAAA;AAGpB,MAAM,QAAQ,GAAG,CAAC,MAAc,EAAE,GAAW,EAAiB,EAAE;IAC9D,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AACjE,CAAC,CAAA;AAED,MAAM,SAAS,GAAG,CAAC,KAAmB,EAAmB,EAAE;IACzD,MAAM,OAAO,GAAG,CAAC,EAAU,EAAE,EAAE;QAC7B,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC7C,OAAO,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAClE,CAAC,CAAA;IACD,MAAM,MAAM,GAAG,CAAC,EAAU,EAAE,EAAE;QAC5B,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC5C,OAAO,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAClD,CAAC,CAAA;IACD,MAAM,aAAa,GAAG,CAAC,EAAU,EAAE,EAAE;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnD,OAAO,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACzD,CAAC,CAAA;IACD,MAAM,QAAQ,GAAG,CAAC,EAAU,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAC7C,OAAO,KAAK,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC5E,CAAC,CAAA;IACD,MAAM,aAAa,GAAG,CAAC,EAAU,EAAE,EAAE;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnD,OAAO,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACzD,CAAC,CAAA;IACD,OAAO;QACL,OAAO;QACP,0BAA0B,EAAE,MAAM;QAClC,cAAc,EAAE,aAAa;QAC7B,wBAAwB,EAAE,OAAO;QACjC,QAAQ;QACR,cAAc,EAAE,aAAa;QAC7B,cAAc,EAAE,OAAO;QACvB,+BAA+B,EAAE,MAAM;QACvC,qBAAqB,EAAE,aAAa;QACpC,6BAA6B,EAAE,OAAO;QACtC,eAAe,EAAE,QAAQ;KAC1B,CAAA;AACH,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,YAAY,GAAG,CAAC,MAAe,EAAY,EAAE;IACjD,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QACrC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC;QACtE,CAAC,CAAC,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,EAAE;YAC3C,CAAC,CAAC,CAAC,MAAM,CAAC;YACV,CAAC,CAAC,EAAE,CAAA;IACR,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC7B,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC;YACnD,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IAC9C,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAA;AAC7E,CAAC,CAAA;AAED,MAAM,MAAM,GAAG,CAAC,KAAmB,EAAE,MAAoB,EAAE,MAAe,EAAE,EAAE,CAC5E,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;AAE3E,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,QAAkB,EAAoC,EAAE,CAAC,CAAC;IACvF,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC7C,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;QACnC,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;QAC7C,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;QACxD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAe,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE;YACvE,GAAG,EAAE,aAAa;YAClB,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,CAChB,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC;gBAC9C,CAAC,QAAQ,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC;gBACnD,CAAC,aAAa,KAAK,IAAI,IAAI,MAAM,CAAC,cAAc,KAAK,aAAa,CAAC;YACrE,MAAM,EAAE,YAAY;SACrB,CAAC,CAAA;QACF,OAAO,YAAY,CAAC,GAAG,EAAE,WAAW,CAAC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC5F,CAAC;IACD,gBAAgB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAClC,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC7C,MAAM,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAA;QACnC,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;QAChE,OAAO,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;IAChE,CAAC;CACF,CAAC,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type OperationHandler } from "@crvouga/mockingbird-service";
|
|
2
|
+
import { createDiscount, requireSubscription, type Services } from "./internal.js";
|
|
3
|
+
export declare const checkoutSessionHandlers: (services: Services) => Record<string, OperationHandler>;
|
|
4
|
+
export { createDiscount, requireSubscription };
|
|
5
|
+
//# sourceMappingURL=checkout-sessions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checkout-sessions.d.ts","sourceRoot":"","sources":["../src/checkout-sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAGlF,OAAO,EAEL,cAAc,EAKd,mBAAmB,EACnB,KAAK,QAAQ,EACd,MAAM,eAAe,CAAA;AAiGtB,eAAO,MAAM,uBAAuB,GAAI,UAAU,QAAQ,KAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAgP3F,CAAA;AAED,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,CAAA"}
|