@crvouga/mockingbird-service-flex 3.10.0 → 4.0.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/CHANGELOG.md +6 -4
- package/README.md +12 -6
- package/dist/{chunk-NKBXNJ5B.js → chunk-3FSLZROR.js} +132 -23
- package/dist/{chunk-NKBXNJ5B.js.map → chunk-3FSLZROR.js.map} +3 -3
- package/dist/{chunk-OATZRDML.js → chunk-H6USVBBB.js} +3 -3
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +3 -1
- package/dist/server.d.ts +14 -1
- package/dist/server.js +2 -2
- package/package.json +1 -1
- /package/dist/{chunk-OATZRDML.js.map → chunk-H6USVBBB.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-flex
|
|
2
2
|
|
|
3
|
+
## 4.0.0 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### ⚠️ Breaking changes
|
|
6
|
+
|
|
7
|
+
- nest webhook objects under their resource key and send refunds as Flex does ([e460af6](https://github.com/crvouga/mockingbird/commit/e460af601280becdf8bd7c4f16dfada2cf181545))
|
|
8
|
+
|
|
3
9
|
## 3.10.0 (2026-10-24)
|
|
4
10
|
|
|
5
11
|
### Features
|
|
@@ -7,10 +13,6 @@
|
|
|
7
13
|
- add buffered SQL durability ([288bdcc](https://github.com/crvouga/mockingbird/commit/288bdcca84582787a54c4fdf9e803db9aac0b8d7))
|
|
8
14
|
- add efficient browser durability ([a8be579](https://github.com/crvouga/mockingbird/commit/a8be5799a883ddf6f731208e06f399410de4661a))
|
|
9
15
|
|
|
10
|
-
### Dependencies
|
|
11
|
-
|
|
12
|
-
- `@crvouga/mockingbird-service-sqlite`
|
|
13
|
-
|
|
14
16
|
## 3.9.0 (2026-10-21)
|
|
15
17
|
|
|
16
18
|
### Features
|
package/README.md
CHANGED
|
@@ -140,10 +140,14 @@ overrides the origin.
|
|
|
140
140
|
|
|
141
141
|
Svix-signed (`svix-id`, `svix-timestamp` = wall clock, `svix-signature: v1,<base64
|
|
142
142
|
HMAC-SHA256(key, "<id>.<ts>.<body>")>`, key = base64-decoded secret after `fwhsec_`/`whsec_`).
|
|
143
|
-
Body: `{event: {event_id, event_type, object, event_dt, test_mode, created_at}}`.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`
|
|
143
|
+
Body: `{event: {event_id, event_type, object, event_dt, test_mode, created_at}}`. As in Flex's
|
|
144
|
+
OpenAPI (`EventObject`) and production deliveries, `object` holds one key named for the
|
|
145
|
+
resource: checkout events `{checkout_session}` (so `object.checkout_session.checkout_session_id`),
|
|
146
|
+
payment-intent events `{payment_intent}`, `charge.*` events `{charge}` (its intent is
|
|
147
|
+
`charge.payment_intent`), `refund.*` and `charge.refund.updated` events `{refund}` (its intent is
|
|
148
|
+
`refund.payment_intent_id`), `customer.subscription.*` `{subscription}` and `product.updated`
|
|
149
|
+
`{product}`. Payment-intent, charge and refund objects name no checkout session: a receiver
|
|
150
|
+
finds the payment through the payment intent.
|
|
147
151
|
|
|
148
152
|
| Event | When |
|
|
149
153
|
| --- | --- |
|
|
@@ -152,7 +156,7 @@ intent plus `checkout_session_id`, refund events `checkout_session` and `payment
|
|
|
152
156
|
| `checkout.session.async_payment_succeeded` | settling a session whose intent was `processing` |
|
|
153
157
|
| `checkout.session.async_payment_failed` | a decline (page, admin, off-session) |
|
|
154
158
|
| `checkout.session.expired` | `…/expire`, or `expires_at` passing on the emulator clock (default 24 h) |
|
|
155
|
-
| `refund.created
|
|
159
|
+
| `refund.created` (twice), `charge.refunded`, `refund.updated`, `charge.refund.updated` | each refund, through the API or the dashboard (`POST /__admin/sessions/:id/refund`), in the order production delivers a dashboard refund. `checkout.session.refunded` is in Flex's event list but has never been seen on a refund, so it is only sent through `POST /__admin/events` |
|
|
156
160
|
| `product.updated` | `PATCH /v1/products/{id}` and `PUT /__admin/products/:id` |
|
|
157
161
|
| `checkout_session.completed`, `checkout_session.expired` | the aliases, with `PUT /__admin/settings {"eventNaming": "underscored"}` |
|
|
158
162
|
|
|
@@ -168,6 +172,7 @@ retried (immediately, 5 s, 5 min, 30 min, 2 h); `GET /__admin/webhooks`, `…/ev
|
|
|
168
172
|
| `POST /__admin/sessions/:id/complete` | `{card?}`: settle as if paid (HSA card unless `card` is `4242…`). |
|
|
169
173
|
| `POST /__admin/sessions/:id/decline` | Payment intent → `requires_payment_method`. |
|
|
170
174
|
| `POST /__admin/sessions/:id/expire` | Session → `expired`. |
|
|
175
|
+
| `POST /__admin/sessions/:id/refund` | `{amount?}`: a refund made in Flex's dashboard (full by default); emits the refund events. 409 when unpaid, 400 over the unrefunded amount. |
|
|
171
176
|
| `POST /__admin/sessions/:id/require_action` | `{next_action_type?}`: `collect_letter_of_medical_necessity` (default), `provide_second_payment_method`, `provide_alternative_payment_method`, `payment_failed`. |
|
|
172
177
|
| `PUT /__admin/sessions/:id/payment-intent` | `{status, amount_received?}`: `requires_payment_method`, `requires_action`, `processing`, `succeeded`, `canceled`. |
|
|
173
178
|
| `GET /__admin/sessions`, `GET /__admin/sessions/:id` | The namespace's sessions. |
|
|
@@ -221,11 +226,12 @@ so product names are synthesised.
|
|
|
221
226
|
|
|
222
227
|
| Export | Kind | Description |
|
|
223
228
|
| --- | --- | --- |
|
|
224
|
-
| `FlexAPI` | class | The in-process emulator: `fetch(request)`, `reset()`, `settle(id)`, `decline(id)`, `expire(id)`, `requireAction(id, type)`, `setPaymentIntent(id, patch)`, `applyRefund(id, amount)`, `putProduct(product)`, `emitFor(type, target)`, `tick()`, `sessions()`, `subscriptions()`, `present(session, view)`. Options: `sqlite`, `now`, `namespace`, `publicNamespace`, `products`, `settings`, `onEvent`. |
|
|
229
|
+
| `FlexAPI` | class | The in-process emulator: `fetch(request)`, `reset()`, `settle(id)`, `decline(id)`, `expire(id)`, `requireAction(id, type)`, `setPaymentIntent(id, patch)`, `applyRefund(id, amount)`, `dashboardRefund(id, amount?)`, `putProduct(product)`, `emitFor(type, target)`, `tick()`, `sessions()`, `subscriptions()`, `present(session, view)`. Options: `sqlite`, `now`, `namespace`, `publicNamespace`, `products`, `settings`, `onEvent`. |
|
|
225
230
|
| `createRuntime` | function | The emulator with the full service contract. Options: `webhooks: {url, secret, retryDelaysMs?, fetch?}`, `products`, `settings`, `tickMs`, `clock`, `seed`, `adminKey`, `onLog`. |
|
|
226
231
|
| `FLEX_PRESETS` | object | Every named fault preset. |
|
|
227
232
|
| `FLEX_NAMESPACE` | string | The service name, `"flex"`. |
|
|
228
233
|
| `FLEX_EVENT_TYPES` | array | Every webhook event type, aliases included. |
|
|
234
|
+
| `eventObjectKey` | function | The key an event type's `object` is nested under (`refund.created` → `refund`, `charge.refund.updated` → `refund`). |
|
|
229
235
|
| `keyMode` | function | `"test"` for `fsk_test_…`, `"live"` for `fsk_…`, otherwise `undefined`. |
|
|
230
236
|
| `isNextActionType` | function | Whether a string is a next-action type. |
|
|
231
237
|
| `CARDS`, `classifyCard`, `substituteSessionId` | values | The hosted page's test cards, its card classifier, and the `{CHECKOUT_SESSION_ID}` substitution. |
|
|
@@ -4097,7 +4097,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
4097
4097
|
};
|
|
4098
4098
|
|
|
4099
4099
|
// ../core/dist/version.js
|
|
4100
|
-
var PACKAGE_VERSION = true ? "
|
|
4100
|
+
var PACKAGE_VERSION = true ? "4.0.0" : UNRELEASED_VERSION;
|
|
4101
4101
|
|
|
4102
4102
|
// ../core/dist/signing.js
|
|
4103
4103
|
var encoder = new TextEncoder();
|
|
@@ -7295,6 +7295,17 @@ var adminRoutes = (runtime) => {
|
|
|
7295
7295
|
`session ${params.id}`
|
|
7296
7296
|
);
|
|
7297
7297
|
},
|
|
7298
|
+
"POST /sessions/:id/refund": ({ namespace, params, body }) => {
|
|
7299
|
+
if (body !== void 0 && body !== null && !isRecord5(body)) {
|
|
7300
|
+
return adminError4(400, 'expected {"amount"?: <cents>}');
|
|
7301
|
+
}
|
|
7302
|
+
const amount = isRecord5(body) ? body.amount : void 0;
|
|
7303
|
+
if (amount !== void 0 && typeof amount !== "number") {
|
|
7304
|
+
return adminError4(400, "amount: integer cents");
|
|
7305
|
+
}
|
|
7306
|
+
const outcome = runtime.instance(namespace).dashboardRefund(params.id, amount);
|
|
7307
|
+
return "session" in outcome ? json5(200, outcome.session) : adminError4(outcome.status, outcome.message);
|
|
7308
|
+
},
|
|
7298
7309
|
"PUT /sessions/:id/payment-intent": ({ namespace, params, body }) => {
|
|
7299
7310
|
if (!isRecord5(body) || !PAYMENT_INTENT_STATUSES.includes(String(body.status))) {
|
|
7300
7311
|
return adminError4(400, `status: ${PAYMENT_INTENT_STATUSES.join(" | ")}`);
|
|
@@ -7435,6 +7446,17 @@ var FLEX_EVENT_TYPES = [
|
|
|
7435
7446
|
"charge.refunded",
|
|
7436
7447
|
"charge.refund.updated"
|
|
7437
7448
|
];
|
|
7449
|
+
var eventObjectKey = (type) => {
|
|
7450
|
+
const prefixes2 = [
|
|
7451
|
+
["checkout.session.", "checkout_session"],
|
|
7452
|
+
["checkout_session.", "checkout_session"],
|
|
7453
|
+
["charge.refund.", "refund"],
|
|
7454
|
+
["charge.dispute.", "dispute"],
|
|
7455
|
+
["customer.subscription.", "subscription"]
|
|
7456
|
+
];
|
|
7457
|
+
const match = prefixes2.find(([prefix]) => type.startsWith(prefix));
|
|
7458
|
+
return match ? match[1] : type.split(".")[0];
|
|
7459
|
+
};
|
|
7438
7460
|
var keyMode = (key) => {
|
|
7439
7461
|
if (!key) return void 0;
|
|
7440
7462
|
if (/^fsk_test_[A-Za-z0-9_-]+$/.test(key)) return "test";
|
|
@@ -7910,7 +7932,11 @@ var FlexAPI = class {
|
|
|
7910
7932
|
this.applyRefund(id, amount);
|
|
7911
7933
|
return this.sessionResponse(context, id);
|
|
7912
7934
|
}
|
|
7913
|
-
/**
|
|
7935
|
+
/**
|
|
7936
|
+
* Refund part or all of a paid session, emitting the refund events in the order Flex sends
|
|
7937
|
+
* them for a dashboard refund: `refund.created` twice, `charge.refunded`, then
|
|
7938
|
+
* `refund.updated` and `charge.refund.updated`.
|
|
7939
|
+
*/
|
|
7914
7940
|
applyRefund(id, amount) {
|
|
7915
7941
|
const session = this.state.sessions.get(id);
|
|
7916
7942
|
if (!session) return void 0;
|
|
@@ -7925,20 +7951,74 @@ var FlexAPI = class {
|
|
|
7925
7951
|
created_at: this.iso()
|
|
7926
7952
|
};
|
|
7927
7953
|
this.state.refunds.insert(refund.refund_id, refund);
|
|
7928
|
-
const
|
|
7929
|
-
|
|
7930
|
-
|
|
7954
|
+
const refundObject = this.presentRefund(refund, session.test_mode);
|
|
7955
|
+
const chargeObject = this.presentCharge(next, refund.refund_id);
|
|
7956
|
+
this.emit("refund.created", refundObject, session.test_mode);
|
|
7957
|
+
this.emit("refund.created", refundObject, session.test_mode);
|
|
7958
|
+
this.emit("charge.refunded", chargeObject, session.test_mode);
|
|
7959
|
+
this.emit("refund.updated", refundObject, session.test_mode);
|
|
7960
|
+
this.emit("charge.refund.updated", refundObject, session.test_mode);
|
|
7961
|
+
return this.state.sessions.get(id);
|
|
7962
|
+
}
|
|
7963
|
+
/** Refund a paid session from Flex's dashboard (`POST /__admin/sessions/:id/refund`). */
|
|
7964
|
+
dashboardRefund(id, amount) {
|
|
7965
|
+
const session = this.state.sessions.get(id);
|
|
7966
|
+
if (!session) return { status: 404, message: `no session ${id}` };
|
|
7967
|
+
const received = session.amount_received ?? 0;
|
|
7968
|
+
if (session.status !== "complete" || received <= 0) {
|
|
7969
|
+
return { status: 409, message: `session ${id} has no payment to refund` };
|
|
7970
|
+
}
|
|
7971
|
+
const remaining = received - session.amount_refunded;
|
|
7972
|
+
const value = amount ?? remaining;
|
|
7973
|
+
if (!Number.isInteger(value) || value <= 0 || value > remaining) {
|
|
7974
|
+
return { status: 400, message: `amount must be between 1 and ${remaining}` };
|
|
7975
|
+
}
|
|
7976
|
+
return { session: this.applyRefund(id, value) };
|
|
7977
|
+
}
|
|
7978
|
+
presentRefund(refund, testMode) {
|
|
7979
|
+
return {
|
|
7980
|
+
refund_id: refund.refund_id,
|
|
7981
|
+
amount: refund.amount,
|
|
7982
|
+
status: refund.status,
|
|
7983
|
+
payment_intent_id: refund.payment_intent,
|
|
7984
|
+
items: [],
|
|
7985
|
+
metadata: null,
|
|
7986
|
+
reason: null,
|
|
7987
|
+
reference_id: null,
|
|
7988
|
+
reference_type: null,
|
|
7989
|
+
reference_status: null,
|
|
7990
|
+
balance_transaction: null,
|
|
7991
|
+
failure_balance_transaction: null,
|
|
7992
|
+
test_mode: testMode,
|
|
7993
|
+
created_at: refund.created_at
|
|
7994
|
+
};
|
|
7995
|
+
}
|
|
7996
|
+
presentCharge(session, seed) {
|
|
7997
|
+
const intent = session.payment_intent ? this.state.paymentIntents.get(session.payment_intent) : void 0;
|
|
7998
|
+
const received = session.amount_received ?? 0;
|
|
7999
|
+
return {
|
|
8000
|
+
charge_id: `fch_${seed.slice(4)}`,
|
|
7931
8001
|
payment_intent: session.payment_intent,
|
|
7932
|
-
amount:
|
|
7933
|
-
|
|
7934
|
-
|
|
8002
|
+
amount: received,
|
|
8003
|
+
amount_captured: received,
|
|
8004
|
+
amount_refunded: session.amount_refunded,
|
|
8005
|
+
refunded: session.amount_refunded >= received,
|
|
8006
|
+
paid: true,
|
|
8007
|
+
captured: true,
|
|
8008
|
+
status: "succeeded",
|
|
8009
|
+
customer: session.customer,
|
|
8010
|
+
payment_method_id: intent?.payment_method ?? null,
|
|
8011
|
+
invoice: null,
|
|
8012
|
+
refunds: null,
|
|
8013
|
+
disputed: false,
|
|
8014
|
+
failure_message: null,
|
|
8015
|
+
balance_transaction: null,
|
|
8016
|
+
failure_balance_transaction: null,
|
|
8017
|
+
application_fee_amount: null,
|
|
8018
|
+
transfer_group: null,
|
|
8019
|
+
test_mode: session.test_mode,
|
|
8020
|
+
created_at: intent?.created_at ?? this.iso()
|
|
7935
8021
|
};
|
|
7936
|
-
this.emit("refund.created", { ...refund, status: "pending" }, session.test_mode);
|
|
7937
|
-
this.emit("charge.refunded", charge, session.test_mode);
|
|
7938
|
-
this.emitSession("checkout.session.refunded", id);
|
|
7939
|
-
this.emit("refund.updated", refund, session.test_mode);
|
|
7940
|
-
this.emit("charge.refund.updated", { ...refund, charge: charge.charge_id }, session.test_mode);
|
|
7941
|
-
return this.state.sessions.get(id);
|
|
7942
8022
|
}
|
|
7943
8023
|
// --- customers and setup intents ---------------------------------------------------------
|
|
7944
8024
|
createCustomer(context) {
|
|
@@ -8052,11 +8132,7 @@ var FlexAPI = class {
|
|
|
8052
8132
|
const intent = { ...this.paymentIntentFor(session), ...patch };
|
|
8053
8133
|
this.state.paymentIntents.update(intent.payment_intent_id, intent);
|
|
8054
8134
|
if (patch.status === "succeeded") {
|
|
8055
|
-
this.emit(
|
|
8056
|
-
"payment_intent.succeeded",
|
|
8057
|
-
{ ...intent, checkout_session_id: id },
|
|
8058
|
-
session.test_mode
|
|
8059
|
-
);
|
|
8135
|
+
this.emit("payment_intent.succeeded", { ...intent }, session.test_mode);
|
|
8060
8136
|
}
|
|
8061
8137
|
return intent;
|
|
8062
8138
|
}
|
|
@@ -8183,8 +8259,40 @@ var FlexAPI = class {
|
|
|
8183
8259
|
const product = this.state.products.get(target.productId);
|
|
8184
8260
|
return product ? this.emit(type, { ...product }, product.test_mode) : void 0;
|
|
8185
8261
|
}
|
|
8186
|
-
if (target.sessionId) return
|
|
8187
|
-
|
|
8262
|
+
if (!target.sessionId) return void 0;
|
|
8263
|
+
const session = this.state.sessions.get(target.sessionId);
|
|
8264
|
+
if (!session) return void 0;
|
|
8265
|
+
const key = eventObjectKey(type);
|
|
8266
|
+
const existingIntent = session.payment_intent ? this.state.paymentIntents.get(session.payment_intent) : void 0;
|
|
8267
|
+
if (key === "payment_intent") {
|
|
8268
|
+
const intent = existingIntent ?? {
|
|
8269
|
+
payment_intent_id: `fpi_${session.checkout_session_id.slice(4)}`,
|
|
8270
|
+
amount: session.amount_total,
|
|
8271
|
+
amount_received: null,
|
|
8272
|
+
customer: session.customer,
|
|
8273
|
+
payment_method: null,
|
|
8274
|
+
status: "requires_payment_method",
|
|
8275
|
+
created_at: session.created_at
|
|
8276
|
+
};
|
|
8277
|
+
return this.emit(type, { ...intent }, session.test_mode);
|
|
8278
|
+
}
|
|
8279
|
+
if (key === "refund" || key === "charge") {
|
|
8280
|
+
const refund = this.state.refunds.list().map((row) => row.value).filter((row) => row.checkout_session === session.checkout_session_id).at(-1) ?? {
|
|
8281
|
+
refund_id: this.state.nextId("fre_"),
|
|
8282
|
+
checkout_session: session.checkout_session_id,
|
|
8283
|
+
payment_intent: session.payment_intent,
|
|
8284
|
+
amount: session.amount_received ?? session.amount_total,
|
|
8285
|
+
status: "succeeded",
|
|
8286
|
+
created_at: this.iso()
|
|
8287
|
+
};
|
|
8288
|
+
const current = this.state.sessions.get(session.checkout_session_id);
|
|
8289
|
+
return this.emit(
|
|
8290
|
+
type,
|
|
8291
|
+
key === "refund" ? this.presentRefund(refund, session.test_mode) : this.presentCharge(current, refund.refund_id),
|
|
8292
|
+
session.test_mode
|
|
8293
|
+
);
|
|
8294
|
+
}
|
|
8295
|
+
return this.emitSession(type, target.sessionId);
|
|
8188
8296
|
}
|
|
8189
8297
|
emitSession(type, id) {
|
|
8190
8298
|
const session = this.state.sessions.get(id);
|
|
@@ -8199,7 +8307,7 @@ var FlexAPI = class {
|
|
|
8199
8307
|
const event = {
|
|
8200
8308
|
event_id: this.state.nextId("fevt_"),
|
|
8201
8309
|
event_type: type,
|
|
8202
|
-
object,
|
|
8310
|
+
object: { [eventObjectKey(type)]: object },
|
|
8203
8311
|
event_dt: Math.floor(this.now() / 1e3),
|
|
8204
8312
|
test_mode: testMode,
|
|
8205
8313
|
created_at: this.iso()
|
|
@@ -8327,9 +8435,10 @@ export {
|
|
|
8327
8435
|
createRuntime2 as createRuntime,
|
|
8328
8436
|
FLEX_NAMESPACE,
|
|
8329
8437
|
FLEX_EVENT_TYPES,
|
|
8438
|
+
eventObjectKey,
|
|
8330
8439
|
keyMode,
|
|
8331
8440
|
periodEnd,
|
|
8332
8441
|
FlexAPI,
|
|
8333
8442
|
isNextActionType
|
|
8334
8443
|
};
|
|
8335
|
-
//# sourceMappingURL=chunk-
|
|
8444
|
+
//# sourceMappingURL=chunk-3FSLZROR.js.map
|