@crvouga/mockingbird-service-stripe 2.1.0 → 2.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog — @crvouga/mockingbird-service-stripe
2
2
 
3
+ ## 2.2.1 (2026-10-05)
4
+
5
+ ### Fixes and improvements
6
+
7
+ - validate card element input ([d53f042](https://github.com/crvouga/mockingbird/commit/d53f042089ee20b2708b489e5412065013309a55))
8
+ - reject empty customer account filters ([b7910c7](https://github.com/crvouga/mockingbird/commit/b7910c73ad24fb1ae6767fc83effce76bfd09eaa))
9
+ - render split card elements independently ([99e9a5d](https://github.com/crvouga/mockingbird/commit/99e9a5d82f9189ca5d347ff5d071d05f2d9fbf21))
10
+
11
+ ## 2.2.0 (2026-10-04)
12
+
13
+ ### Features
14
+
15
+ - add scripted local API mock ([4a76a11](https://github.com/crvouga/mockingbird/commit/4a76a114e2efad45934e0a93cf29576fd418e15c))
16
+
17
+ ### Fixes and improvements
18
+
19
+ - support embedded route parameters ([127cad9](https://github.com/crvouga/mockingbird/commit/127cad997ef106950c1b866070eea191ba54403f))
20
+
3
21
  ## 2.1.0 (2026-10-04)
4
22
 
5
23
  ### Features
@@ -12,10 +30,6 @@
12
30
  - fill the embedded viewport ([c1a35a4](https://github.com/crvouga/mockingbird/commit/c1a35a4789389ee9cd06c286d09bd47c4d06c71e))
13
31
  - make dialog cleanup immediate ([0c1a3c5](https://github.com/crvouga/mockingbird/commit/0c1a3c5b12584d50c19dcae595b951b8e5417b9c))
14
32
 
15
- ### Dependencies
16
-
17
- - `@crvouga/mockingbird-service-sqlite`
18
-
19
33
  ## 2.0.0 (2026-10-04)
20
34
 
21
35
  ### ⚠️ Breaking changes
package/README.md CHANGED
@@ -252,7 +252,12 @@ finalize; a customer credit balance is applied at finalize; `void` works only on
252
252
  payment mode, inactive prices, mixed currencies, quantities below 1, `trial_end` under 48 h.
253
253
  - `POST /__admin/checkout/sessions/:id/complete {"card": "4242…"}` does the same without a browser;
254
254
  `…/expire` and `…/async_payment_succeeded` too.
255
- - `GET /v3` — the Stripe.js stand-in: `Stripe(pk)`, `elements()` → `create("payment"|"card")`,
255
+ - `GET /v3` — the Stripe.js stand-in: `Stripe(pk)`, `elements()` → combined `create("payment"|"card")`
256
+ or split `create("cardNumber"|"cardExpiry"|"cardCvc")` inputs. Split inputs share card data
257
+ within their `Elements` group, add no visible labels, and the combined card honors
258
+ `hidePostalCode`. Card inputs format number/expiry/CVC values, report field-level `empty`,
259
+ `complete`, `brand`, and validation errors through change events, and refuse tokenization until
260
+ all required split or combined values are valid. It also provides
256
261
  `confirmPayment`, `confirmSetup`, `confirmCardPayment`, `confirmCardSetup`,
257
262
  `retrievePaymentIntent`, `retrieveSetupIntent`, `createPaymentMethod`, `handleCardAction`. It calls
258
263
  `POST /v1/{payment,setup}_intents/:id/confirm` with the publishable key and `client_secret` (the
@@ -4,7 +4,7 @@ import {
4
4
  parseDuration,
5
5
  resolveAdminPrefix,
6
6
  validateAccounts
7
- } from "./chunk-LUKVBQDH.js";
7
+ } from "./chunk-7ZVXTTT4.js";
8
8
 
9
9
  // src/server.ts
10
10
  import { readFileSync } from "node:fs";
@@ -874,4 +874,4 @@ export {
874
874
  createServer2 as createServer,
875
875
  serveTarget
876
876
  };
877
- //# sourceMappingURL=chunk-IWIBLNQX.js.map
877
+ //# sourceMappingURL=chunk-4G5L7P7I.js.map
@@ -1028,7 +1028,40 @@ var decodeBody = (contentType, bytes) => {
1028
1028
  return { kind: "bytes", value: bytes };
1029
1029
  };
1030
1030
  var readBody = async (message) => {
1031
- const bytes = new Uint8Array(await message.arrayBuffer());
1031
+ if (!(message instanceof Request) || !message.body) {
1032
+ if (message instanceof Request)
1033
+ message.signal.throwIfAborted();
1034
+ return decodeBody(message.headers.get("content-type"), new Uint8Array(await message.arrayBuffer()));
1035
+ }
1036
+ message.signal.throwIfAborted();
1037
+ const reader = message.body.getReader();
1038
+ const cancel = () => {
1039
+ void reader.cancel(message.signal.reason).catch(() => {
1040
+ });
1041
+ };
1042
+ message.signal.addEventListener("abort", cancel, { once: true });
1043
+ const chunks = [];
1044
+ let size = 0;
1045
+ try {
1046
+ for (; ; ) {
1047
+ message.signal.throwIfAborted();
1048
+ const { done, value } = await reader.read();
1049
+ message.signal.throwIfAborted();
1050
+ if (done)
1051
+ break;
1052
+ chunks.push(value);
1053
+ size += value.byteLength;
1054
+ }
1055
+ } finally {
1056
+ message.signal.removeEventListener("abort", cancel);
1057
+ reader.releaseLock();
1058
+ }
1059
+ const bytes = new Uint8Array(size);
1060
+ let offset = 0;
1061
+ for (const chunk of chunks) {
1062
+ bytes.set(chunk, offset);
1063
+ offset += chunk.byteLength;
1064
+ }
1032
1065
  return decodeBody(message.headers.get("content-type"), bytes);
1033
1066
  };
1034
1067
 
@@ -1743,7 +1776,42 @@ var verifyOperations = (document2, handlers) => {
1743
1776
  }
1744
1777
  return problems;
1745
1778
  };
1746
- var honoPath = (template) => template.replace(/\{([^}]+)\}/g, ":$1");
1779
+ var escapePattern = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1780
+ var serviceRoute = (template) => {
1781
+ const names = [];
1782
+ let pattern = "", end = 0;
1783
+ for (const match of template.matchAll(/\{([^}]+)\}/g)) {
1784
+ pattern += `${escapePattern(template.slice(end, match.index))}([^/]+?)`;
1785
+ const name = match[1];
1786
+ if (!name)
1787
+ throw new Error(`Invalid OpenAPI route template: ${template}`);
1788
+ names.push(name);
1789
+ end = match.index + match[0].length;
1790
+ }
1791
+ pattern += escapePattern(template.slice(end));
1792
+ const matcher = new RegExp(`^${pattern}$`);
1793
+ const path = template.split("/").map((segment, index) => {
1794
+ if (!segment.includes("{"))
1795
+ return segment;
1796
+ const literal = /^\{([^}]+)\}$/.exec(segment);
1797
+ if (literal)
1798
+ return `:${literal[1]}`;
1799
+ let expression = "", offset = 0;
1800
+ for (const match of segment.matchAll(/\{[^}]+\}/g)) {
1801
+ expression += `${escapePattern(segment.slice(offset, match.index))}[^/]+?`;
1802
+ offset = match.index + match[0].length;
1803
+ }
1804
+ expression += escapePattern(segment.slice(offset));
1805
+ return `:mockingbirdSegment${index}{${expression}}`;
1806
+ }).join("/");
1807
+ return {
1808
+ path,
1809
+ params: (pathname) => {
1810
+ const values = matcher.exec(pathname);
1811
+ return Object.fromEntries(names.map((name, index) => [name, decodeURIComponent(values?.[index + 1] ?? "")]));
1812
+ }
1813
+ };
1814
+ };
1747
1815
  var routeOrder = (a, b) => {
1748
1816
  const sa = a.path.split("/");
1749
1817
  const sb = b.path.split("/");
@@ -1778,6 +1846,7 @@ var createService = (options) => {
1778
1846
  for (const operation of operations) {
1779
1847
  const metadata = operationMetadata(operation.operation);
1780
1848
  const handler = options.handlers[operation.operationId];
1849
+ const registeredRoute = serviceRoute(operation.path);
1781
1850
  const route = async (c) => {
1782
1851
  const request = c.req.raw;
1783
1852
  if (!metadata.supported || !handler) {
@@ -1787,7 +1856,7 @@ var createService = (options) => {
1787
1856
  const context = {
1788
1857
  request,
1789
1858
  url,
1790
- params: c.req.param(),
1859
+ params: registeredRoute.params(url.pathname),
1791
1860
  query: queryOf(url),
1792
1861
  body: await readBody(request),
1793
1862
  sqlite: options.sqlite,
@@ -1799,7 +1868,7 @@ var createService = (options) => {
1799
1868
  const short = await options.before?.(context);
1800
1869
  return withIssues(request, short ?? await handler(context));
1801
1870
  };
1802
- app.on(operation.method.toUpperCase(), honoPath(operation.path), route);
1871
+ app.on(operation.method.toUpperCase(), registeredRoute.path, route);
1803
1872
  }
1804
1873
  return {
1805
1874
  app,
@@ -2268,7 +2337,7 @@ var stateAdminRoutes = (routes) => {
2268
2337
  };
2269
2338
 
2270
2339
  // ../core/dist/version.js
2271
- var PACKAGE_VERSION = true ? "2.1.0" : UNRELEASED_VERSION;
2340
+ var PACKAGE_VERSION = true ? "2.2.1" : UNRELEASED_VERSION;
2272
2341
 
2273
2342
  // ../core/dist/signing.js
2274
2343
  var encoder = new TextEncoder();
@@ -11748,9 +11817,120 @@ var stripeJs = (base2) => `/* Mockingbird Stripe.js stand-in */
11748
11817
  ["cvc", "CVC", "stripe-mock-cvc", "123"],
11749
11818
  ["zip", "ZIP", "stripe-mock-zip", "94107"],
11750
11819
  ];
11751
- function Element(type, options) {
11820
+ function fieldsFor(type, options) {
11821
+ var names = type === "cardNumber" ? ["card"]
11822
+ : type === "cardExpiry" ? ["exp"]
11823
+ : type === "cardCvc" ? ["cvc"]
11824
+ : type === "postalCode" ? ["zip"]
11825
+ : FIELDS.map(function (field) { return field[0]; });
11826
+ if (type === "card" && options && options.hidePostalCode) {
11827
+ names = names.filter(function (name) { return name !== "zip"; });
11828
+ }
11829
+ return FIELDS.filter(function (field) { return names.indexOf(field[0]) !== -1; });
11830
+ }
11831
+ function digits(value) { return String(value || "").replace(/\\D/g, ""); }
11832
+ function brandOf(value) {
11833
+ var number = digits(value);
11834
+ if (/^3[47]/.test(number)) return "amex";
11835
+ if (/^(5[1-5]|2[2-7])/.test(number)) return "mastercard";
11836
+ if (/^(6011|65)/.test(number)) return "discover";
11837
+ if (/^4/.test(number)) return "visa";
11838
+ return "unknown";
11839
+ }
11840
+ function luhn(number) {
11841
+ var sum = 0;
11842
+ var alternate = false;
11843
+ for (var index = number.length - 1; index >= 0; index -= 1) {
11844
+ var digit = Number(number[index]);
11845
+ if (alternate) { digit *= 2; if (digit > 9) digit -= 9; }
11846
+ sum += digit;
11847
+ alternate = !alternate;
11848
+ }
11849
+ return number.length > 0 && sum % 10 === 0;
11850
+ }
11851
+ function cardError(code, message) {
11852
+ return { type: "validation_error", code: code, message: message };
11853
+ }
11854
+ function validateField(element, name) {
11855
+ var raw = element.value(name);
11856
+ if (name === "card") {
11857
+ var number = digits(raw);
11858
+ var brand = brandOf(number);
11859
+ var expected = brand === "amex" ? 15 : 16;
11860
+ if (!number || number.length < expected) return cardError("incomplete_number", "Your card number is incomplete.");
11861
+ if (number.length !== expected || !luhn(number)) return cardError("invalid_number", "Your card number is invalid.");
11862
+ }
11863
+ if (name === "exp") {
11864
+ var expiry = digits(raw);
11865
+ if (expiry.length < 4) return cardError("incomplete_expiry", "Your card's expiration date is incomplete.");
11866
+ var month = Number(expiry.slice(0, 2));
11867
+ var year = 2000 + Number(expiry.slice(2, 4));
11868
+ if (month < 1 || month > 12) return cardError("invalid_expiry_month", "Your card's expiration month is invalid.");
11869
+ var now = new Date();
11870
+ if (year < now.getFullYear() || (year === now.getFullYear() && month < now.getMonth() + 1)) {
11871
+ return cardError("invalid_expiry_year_past", "Your card's expiration year is in the past.");
11872
+ }
11873
+ }
11874
+ if (name === "cvc") {
11875
+ var cvc = digits(raw);
11876
+ var cvcLength = brandOf(element.value("card")) === "amex" ? 4 : 3;
11877
+ if (cvc.length < cvcLength) return cardError("incomplete_cvc", "Your card's security code is incomplete.");
11878
+ if (cvc.length !== cvcLength) return cardError("invalid_cvc", "Your card's security code is invalid.");
11879
+ }
11880
+ if (name === "zip" && !String(raw || "").trim()) {
11881
+ return cardError("incomplete_zip", "Your postal code is incomplete.");
11882
+ }
11883
+ return null;
11884
+ }
11885
+ function stateOf(element) {
11886
+ var fields = fieldsFor(element.type, element.options);
11887
+ var empty = fields.every(function (field) { return String(element.value(field[0]) || "") === ""; });
11888
+ var error = null;
11889
+ for (var index = 0; index < fields.length && !error; index += 1) error = validateField(element, fields[index][0]);
11890
+ var event = { complete: !error, empty: empty, elementType: element.type };
11891
+ if (error) event.error = error;
11892
+ if (element.type === "card" || element.type === "cardNumber") event.brand = brandOf(element.value("card"));
11893
+ return event;
11894
+ }
11895
+ function cardValidationOf(element) {
11896
+ var card = element.card();
11897
+ var values = {
11898
+ card: card.number,
11899
+ exp: String(card.exp_month || "").padStart(2, "0") + "/" + String(card.exp_year || "").slice(-2),
11900
+ cvc: card.cvc,
11901
+ };
11902
+ var cardView = { value: function (name) { return values[name] || ""; } };
11903
+ var names = ["card", "exp", "cvc"];
11904
+ for (var index = 0; index < names.length; index += 1) {
11905
+ var error = validateField(cardView, names[index]);
11906
+ if (error) return error;
11907
+ }
11908
+ return null;
11909
+ }
11910
+ function formatInput(element, input) {
11911
+ if (input.name === "card") {
11912
+ var number = digits(input.value).slice(0, 19);
11913
+ var brand = brandOf(number);
11914
+ var sizes = brand === "amex" ? [4, 6, 5] : [4, 4, 4, 4, 3];
11915
+ var groups = [];
11916
+ var offset = 0;
11917
+ sizes.forEach(function (size) {
11918
+ if (offset < number.length) groups.push(number.slice(offset, offset + size));
11919
+ offset += size;
11920
+ });
11921
+ input.value = groups.join(" ");
11922
+ } else if (input.name === "exp") {
11923
+ var expiry = digits(input.value).slice(0, 4);
11924
+ input.value = expiry.length > 2 ? expiry.slice(0, 2) + "/" + expiry.slice(2) : expiry;
11925
+ } else if (input.name === "cvc") {
11926
+ var limit = brandOf(element.value("card")) === "amex" ? 4 : 3;
11927
+ input.value = digits(input.value).slice(0, limit);
11928
+ }
11929
+ }
11930
+ function Element(type, options, group) {
11752
11931
  this.type = type;
11753
11932
  this.options = options || {};
11933
+ this.group = group;
11754
11934
  this.node = null;
11755
11935
  this.handlers = {};
11756
11936
  }
@@ -11760,45 +11940,63 @@ var stripeJs = (base2) => `/* Mockingbird Stripe.js stand-in */
11760
11940
  var root = document.createElement("div");
11761
11941
  root.setAttribute("data-testid", "stripe-mock-element");
11762
11942
  root.setAttribute("data-element-type", this.type);
11763
- FIELDS.forEach(function (field) {
11764
- var label = document.createElement("label");
11765
- label.textContent = field[1];
11943
+ var split = this.type === "cardNumber" || this.type === "cardExpiry" || this.type === "cardCvc" || this.type === "postalCode";
11944
+ var self = this;
11945
+ fieldsFor(this.type, this.options).forEach(function (field) {
11766
11946
  var input = document.createElement("input");
11767
11947
  input.name = field[0];
11768
11948
  input.setAttribute("data-testid", field[2]);
11949
+ input.setAttribute("aria-label", field[1]);
11769
11950
  input.placeholder = field[3];
11770
- label.appendChild(input);
11771
- root.appendChild(label);
11951
+ input.addEventListener("input", function () {
11952
+ formatInput(self, input);
11953
+ self.emit("change", stateOf(self));
11954
+ });
11955
+ input.addEventListener("focus", function () { self.emit("focus", { elementType: self.type }); });
11956
+ input.addEventListener("blur", function () { self.emit("blur", { elementType: self.type }); });
11957
+ if (split) {
11958
+ root.appendChild(input);
11959
+ } else {
11960
+ var label = document.createElement("label");
11961
+ label.textContent = field[1];
11962
+ label.appendChild(input);
11963
+ root.appendChild(label);
11964
+ }
11772
11965
  });
11773
11966
  host.appendChild(root);
11774
11967
  this.node = root;
11775
- var self = this;
11776
- root.addEventListener("input", function () { self.emit("change", { complete: true, empty: false, elementType: self.type }); });
11777
11968
  setTimeout(function () { self.emit("ready", { elementType: self.type }); }, 0);
11778
11969
  return this;
11779
11970
  };
11780
11971
  Element.prototype.value = function (name) {
11781
11972
  var input = this.node && this.node.querySelector('[name="' + name + '"]');
11782
- return input ? input.value : "";
11973
+ if (input) return input.value;
11974
+ for (var index = 0; index < this.group.length; index += 1) {
11975
+ var sibling = this.group[index];
11976
+ var siblingInput = sibling !== this && sibling.node && sibling.node.querySelector('[name="' + name + '"]');
11977
+ if (siblingInput) return siblingInput.value;
11978
+ }
11979
+ return "";
11783
11980
  };
11784
11981
  Element.prototype.card = function () {
11785
- var exp = (this.value("exp") || "12/34").split("/");
11786
- var year = Number(exp[1] || "34");
11982
+ var exp = String(this.value("exp") || "").split("/");
11983
+ var year = Number(exp[1] || "0");
11787
11984
  return {
11788
- number: (this.value("card") || "4242424242424242").replace(/\\s+/g, ""),
11789
- exp_month: Number(exp[0] || "12"),
11985
+ number: digits(this.value("card")),
11986
+ exp_month: Number(exp[0] || "0"),
11790
11987
  exp_year: year < 100 ? 2000 + year : year,
11791
- cvc: this.value("cvc") || "123",
11988
+ cvc: digits(this.value("cvc")),
11792
11989
  };
11793
11990
  };
11991
+ Element.prototype.validation = function () { return stateOf(this); };
11794
11992
  Element.prototype.on = function (name, handler) { (this.handlers[name] = this.handlers[name] || []).push(handler); return this; };
11795
11993
  Element.prototype.off = function (name, handler) { this.handlers[name] = (this.handlers[name] || []).filter(function (h) { return h !== handler; }); return this; };
11796
11994
  Element.prototype.once = function (name, handler) { var self = this; var wrapped = function (e) { self.off(name, wrapped); handler(e); }; return this.on(name, wrapped); };
11797
11995
  Element.prototype.emit = function (name, event) { (this.handlers[name] || []).forEach(function (h) { h(event); }); };
11798
- Element.prototype.update = function (options) { this.options = Object.assign(this.options, options || {}); };
11996
+ Element.prototype.update = function (options) { this.options = Object.assign(this.options, options || {}); if (this.node) this.emit("change", stateOf(this)); };
11799
11997
  Element.prototype.focus = function () { var input = this.node && this.node.querySelector("input"); if (input) input.focus(); };
11800
- Element.prototype.blur = function () {};
11801
- Element.prototype.clear = function () { if (this.node) this.node.querySelectorAll("input").forEach(function (i) { i.value = ""; }); };
11998
+ Element.prototype.blur = function () { var input = this.node && this.node.querySelector("input"); if (input && input.blur) input.blur(); };
11999
+ Element.prototype.clear = function () { if (this.node) this.node.querySelectorAll("input").forEach(function (i) { i.value = ""; }); this.emit("change", stateOf(this)); };
11802
12000
  Element.prototype.collapse = function () {};
11803
12001
  Element.prototype.unmount = function () { if (this.node && this.node.parentNode) this.node.parentNode.removeChild(this.node); this.node = null; };
11804
12002
  Element.prototype.destroy = Element.prototype.unmount;
@@ -11814,7 +12012,7 @@ var stripeJs = (base2) => `/* Mockingbird Stripe.js stand-in */
11814
12012
  return {
11815
12013
  _clientSecret: clientSecret,
11816
12014
  _elements: created,
11817
- create: function (type, opts) { var element = new Element(type, opts); created.push(element); return element; },
12015
+ create: function (type, opts) { var element = new Element(type, opts, created); created.push(element); return element; },
11818
12016
  getElement: function (type) {
11819
12017
  var elementType = type && type.__elementType ? type.__elementType : type;
11820
12018
  return created.filter(function (e) { return e.type === elementType || (e.type === "payment" && type && type.type === "payment"); })[0] || null;
@@ -11845,6 +12043,13 @@ var stripeJs = (base2) => `/* Mockingbird Stripe.js stand-in */
11845
12043
  "4000051230000072": "tok_hsa",
11846
12044
  };
11847
12045
  Stripe.prototype.createToken = function (element) {
12046
+ var validation = element && typeof element.validation === "function" ? cardValidationOf(element) : null;
12047
+ if (validation) {
12048
+ return Promise.resolve({ error: validation });
12049
+ }
12050
+ if (!element) {
12051
+ return Promise.resolve({ error: cardError("incomplete_number", "Your card number is incomplete.") });
12052
+ }
11848
12053
  var card = element && typeof element.card === "function" ? element.card() : null;
11849
12054
  var number = card && String(card.number || "").replace(/[\\s-]/g, "");
11850
12055
  var token = number && CARD_TOKENS[number];
@@ -13358,6 +13563,7 @@ var paymentMethodHandlers = (services) => ({
13358
13563
  render: renderPaymentMethod
13359
13564
  });
13360
13565
  const account = stringOf(params, "customer_account");
13566
+ if (context.query.customer_account === "") throw parameterInvalidEmpty("customer_account");
13361
13567
  if (account !== null && account !== "")
13362
13568
  throw resourceMissing("customer", account, "customer_account", 400);
13363
13569
  return jsonRes(200, page);
@@ -18950,4 +19156,4 @@ export {
18950
19156
  createRuntime2 as createRuntime,
18951
19157
  StripeAPI
18952
19158
  };
18953
- //# sourceMappingURL=chunk-LUKVBQDH.js.map
19159
+ //# sourceMappingURL=chunk-7ZVXTTT4.js.map