@firedrill-tools/stripe 0.1.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/LICENSE +201 -0
- package/README.md +433 -0
- package/firedrill/agent.target.json +17 -0
- package/firedrill/api-unavailable.scenario.json +11 -0
- package/firedrill/baseline.scenario.json +5 -0
- package/firedrill/conformance.suite.json +17 -0
- package/firedrill/rate-limited.scenario.json +11 -0
- package/firedrill/refund-committed-lost.scenario.json +11 -0
- package/firedrill/stripe-api-unavailable.drill.json +463 -0
- package/firedrill/stripe-denied.drill.json +74 -0
- package/firedrill/stripe-large-pages.drill.json +153 -0
- package/firedrill/stripe-live-mode.drill.json +136 -0
- package/firedrill/stripe-mcp-aliases.drill.json +406 -0
- package/firedrill/stripe-no-permissions.drill.json +1143 -0
- package/firedrill/stripe-rate-limited.drill.json +616 -0
- package/firedrill/stripe-refund-committed-lost.drill.json +139 -0
- package/firedrill/stripe-rest-flow.drill.json +1401 -0
- package/firedrill/stripe-restricted-key.drill.json +171 -0
- package/firedrill/tools/stripe/app/assets/ATTRIBUTION.md +36 -0
- package/firedrill/tools/stripe/app/assets/fonts/OFL.txt +93 -0
- package/firedrill/tools/stripe/app/assets/stripe-s.svg +1 -0
- package/firedrill/tools/stripe/app/assets/stripe.svg +1 -0
- package/firedrill/tools/stripe/app/site/app.js +456 -0
- package/firedrill/tools/stripe/app/site/assets/fonts/inter-latin.woff2 +0 -0
- package/firedrill/tools/stripe/app/site/assets/stripe-s.svg +1 -0
- package/firedrill/tools/stripe/app/site/assets/stripe.svg +1 -0
- package/firedrill/tools/stripe/app/site/icons.js +90 -0
- package/firedrill/tools/stripe/app/site/index.html +137 -0
- package/firedrill/tools/stripe/app/site/pages-billing.js +902 -0
- package/firedrill/tools/stripe/app/site/pages-catalog.js +314 -0
- package/firedrill/tools/stripe/app/site/pages-customers.js +416 -0
- package/firedrill/tools/stripe/app/site/pages-home.js +373 -0
- package/firedrill/tools/stripe/app/site/pages-payments.js +502 -0
- package/firedrill/tools/stripe/app/site/store.js +99 -0
- package/firedrill/tools/stripe/app/site/styles.css +2512 -0
- package/firedrill/tools/stripe/app/site/ui.js +767 -0
- package/firedrill/tools/stripe/app/site/widgets.js +707 -0
- package/firedrill/tools/stripe/behavior.mjs +148 -0
- package/firedrill/tools/stripe/lib/cards.mjs +53 -0
- package/firedrill/tools/stripe/lib/form.mjs +204 -0
- package/firedrill/tools/stripe/lib/ids.mjs +85 -0
- package/firedrill/tools/stripe/lib/money.mjs +35 -0
- package/firedrill/tools/stripe/lib/objects.mjs +229 -0
- package/firedrill/tools/stripe/lib/periods.mjs +41 -0
- package/firedrill/tools/stripe/lib/size.mjs +55 -0
- package/firedrill/tools/stripe/lib/state.mjs +230 -0
- package/firedrill/tools/stripe/lib/validate.mjs +184 -0
- package/firedrill/tools/stripe/lib/wire.mjs +98 -0
- package/firedrill/tools/stripe/ops/billing.mjs +914 -0
- package/firedrill/tools/stripe/ops/catalog.mjs +203 -0
- package/firedrill/tools/stripe/ops/customers.mjs +241 -0
- package/firedrill/tools/stripe/ops/dashboard.mjs +29 -0
- package/firedrill/tools/stripe/ops/payments.mjs +608 -0
- package/firedrill/tools/stripe/stripe.tool.json +28833 -0
- package/firedrill/world.json +8527 -0
- package/firedrill.json +5 -0
- package/package.json +64 -0
- package/starter.json +7999 -0
- package/test/conformance.mjs +1133 -0
package/README.md
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# @firedrill-tools/stripe
|
|
2
|
+
|
|
3
|
+
A synthetic **Stripe account in test mode** for [Firedrill](https://firedrill.run): customers, synthetic card
|
|
4
|
+
payment methods, PaymentIntents with the full status machine, charges, refunds, products, prices, invoice items,
|
|
5
|
+
invoices (draft → open → paid | void), subscriptions and a computed balance, exposed through a bounded subset of
|
|
6
|
+
the Stripe REST API v1 (`/v1/…`, form-encoded requests, API-version `2026-08-26.dahlia` object shapes, the Stripe
|
|
7
|
+
error envelope, `Idempotency-Key`) and through the per-resource tool names of Stripe's agent toolkit MCP server.
|
|
8
|
+
Agents that talk to Stripe through `stripe-node`/`stripe-python`-style REST calls or over MCP can be exercised
|
|
9
|
+
against it without a Stripe account, a secret key or network access.
|
|
10
|
+
|
|
11
|
+
Everything is computed from the world's state: ids come from a counter row, timestamps from Firedrill's virtual
|
|
12
|
+
clock, card outcomes from a fixed test-card catalogue. Nothing here contacts Stripe or a card network; no e-mail,
|
|
13
|
+
receipt, PDF or hosted page is ever sent or served.
|
|
14
|
+
|
|
15
|
+
Package id `stripe`, version `0.1.0`, engine `>=0.1.0 <0.2.0`, license Apache-2.0. The package ships a **browser
|
|
16
|
+
app**: a recreation of the Stripe Dashboard (sandbox) that drives the same operations — see "Browser app" below.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
firedrill tool add @firedrill-tools/stripe --install
|
|
22
|
+
firedrill serve --scenario baseline --no-open
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`firedrill serve` prints the HTTP and MCP endpoints and their tokens. A new world receives the starter data and
|
|
26
|
+
the grants below. Existing worlds do not change silently: add the grants and (optionally) the `starter.json` rows
|
|
27
|
+
to your own world or scenario.
|
|
28
|
+
|
|
29
|
+
### Grants and identities
|
|
30
|
+
|
|
31
|
+
Every actor that should use the Tool needs the grants for the operations it may call (`packageId: "stripe"`):
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
balance.retrieve dashboard.context
|
|
35
|
+
customers.create customers.retrieve customers.update customers.list
|
|
36
|
+
payment_methods.list payment_methods.attach payment_methods.detach
|
|
37
|
+
payment_intents.create payment_intents.retrieve payment_intents.list payment_intents.confirm
|
|
38
|
+
payment_intents.capture payment_intents.cancel
|
|
39
|
+
charges.retrieve charges.list refunds.create refunds.list
|
|
40
|
+
products.create products.retrieve products.update products.list prices.create prices.retrieve prices.list
|
|
41
|
+
invoice_items.create invoices.create invoices.retrieve invoices.list invoices.finalize invoices.pay invoices.void
|
|
42
|
+
subscriptions.create subscriptions.retrieve subscriptions.list subscriptions.update subscriptions.cancel
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
A Stripe secret key is account-wide, so there is no per-user record ownership. Actor **attributes** model what
|
|
46
|
+
varies per key; every attribute is optional and a fresh actor without any attribute behaves as a full test-mode
|
|
47
|
+
secret key:
|
|
48
|
+
|
|
49
|
+
| attribute | meaning |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `livemode` (boolean, default `false`) | Every object the actor reads or creates is stamped with this `livemode`. Stripe's test payment method ids (`pm_card_visa`, …) resolve only in test mode; a live-mode key gets `404 resource_missing` (`No such PaymentMethod: 'pm_card_visa'`). Rows are shared across actors regardless of mode (one account, one dataset). |
|
|
52
|
+
| `permissions` (object) | Restricted-key permissions: keys are the resource groups `balance`, `customers`, `payment_methods`, `payment_intents`, `charges`, `refunds`, `products` (products and prices), `invoices` (invoices and invoice items), `subscriptions`; values `none`, `read` or `write`. Absent attribute = a full secret key. A missing key inside the object = `none`. An operation needing a level the key lacks fails `PERMISSION_DENIED` (HTTP 403, `invalid_request_error`, "This API key does not have the required permissions for this endpoint on account … Having the '<group>: <level>' permission would allow this request to continue."). Cross-resource operations need several groups (`refunds.create`: `refunds: write` + `charges: read`; `subscriptions.create`: `subscriptions: write`, `customers: read`, `products: read`, `invoices: write`, `payment_intents: write`). |
|
|
53
|
+
| `keyName` (string) | Display only; never rendered into responses. |
|
|
54
|
+
|
|
55
|
+
Framework grants decide whether an actor may call an operation at all (framework `denied` → 403 in the Stripe
|
|
56
|
+
envelope); the attributes decide what the call may do.
|
|
57
|
+
|
|
58
|
+
### Connections
|
|
59
|
+
|
|
60
|
+
Two connection recipes are declared: `stripe-sdk` (`STRIPE_SECRET_KEY` = the HTTP binding token, sent as
|
|
61
|
+
`Authorization: Bearer …`; `STRIPE_API_BASE` = the HTTP binding URL) and `stripe-agent-toolkit-mcp`
|
|
62
|
+
(`STRIPE_MCP_URL` / `STRIPE_MCP_TOKEN` for the MCP binding). The values are isolated world tokens for test
|
|
63
|
+
processes only.
|
|
64
|
+
|
|
65
|
+
## Starting data
|
|
66
|
+
|
|
67
|
+
`starter.json` (and the conformance `firedrill/world.json`) contain 109 fictional rows and set `virtualTimeUs`
|
|
68
|
+
`1789376400000000` (2026-09-14T09:00:00Z), which `firedrill tool add` copies into a new project's world, so objects
|
|
69
|
+
created in a fresh install are stamped 2026-09-14 and sort after the seeded data: account `acct_1S00Q1HxKLumenTr` **Lumen Trail Supply Co.** (US, `usd`, statement
|
|
70
|
+
descriptor `LUMEN TRAIL`), an outdoor-gear shop selling one-off orders and two membership plans, with 12
|
|
71
|
+
customers (one without e-mail, two sharing an e-mail, a mixed-case e-mail, non-ASCII names, a GB customer with
|
|
72
|
+
`tax_exempt: reverse`, a customer with three cards, one without any payment method, one whose default card
|
|
73
|
+
declines, one whose card requires 3D Secure, one delinquent with a past-due invoice, one with three pending invoice
|
|
74
|
+
items in two currencies), 14 materialised test cards (one unattached), 6 products (one inactive), 10 prices (four
|
|
75
|
+
one-time incl. `gbp` and a zero amount, five recurring incl. a 14-day trial and an inactive legacy plan, lookup
|
|
76
|
+
keys `summit_monthly`/`summit_yearly`), 21 PaymentIntents in every status (three charges inside the two-day
|
|
77
|
+
pending window, one uncaptured, one declined with `last_payment_error`, one 3DS `requires_action`, one canceled,
|
|
78
|
+
seven belonging to invoices), 13 charges, 4 refunds, 9 invoice items, 12 invoices (draft with lines, empty draft,
|
|
79
|
+
open `send_invoice`, open past due, paid manual and subscription invoices, void, uncollectible) and 6
|
|
80
|
+
subscriptions (`active` with two items, `trialing`, `past_due`, `active` with `cancel_at_period_end`, `canceled`,
|
|
81
|
+
`incomplete`; one anchored on the 31st). Starter ids use object sequences below 1000; created objects start at
|
|
82
|
+
sequence 1000 (`cus_0000RS…`).
|
|
83
|
+
|
|
84
|
+
## Operations
|
|
85
|
+
|
|
86
|
+
| operation | MCP alias (agent toolkit) | HTTP route |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `balance.retrieve` | `retrieve_balance` | `GET /v1/balance` |
|
|
89
|
+
| `dashboard.context` | — | — (canonical operation only: `POST /v1/operations/stripe/dashboard.context`, MCP `stripe.dashboard.context`) |
|
|
90
|
+
| `customers.create` | `create_customer` | `POST /v1/customers` |
|
|
91
|
+
| `customers.retrieve` | — | `GET /v1/customers/{customer}` |
|
|
92
|
+
| `customers.update` | — | `POST /v1/customers/{customer}` |
|
|
93
|
+
| `customers.list` | `list_customers` | `GET /v1/customers` |
|
|
94
|
+
| `payment_methods.list` | — | `GET /v1/payment_methods?customer=` and `GET /v1/customers/{customer}/payment_methods` |
|
|
95
|
+
| `payment_methods.attach` | — | `POST /v1/payment_methods/{payment_method}/attach` |
|
|
96
|
+
| `payment_methods.detach` | — | `POST /v1/payment_methods/{payment_method}/detach` |
|
|
97
|
+
| `payment_intents.create` | — | `POST /v1/payment_intents` |
|
|
98
|
+
| `payment_intents.retrieve` | — | `GET /v1/payment_intents/{payment_intent}` |
|
|
99
|
+
| `payment_intents.list` | `list_payment_intents` | `GET /v1/payment_intents` |
|
|
100
|
+
| `payment_intents.confirm` | — | `POST /v1/payment_intents/{payment_intent}/confirm` |
|
|
101
|
+
| `payment_intents.capture` | — | `POST /v1/payment_intents/{payment_intent}/capture` |
|
|
102
|
+
| `payment_intents.cancel` | — | `POST /v1/payment_intents/{payment_intent}/cancel` |
|
|
103
|
+
| `charges.retrieve` | — | `GET /v1/charges/{charge}` |
|
|
104
|
+
| `charges.list` | — | `GET /v1/charges` |
|
|
105
|
+
| `refunds.create` | `create_refund` | `POST /v1/refunds` |
|
|
106
|
+
| `refunds.list` | — | `GET /v1/refunds` |
|
|
107
|
+
| `products.create` | `create_product` | `POST /v1/products` |
|
|
108
|
+
| `products.retrieve` | — | `GET /v1/products/{product}` |
|
|
109
|
+
| `products.update` | — | `POST /v1/products/{product}` |
|
|
110
|
+
| `products.list` | `list_products` | `GET /v1/products` |
|
|
111
|
+
| `prices.create` | `create_price` | `POST /v1/prices` |
|
|
112
|
+
| `prices.retrieve` | — | `GET /v1/prices/{price}` |
|
|
113
|
+
| `prices.list` | `list_prices` | `GET /v1/prices` |
|
|
114
|
+
| `invoice_items.create` | `create_invoice_item` | `POST /v1/invoiceitems` |
|
|
115
|
+
| `invoices.create` | `create_invoice` | `POST /v1/invoices` |
|
|
116
|
+
| `invoices.retrieve` | — | `GET /v1/invoices/{invoice}` |
|
|
117
|
+
| `invoices.list` | `list_invoices` | `GET /v1/invoices` |
|
|
118
|
+
| `invoices.finalize` | `finalize_invoice` | `POST /v1/invoices/{invoice}/finalize` |
|
|
119
|
+
| `invoices.pay` | — | `POST /v1/invoices/{invoice}/pay` |
|
|
120
|
+
| `invoices.void` | — | `POST /v1/invoices/{invoice}/void` |
|
|
121
|
+
| `subscriptions.create` | — | `POST /v1/subscriptions` |
|
|
122
|
+
| `subscriptions.retrieve` | — | `GET /v1/subscriptions/{subscription}` |
|
|
123
|
+
| `subscriptions.list` | `list_subscriptions` | `GET /v1/subscriptions` |
|
|
124
|
+
| `subscriptions.update` | `update_subscription` | `POST /v1/subscriptions/{subscription}` |
|
|
125
|
+
| `subscriptions.cancel` | `cancel_subscription` | `DELETE /v1/subscriptions/{subscription}` |
|
|
126
|
+
|
|
127
|
+
Canonical names are also reachable as `POST /v1/operations/stripe/<operation>` and as MCP tools
|
|
128
|
+
`stripe.<operation>`. `dashboard.context` is the one operation without a Stripe route: it returns the simulated
|
|
129
|
+
account (`account.id`, `business_name`, `country`, `default_currency`, `statement_descriptor`, `support_email`),
|
|
130
|
+
the world's virtual time (`now`, Unix seconds), `livemode`, the calling key's permission level per resource group
|
|
131
|
+
(`permissions.<group>` = `none | read | write`) and `api_version`. It never fails (a key without permissions gets
|
|
132
|
+
`none` everywhere) and exists so the browser app — or any client — can render "today" from the world clock rather
|
|
133
|
+
than the machine clock. Every operation takes the Stripe REST parameter names (snake_case, `expand[]`,
|
|
134
|
+
`metadata[key]`, `items[0][price]`, `created[gte]`, …) and returns the Stripe API object or list envelope
|
|
135
|
+
`{ object: "list", data, has_more, url }`.
|
|
136
|
+
|
|
137
|
+
Behaviour highlights, all computed from state:
|
|
138
|
+
|
|
139
|
+
- **Cards** are synthetic. Stripe's published test ids `pm_card_visa`, `pm_card_visa_debit`, `pm_card_mastercard`,
|
|
140
|
+
`pm_card_amex`, `pm_card_chargeDeclined`, `pm_card_chargeDeclinedInsufficientFunds` and
|
|
141
|
+
`pm_card_authenticationRequired` are accepted wherever a payment method is expected; attaching or confirming
|
|
142
|
+
with one materialises a real `pm_…` object (the response id differs from the test id, as in Stripe's test mode).
|
|
143
|
+
A materialised card keeps a private outcome that decides every later confirmation; the fingerprint reveals the
|
|
144
|
+
underlying test card.
|
|
145
|
+
- **PaymentIntents** follow `requires_payment_method → requires_confirmation → requires_action | requires_capture |
|
|
146
|
+
succeeded | canceled`. `confirm=true` on create, `/confirm`, `/capture` (partial capture supported, the remainder
|
|
147
|
+
is released without a refund object) and `/cancel` (an uncaptured charge is released: `refunded: true`). Every
|
|
148
|
+
attempt produces a charge with `payment_method_details.card`, `outcome`, `balance_transaction`, a `.test` receipt
|
|
149
|
+
URL; successful captures emit `payment_intent.succeeded`.
|
|
150
|
+
- **Declines answer HTTP 402 `card_error`** with Stripe's error envelope (`type: card_error`, `code: card_declined`,
|
|
151
|
+
`decline_code`, `message`, `doc_url`, `request_log_url`). **Unlike Stripe, the failed attempt is not persisted**
|
|
152
|
+
by `payment_intents.create` (`confirm=true`), `payment_intents.confirm` or `invoices.pay`: Firedrill discards every
|
|
153
|
+
state write of an operation that ends in a declared error, id counters included (framework constraint, see
|
|
154
|
+
Limitations). The 402 body therefore names only objects that existed before the call:
|
|
155
|
+
- **Creation-time declines** (`payment_intents.create` with `confirm=true`, and `subscriptions.create` with
|
|
156
|
+
`payment_behavior=error_if_incomplete`) deliberately carry **no `payment_intent`, no `payment_method` and no
|
|
157
|
+
object id** — Stripe would include the new intent, but here it was never created. Its id would answer 404 and
|
|
158
|
+
be handed to the next object created, so an agent retrying with it would silently act on an unrelated record.
|
|
159
|
+
Retry with a new create; list `GET /v1/payment_intents` to confirm nothing was stored.
|
|
160
|
+
- **`/confirm` and `/pay` declines** name the **existing** PaymentIntent as the attempt would have left it
|
|
161
|
+
(`requires_payment_method` with `last_payment_error`), but never a charge (`latest_charge: null`, no
|
|
162
|
+
`last_payment_error.charge`) and the PaymentMethod only when it existed before the call (the intent's stored
|
|
163
|
+
method or the customer's default); a catalogue test card such as `pm_card_chargeDeclined` passed to the call is
|
|
164
|
+
materialised inside the rolled-back operation, so `payment_method` and `last_payment_error.payment_method` are
|
|
165
|
+
omitted. A retrieve afterwards shows the pre-attempt state (e.g. `requires_confirmation` with the original
|
|
166
|
+
`payment_method`, `last_payment_error: null`); after a `/pay` decline the invoice stays `open` with
|
|
167
|
+
`attempt_count: 0`.
|
|
168
|
+
|
|
169
|
+
No failed charge is stored and `payment_intent.declined` is not emitted by these three operations. Retrying
|
|
170
|
+
`/confirm` or `/pay` with another `payment_method` works. The failed attempt *is* persisted (intent
|
|
171
|
+
`requires_payment_method` with `last_payment_error`, a `failed` charge, invoice `attempt_count: 1`, event
|
|
172
|
+
`payment_intent.declined`) where the call itself succeeds: `subscriptions.create` with `payment_behavior`
|
|
173
|
+
`default_incomplete` (default) or `allow_incomplete`, which returns an `incomplete` subscription.
|
|
174
|
+
- **Refunds** by `charge` or `payment_intent`, partial or full; `amount_too_large` beyond the unrefunded amount,
|
|
175
|
+
`charge_already_refunded` once fully refunded, `charge_not_captured` for authorisations, `charge_not_refundable`
|
|
176
|
+
for failed charges. Each refund emits `charge.refunded`.
|
|
177
|
+
- **Balance** is computed per currency: `available` = captured charges older than two virtual days minus their
|
|
178
|
+
refunds, `pending` = the same for the last two virtual days.
|
|
179
|
+
- **Customers** (`email` is not unique; the `email` filter is exact and case-sensitive), partial updates,
|
|
180
|
+
metadata merge (`""` deletes a key; 50 keys / 40-char keys / 500-char values), `invoice_settings.default_payment_method`
|
|
181
|
+
must be attached to the customer. `delinquent` is derived: `true` while an open invoice is past its `due_date`.
|
|
182
|
+
- **Products and prices**: `default_price` only through `products.update` with a price of that product;
|
|
183
|
+
`lookup_key` unique among active prices (`resource_already_exists`); recurring intervals bounded to one year;
|
|
184
|
+
prices are immutable (no `prices.update`).
|
|
185
|
+
- **Invoice items** are pending (`invoice: null`) until the customer's next `invoices.create` sweeps the ones in
|
|
186
|
+
the invoice currency (`pending_invoice_items_behavior=exclude` skips them), or are appended to a given **draft**
|
|
187
|
+
invoice (`invoice_not_editable` otherwise). `invoices.create` infers `send_invoice` when `days_until_due` is given.
|
|
188
|
+
- **Invoices**: `finalize` assigns `<customer prefix>-<sequence>`, `.test` hosted/PDF URLs and, for a positive
|
|
189
|
+
total, opens the invoice with one `InvoicePayment` and a `requires_payment_method` PaymentIntent (nothing is
|
|
190
|
+
attempted automatically); a zero total is `paid` immediately. `pay` confirms that intent with `payment_method`,
|
|
191
|
+
the intent's method or the customer default (`invoice_no_payment_method` when none), or marks it
|
|
192
|
+
`paid_out_of_band` (the intent is canceled). `void` cancels the intent (`cancellation_reason: void_invoice`). A
|
|
193
|
+
PaymentIntent that belongs to an open invoice cannot be confirmed or canceled directly
|
|
194
|
+
(`payment_intent_invoice_managed`).
|
|
195
|
+
- **Subscriptions**: every price must be active, recurring, and share one currency and interval; the first invoice
|
|
196
|
+
(`billing_reason: subscription_create`, one `subscription_item_details` line per item) is finalized and — for
|
|
197
|
+
`charge_automatically` with a positive total — paid in the same transaction: `active` on success, `incomplete`
|
|
198
|
+
without a usable method or on a decline, `trialing` with `trial_period_days`/`trial_end` (a `$0` paid invoice),
|
|
199
|
+
`active` with an open invoice for `send_invoice`. Item periods (`current_period_start/end`) live on the items;
|
|
200
|
+
month/year steps clamp to the end of month. `update` edits items (add/change/remove, never all),
|
|
201
|
+
`cancel_at_period_end` (`cancel_at` = period end, `cancellation_details.reason: cancellation_requested`, status
|
|
202
|
+
unchanged), `default_payment_method`, `trial_end` (`"now"` activates), metadata and `cancellation_details`;
|
|
203
|
+
`proration_behavior` is accepted and ignored. `DELETE` cancels immediately (`ended_at`), voids an incomplete
|
|
204
|
+
subscription's open invoice and emits `customer.subscription.deleted`; a second `DELETE` answers 404 as Stripe
|
|
205
|
+
does. Listing hides `canceled`/`incomplete_expired` unless `status=all`, `ended` or the status is named.
|
|
206
|
+
- **Lists** are newest first with `limit` (1–100, default 10), `starting_after`/`ending_before` cursors that must
|
|
207
|
+
name an existing object (`resource_missing` on the cursor otherwise), `created`/`due_date` range filters and the
|
|
208
|
+
documented equality filters. `expand[]` (one level, `data.` prefix on lists) is allowed for: PaymentIntent
|
|
209
|
+
`customer`, `payment_method`, `latest_charge`; Charge `customer`, `payment_intent`, `payment_method`, `refunds`;
|
|
210
|
+
Refund `charge`, `payment_intent`; Customer `invoice_settings.default_payment_method`; PaymentMethod `customer`;
|
|
211
|
+
Product `default_price`; Price `product`; InvoiceItem `customer`, `invoice`; Invoice `customer`,
|
|
212
|
+
`default_payment_method`, `payments.data.payment.payment_intent`, `lines.data.pricing.price_details.price`,
|
|
213
|
+
`parent.subscription_details.subscription`; Subscription `customer`, `default_payment_method`, `latest_invoice`,
|
|
214
|
+
`latest_invoice.payments.data.payment.payment_intent`. Anything else: `This property cannot be expanded`.
|
|
215
|
+
- **Response size.** The framework refuses any HTTP response over 1 MiB, so list pages are filled by `limit` *and* by
|
|
216
|
+
the encoded UTF-8 size of the objects returned, expansions included (at most 950,000 bytes of objects per page).
|
|
217
|
+
A page the byte budget ends early answers `has_more: true`, and `starting_after` (its last object) or
|
|
218
|
+
`ending_before` (its first object) resumes exactly at the first object not returned; backward pages are filled from
|
|
219
|
+
the cursor outward. A single object, with its requested expansions, larger than 900,000 bytes of JSON answers 400
|
|
220
|
+
`invalid_request_error` (`parameter_invalid`, "… is too large to return in one response …") instead of a partial
|
|
221
|
+
body; `POST /v1/invoiceitems` refuses a line that would push its draft invoice past that bound (nothing is written).
|
|
222
|
+
An expanded `charge.refunds` embeds the newest 10 refunds with `has_more` and `total_count`, as Stripe does; list
|
|
223
|
+
the rest with `GET /v1/refunds?charge=`.
|
|
224
|
+
|
|
225
|
+
### Wire format
|
|
226
|
+
|
|
227
|
+
`POST` routes take `application/x-www-form-urlencoded` bodies in Stripe's bracket syntax (a JSON body gets the
|
|
228
|
+
framework's 415); `GET`/`DELETE` routes take query strings in the same syntax and no body. `Authorization: Bearer
|
|
229
|
+
<token>` is the only accepted auth (HTTP Basic, `curl -u sk_test_…:`, is rejected with the framework's 401).
|
|
230
|
+
`Idempotency-Key` is honoured by the framework: the same key replays the original response (including a replayed
|
|
231
|
+
503). Every response carries `Request-Id: req_…`, `Stripe-Version: 2026-08-26.dahlia` and echoes
|
|
232
|
+
`Idempotency-Key`. Errors use `{ error: { type, code?, message, param?, doc_url?, decline_code?, payment_intent?,
|
|
233
|
+
payment_method?, request_log_url } }`:
|
|
234
|
+
|
|
235
|
+
| tool error | HTTP | `type` / `code` |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| `INVALID_REQUEST` | 400 | `invalid_request_error` / `parameter_missing`, `parameter_invalid`, `parameter_invalid_integer`, `parameter_invalid_string`, `parameter_invalid_empty`, `parameter_unknown`, `parameter_invalid_boolean`, `email_invalid`, `resource_already_exists`, `amount_too_small`, `amount_too_large`, `state_bound_exceeded`\* |
|
|
238
|
+
| `INVALID_STATE` | 400 | `invalid_request_error` / `payment_intent_unexpected_state`, `charge_already_refunded`, `invoice_no_customer_line_items`, `invoice_not_editable`, `invoice_payment_intent_requires_action`, `subscription_canceled`, and this Tool's own codes\* `payment_method_already_attached`, `payment_method_unattached`, `payment_method_customer_mismatch`, `payment_intent_payment_method_missing`, `payment_intent_invoice_managed`, `charge_not_captured`, `charge_not_refundable`, `invoice_not_finalized`, `invoice_already_paid`, `invoice_void`, `invoice_no_payment_method`, `invoice_payment_intent_missing`, `customer_missing_payment_method` |
|
|
239
|
+
| `CARD_DECLINED` | 402 | `card_error` / `card_declined` with `decline_code` `generic_decline` or `insufficient_funds` |
|
|
240
|
+
| `PERMISSION_DENIED` | 403 | `invalid_request_error` (no code); a framework-denied actor gets the same shape |
|
|
241
|
+
| `RESOURCE_MISSING` | 404 | `invalid_request_error` / `resource_missing`, `param` names the parameter |
|
|
242
|
+
| `RATE_LIMITED` | 429 | `invalid_request_error` / `rate_limit` |
|
|
243
|
+
| `API_ERROR` | 503 | `api_error` |
|
|
244
|
+
| framework schema rejection | 400 | `invalid_request_error` / `parameter_invalid` with the framework message (`arguments do not match stripe.<operation>`) — an unknown parameter or a non-integer `amount` lands here; the offending parameter is not named |
|
|
245
|
+
| form/query decoding refused | 400 | `invalid_request_error` / `parameter_unknown` "Received unknown parameter: <key>" for a bracket segment `__proto__`, `constructor` or `prototype` (in any position, including `metadata[...]`); `parameter_invalid` "Invalid array: <key>…" for an array index above 999 or more than 1,000 `[]` entries; `parameter_invalid` "Invalid parameter: <key> is nested more than 20 levels deep." — `param` names the (truncated) key. These messages are this Tool's wording\* |
|
|
246
|
+
|
|
247
|
+
Codes marked \* are this Tool's naming where Stripe's exact code was not verified.
|
|
248
|
+
|
|
249
|
+
**Malformed percent-encoding.** The framework decodes query strings and form bodies leniently: a broken escape such as
|
|
250
|
+
`%E0%A4%A` becomes U+FFFD (`%ZZ` stays literal, and a broken escape in a *path* segment is a 404). A filter value that
|
|
251
|
+
carries U+FFFD is a mangled request, not a value that legitimately matches nothing, so every free-text list filter —
|
|
252
|
+
`email` (customers), `customer`, `type` (payment methods), `ids` (products), `product`, `lookup_keys`, `currency`
|
|
253
|
+
(prices), `customer`, `subscription` (invoices), `customer`, `price` (subscriptions), `customer` (payment intents),
|
|
254
|
+
`customer`, `payment_intent` (charges) and `charge`, `payment_intent` (refunds) — answers
|
|
255
|
+
`400 invalid_request_error / parameter_invalid` "Invalid `<param>`: the value contains an
|
|
256
|
+
invalid character (U+FFFD); check the percent-encoding of the request." with `param` naming the parameter, over both the
|
|
257
|
+
`/v1/...` routes and the canonical `/v1/operations/stripe/<operation>` path. A correctly encoded U+FFFD (`%EF%BF%BD`) is
|
|
258
|
+
rejected the same way. Correctly encoded non-ASCII text (`caf%C3%A9`, CJK, emoji) is unaffected and still matches.
|
|
259
|
+
The five closed-enum filters (`type` on
|
|
260
|
+
prices, `status` and `collection_method` on invoices and on subscriptions) never reach that guard: the declared enum
|
|
261
|
+
schema rejects a mangled value first, so they answer `400 invalid_request_error / parameter_invalid` with the generic
|
|
262
|
+
framework message (`arguments do not match stripe.<operation>.list`) and **no** `param` field.
|
|
263
|
+
`starting_after`/`ending_before` cursors and `expand[]` entries already answer `resource_missing` (404) and
|
|
264
|
+
"This property cannot be expanded" (400) for such values, so they are left as they are.
|
|
265
|
+
|
|
266
|
+
The form decoder (`lib/form.mjs`) is bounded and never throws: it builds null-prototype maps, rejects reserved
|
|
267
|
+
segments, caps nesting depth (20 bracket segments) and array indices (0–999, leading zeros not significant, so `items[0001]`
|
|
268
|
+
is index 1), and does work proportional to the request size. A codec cannot return a response from `decode` (a throw becomes the framework's
|
|
269
|
+
`HTTP_REQUEST_MAPPING_FAILED`), so a refused form travels to `encode` as the single argument `firedrill:form_error`,
|
|
270
|
+
which every closed input schema rejects before any handler runs; `encode` renders it as the row above. Such a request
|
|
271
|
+
is never executed and forwards no `Idempotency-Key`. Canonical JSON arguments are parsed by the framework, which drops
|
|
272
|
+
a `__proto__` member before the handler sees it. Paths outside the table above
|
|
273
|
+
get the framework 404 (`{"error":"route not found"}`, not Stripe's `Unrecognized request URL`).
|
|
274
|
+
|
|
275
|
+
## Events and faults
|
|
276
|
+
|
|
277
|
+
Events: `payment_intent.succeeded` (once per intent reaching `succeeded`), `payment_intent.declined` (once per
|
|
278
|
+
persisted decline — a Tool-specific name; Stripe's analogue is `payment_intent.payment_failed`), `invoice.paid`,
|
|
279
|
+
`charge.refunded` (once per refund, with `fully_refunded`) and `customer.subscription.deleted`. Consumers can attach
|
|
280
|
+
callbacks to them in their own world.
|
|
281
|
+
|
|
282
|
+
Faults (select them in a scenario with `faults: [{ packageId: "stripe", faultId }]`):
|
|
283
|
+
|
|
284
|
+
| fault | timing | effect |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| `rate-limited` | before, the 37 Stripe-shaped operations (not `dashboard.context`) | `429 rate_limit` "Request rate limit exceeded…"; nothing runs. |
|
|
287
|
+
| `api-unavailable` | before, every write (20 operations) | `503 api_error`; nothing is written, reads keep working. |
|
|
288
|
+
| `refund-committed-lost` | after commit, `refunds.create` | The refund **is** created (charge `amount_refunded` advanced, `charge.refunded` emitted) yet the caller sees 503. A retry with the **same** `Idempotency-Key` replays the 503 without a second refund; a retry with a **new** key refunds again — a partial-amount retry double-refunds silently, a full-amount retry hits `400 charge_already_refunded`. Exactly the trap an agent must handle. |
|
|
289
|
+
|
|
290
|
+
## Conformance
|
|
291
|
+
|
|
292
|
+
`firedrill tool test stripe` runs ten drills twice over the HTTP and MCP bindings (a scripted target,
|
|
293
|
+
`test/conformance.mjs`, Node built-ins only): the full REST flow (every route, error path and edge case), the MCP
|
|
294
|
+
aliases, a read-only restricted key, a key without permissions, a live-mode key, a framework-denied actor, large
|
|
295
|
+
byte-bounded pages walked both ways (the "Response size" rules), and one drill per fault. Together they observe every operation, every declared error of every operation, all five events
|
|
296
|
+
and all three faults, and the repeat pass proves determinism.
|
|
297
|
+
|
|
298
|
+
## Protocol compatibility
|
|
299
|
+
|
|
300
|
+
The HTTP surface follows the Stripe API reference field-by-field for the routes above (form encoding,
|
|
301
|
+
`Idempotency-Key`, list envelope, error envelope, statuses, API-version `2026-08-26.dahlia` object shapes), so
|
|
302
|
+
`stripe` (npm 22.x) constructed with `host`/`port`/`protocol` from the HTTP binding and `stripe` (PyPI) with
|
|
303
|
+
`stripe.api_base` are the intended clients — but **no official client has been run against this Tool**:
|
|
304
|
+
compatibility is by reference, not verified against a real client, and the manifest `compatibility` array is
|
|
305
|
+
empty. The sixteen MCP aliases reproduce the per-resource tool names of `@stripe/agent-toolkit` 0.7.9 / `@stripe/mcp`
|
|
306
|
+
≤ 0.2.x and accept those tools' argument names; they return the full REST object or list (a superset of the
|
|
307
|
+
toolkit's trimmed results). Stripe's current remote MCP server (`mcp.stripe.com`, `@stripe/mcp` 0.3.x,
|
|
308
|
+
`@stripe/agent-toolkit` 0.9.x) exposes generic `stripe_api_read` / `stripe_api_write` / `stripe_api_search` /
|
|
309
|
+
`get_stripe_account_info` tools instead; those, and the toolkit's payment-link, coupon, dispute and documentation
|
|
310
|
+
tools, are not provided.
|
|
311
|
+
|
|
312
|
+
## Limitations
|
|
313
|
+
|
|
314
|
+
- A bounded synthetic subset: 38 operations (37 Stripe-shaped + `dashboard.context`), 38 routes, eleven resource types. No Checkout Sessions, Payment Links,
|
|
315
|
+
SetupIntents, Sources/Tokens/legacy cards, Coupons, Disputes, Payouts, Transfers/Connect (`Stripe-Account` is
|
|
316
|
+
ignored), Balance transactions, Events, Webhook endpoints, Search endpoints, Tax, Billing portal, Quotes, Credit
|
|
317
|
+
notes, Subscription schedules, metered billing, API v2 or OAuth. `payment_intents.update`,
|
|
318
|
+
`invoices.update/delete/send/mark_uncollectible`, `customers.delete`, `payment_methods.create`, `products.delete`
|
|
319
|
+
and `prices.update` are absent.
|
|
320
|
+
- Byte-bounded responses: pages may hold fewer than `limit` objects when the objects are large (see "Response size");
|
|
321
|
+
the 900,000-byte single-object bound and its error message are this Tool's, not Stripe's (Stripe bounds objects
|
|
322
|
+
through its own field limits).
|
|
323
|
+
- Form and query parameters: the segments `__proto__`, `constructor` and `prototype` are refused everywhere (Stripe
|
|
324
|
+
would accept them as `metadata` keys), nesting is capped at 20 bracket segments and arrays at 1,000 entries
|
|
325
|
+
(indices 0–999); the error messages for these refusals are this Tool's wording, not Stripe's.
|
|
326
|
+
- Cards are the only payment method type and come from a seven-entry test catalogue; no raw card numbers, tokens,
|
|
327
|
+
wallets, bank debits or 3D Secure completion (a `requires_action` intent can only be canceled).
|
|
328
|
+
`automatic_async` capture behaves like `automatic`.
|
|
329
|
+
- **Declined confirmations are not persisted — a framework constraint.** Stripe answers 402 *and* stores the
|
|
330
|
+
attempt. A Firedrill operation either commits, and then its HTTP status is always the route's 2xx
|
|
331
|
+
`successStatus` (`@firedrill-run/protocol-http` `src/wire.ts:423-424`; the codec's `ToolHttpResponse` has no status
|
|
332
|
+
field, `@firedrill-run/tool-sdk` `src/types.ts:105-108`; `successStatus` is limited to 200–299,
|
|
333
|
+
`@firedrill-run/contracts` `src/operation.ts:278`), or fails with a declared error, which the kernel turns into an
|
|
334
|
+
abort of the state transaction the handler ran in (`@firedrill-run/world-kernel` `src/world-kernel.ts:424-433` and
|
|
335
|
+
`:794-811`; `ToolFailureOptions` has no commit option, `@firedrill-run/tool-sdk` `src/failure.ts:6-11`). A 402 with
|
|
336
|
+
persisted state is therefore not expressible, and `payment_intents.create/confirm` and `invoices.pay` declines
|
|
337
|
+
roll back as described under Operations. Failed attempts are persisted only through `subscriptions.create`.
|
|
338
|
+
Because the rollback also resets the id counters, a creation-time decline body (`payment_intents.create` with
|
|
339
|
+
`confirm=true`, `subscriptions.create` with `error_if_incomplete`) deliberately omits the PaymentIntent — it was
|
|
340
|
+
never created, and its id would 404 and then be reassigned to the next object — so it carries the error envelope
|
|
341
|
+
only, unlike Stripe's body; `/confirm` and `/pay` decline bodies name only the existing intent and pre-existing
|
|
342
|
+
PaymentMethod. Assert declines through the 402 envelope, through `subscriptions.create` state or through
|
|
343
|
+
`payment_intent.declined`.
|
|
344
|
+
- Money: integer minor units; the 50-minor-unit minimum applies to every currency (no per-currency minimums, no
|
|
345
|
+
zero-decimal handling beyond accepting `jpy` amounts as-is); twelve supported currencies; no conversion; customer
|
|
346
|
+
`balance` credit is shown but never applied; no taxes, discounts, coupons, shipping or application fees
|
|
347
|
+
(`total == subtotal`); partial capture does not create the release refund object.
|
|
348
|
+
- Invoices: `auto_advance` is stored but nothing advances automatically; no dunning, retries,
|
|
349
|
+
`next_payment_attempt`, e-mail, PDF or hosted page (`.test` URLs resolve nowhere); `paid_out_of_band` cancels the
|
|
350
|
+
intent instead of recording an out-of-band payment object; ad-hoc items carry `pricing: null`; `uncollectible`
|
|
351
|
+
is reachable only through starter data; the customer snapshot is refreshed at finalization.
|
|
352
|
+
- Subscriptions: no renewal cycling, no `past_due`/`unpaid`/`paused`/`incomplete_expired` transitions (starter
|
|
353
|
+
data only), no proration invoice items, one interval per subscription, `trial_end: "now"` activates without an
|
|
354
|
+
invoice, `cancel_at` is stored but never fires, `DELETE` accepts no body (`invoice_now`/`prorate`/
|
|
355
|
+
`cancellation_details` on cancel get the framework's 400 `HTTP_BODY_NOT_ALLOWED`), `billing_mode` is always
|
|
356
|
+
`flexible`.
|
|
357
|
+
- Balance uses a fixed two-day pending window attributed by charge date (a refund reduces its charge's bucket);
|
|
358
|
+
no payouts, fees or `instant_available`; `txn_…` ids have no object behind them.
|
|
359
|
+
- Lists scan the namespace with a bound of 10 000 rows and fail `400 state_bound_exceeded` rather than truncate.
|
|
360
|
+
`url` in list envelopes is a relative path.
|
|
361
|
+
- Authentication is the world token as `Authorization: Bearer`; restricted-key permissions are nine coarse groups
|
|
362
|
+
(no Stripe-granular scopes); the 403 wording is modelled on, not verified against, Stripe's. `Stripe-Version`,
|
|
363
|
+
`Stripe-Account` and `Stripe-Context` headers are accepted and ignored.
|
|
364
|
+
- Framework-level rejections (unknown parameters, schema type mismatches, `Idempotency-Key` longer than 255
|
|
365
|
+
characters, a body on `DELETE`) carry the framework's messages, not Stripe's `parameter_unknown` with `param`.
|
|
366
|
+
- Rate-limit and outage faults are scenario-selected only — no real per-second accounting.
|
|
367
|
+
- The browser app is a recreation of the Dashboard's core screens, not the whole product. Chrome for features outside
|
|
368
|
+
this Tool renders in place with its glyph, hover state and a "(not simulated)" tooltip, and opens a short
|
|
369
|
+
"… is not simulated by this Tool" panel instead of inventing data. Not simulated: sidebar Connect (Overview,
|
|
370
|
+
Connected accounts, Transfers), Payments › Analytics, Disputes, Radar, Payment Links, Terminal, Billing › Overview,
|
|
371
|
+
Usage-based, Revenue recovery, Reporting (Reports, Sigma, Revenue Recognition, Data management), More (Workflows,
|
|
372
|
+
Tax, Identity, Issuing, Financial Connections, Capital, Climate); the sandbox banner's "Switch to live account";
|
|
373
|
+
account menu "Switch to sandbox", "Manage sandboxes", "Create account"; top-bar Apps; Create menu "Payment link",
|
|
374
|
+
"Coupon", "Quote"; help menu "Documentation", "Contact support"; settings menu "Personal details", "Team and
|
|
375
|
+
security", "Branding", "Billing settings"; every list's "Export", "Edit columns" (plus Customers "Analyze" and
|
|
376
|
+
Product catalog "Export prices"); filter chips other than Payments "Date and time" and the Invoices customer
|
|
377
|
+
filter (Amount, Currency, Status, Payment method, Card, Created date, Due date, Type, Customer, Product, Price, More
|
|
378
|
+
filters); Customers segment cards other than All (Top customers, First-time customers, Repeat customers, Recent
|
|
379
|
+
customers, High refunds, High disputes: shown with a "–" count because segment analytics are not computed; All
|
|
380
|
+
counts `customers.list`); Product catalog section tabs Coupons, Shipping rates, Tax rates and Pricing tables (the
|
|
381
|
+
Products tab and its All / Active / Archived cards are live); Home "Add"/"Edit" widgets, the date-range, granularity
|
|
382
|
+
and comparison chips (the overview is fixed to 7 daily points), the "Dispute activity" and "MRR" overview widgets
|
|
383
|
+
and the Payouts card; payment "Add note" and "Events and logs"; customer "Events"; and the bulk actions on selected list rows ("Export selected", "Edit", "Delete" on Payments, Customers, Product catalog, Invoices and Subscriptions) — the row and header checkboxes select and deselect rows for real, but no bulk operation exists behind them. The Home "Net volume" widget is
|
|
384
|
+
captured charge volume minus succeeded refunds per day; Stripe fees are not modelled, so no fee is subtracted. Search matches
|
|
385
|
+
names, e-mails, descriptions and invoice numbers of the newest 1,000 records of each type (a note names every
|
|
386
|
+
group that stopped at that bound) and jumps to pasted ids (no full-text search). Payment status tabs count and filter a client-side index because the API has no status filter
|
|
387
|
+
for PaymentIntents. Shortcuts (pinned and recently visited pages) are a per-browser preference in localStorage.
|
|
388
|
+
Card brands are text chips, not network logos. Dates render in UTC. The sandbox banner's exact colours and copy are
|
|
389
|
+
modelled on Stripe's documented "banner at the top of the Dashboard", not verified pixel-for-pixel.
|
|
390
|
+
|
|
391
|
+
## Browser app
|
|
392
|
+
|
|
393
|
+
`firedrill serve` also serves a **Stripe Dashboard look-alike** (declared as `ui: { root: "app/site" }`): the
|
|
394
|
+
sandbox banner across the top ("You're testing in a sandbox…"), the account switcher, the left navigation (Home ·
|
|
395
|
+
Balances · Transactions · Customers · Product catalog, Shortcuts with pinned and recent pages, the Products groups
|
|
396
|
+
Connect · Payments · Billing · Reporting · More, Developers), the sidebar search with `/` (directly under the account switcher, as the current Dashboard places it) and the Collapse control at the sidebar foot that turns the navigation into an icon rail (remembered per browser; a Search button in the header opens the navigation on narrow screens), Developers, help (`?`), Apps,
|
|
397
|
+
notifications, settings and the round "+" Create menu, list pages with status cards, dashed filter chips, Export /
|
|
398
|
+
Edit columns, row-selection checkboxes with a floating "N selected" action bar, and cursor pagination (Payments, Customers,
|
|
399
|
+
Product catalog, Invoices, Subscriptions, Refunds, Transactions), detail pages with meta bar, timeline and
|
|
400
|
+
key/value sections (payment, customer with a right-hand Details rail, product, invoice, subscription), Home with a
|
|
401
|
+
Today gross-volume chart (hourly, virtual time), balance card and "Your overview" widgets (Gross volume, Successful
|
|
402
|
+
payments, Failed payments, Net volume and New customers computed from charges, refunds and customers), Customers
|
|
403
|
+
segment cards, Product catalog section tabs with All / Active / Archived counts, and the create flows: "Create a payment"
|
|
404
|
+
(customer picker, saved cards + Stripe's test cards, confirm/capture options, 402 declines shown inline),
|
|
405
|
+
"Add customer", "Add a product" with its first price, "Add a price", "Create an invoice" → draft editor (add
|
|
406
|
+
catalog or one-off items, finalize, charge, void, mark paid out of band), "Create a subscription" (multiple
|
|
407
|
+
prices, trial, collection method, payment behaviour), refund / capture / confirm / cancel dialogs, payment-method
|
|
408
|
+
attach/detach/default, subscription update/cancel/resume. No list stops silently at one request or at a bound.
|
|
409
|
+
Transactions "All activity" merges `charges.list` and `refunds.list` server-side page by page (one
|
|
410
|
+
`starting_after` cursor per stream, Previous / Next), so every charge and refund is reachable. A customer's payments,
|
|
411
|
+
subscriptions and invoices and a subscription's invoices have Previous / Next pagination. Where the app reads a
|
|
412
|
+
client-side index (a product's prices, a payment's refunds, a customer's cards, the Home overview metrics, payment
|
|
413
|
+
status tabs, the price, customer and card pickers and search), the index is read page by page with `starting_after`
|
|
414
|
+
up to 1,000 records. When that bound is reached the app says so: a `+` on counts and totals, "Partial: first N …" on
|
|
415
|
+
Home widgets, "Showing the first N …; more exist" on sections and pickers. The customer picker then looks up an exact
|
|
416
|
+
e-mail (`customers.list?email=`) or `cus_…` id on the server. Price pickers filter currency on the server and
|
|
417
|
+
expand `data.product`. Every control calls the same operations as the REST
|
|
418
|
+
and MCP surfaces through `/_firedrill/client.js` with idempotency keys on mutations; the page polls the world
|
|
419
|
+
revision and re-reads after external changes without discarding a form being edited. Restricted keys see
|
|
420
|
+
"This key does not have … permission" panels; framework-denied actors see a denied panel; faults surface as the
|
|
421
|
+
Dashboard's error banners. Every form label is linked to its control (`for`, or `aria-labelledby` on composite
|
|
422
|
+
controls such as the amount + currency group and the payment-method radio group), hints and field errors are
|
|
423
|
+
attached with `aria-describedby`, and ids come from a counter. "Today" and every date come from `dashboard.context.now` (virtual time), never from
|
|
424
|
+
the browser clock. The app bundles **Inter** (OFL) as the closest permissive match to Stripe's proprietary Söhne
|
|
425
|
+
face and the official Stripe marks downloaded from the `logos-apps` library; sources are listed in
|
|
426
|
+
`firedrill/tools/stripe/app/assets/ATTRIBUTION.md`.
|
|
427
|
+
|
|
428
|
+
## Trademarks
|
|
429
|
+
|
|
430
|
+
Stripe, the Stripe "S" icon and the Stripe wordmark are trademarks of Stripe, Inc. They are used only to identify
|
|
431
|
+
the service this package simulates in a test environment; there is no affiliation with or endorsement by Stripe.
|
|
432
|
+
Card network names (Visa, Mastercard, American Express) appearing as text labels are trademarks of their
|
|
433
|
+
respective owners and are used descriptively only.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"target": {
|
|
4
|
+
"id": "conformance-agent",
|
|
5
|
+
"kind": "command",
|
|
6
|
+
"bindings": [
|
|
7
|
+
"http",
|
|
8
|
+
"mcp"
|
|
9
|
+
],
|
|
10
|
+
"executable": "node",
|
|
11
|
+
"arguments": [
|
|
12
|
+
"test/conformance.mjs"
|
|
13
|
+
],
|
|
14
|
+
"workingDirectory": ".",
|
|
15
|
+
"timeoutMs": 120000
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "conformance",
|
|
4
|
+
"title": "Stripe Tool conformance: REST flow, MCP aliases, restricted keys, live mode, denial and every fault",
|
|
5
|
+
"drills": [
|
|
6
|
+
"stripe-rest-flow",
|
|
7
|
+
"stripe-mcp-aliases",
|
|
8
|
+
"stripe-restricted-key",
|
|
9
|
+
"stripe-no-permissions",
|
|
10
|
+
"stripe-live-mode",
|
|
11
|
+
"stripe-denied",
|
|
12
|
+
"stripe-rate-limited",
|
|
13
|
+
"stripe-api-unavailable",
|
|
14
|
+
"stripe-refund-committed-lost",
|
|
15
|
+
"stripe-large-pages"
|
|
16
|
+
]
|
|
17
|
+
}
|