@crvouga/mockingbird-service-stripe 2.2.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,6 +1,14 @@
1
1
  # Changelog — @crvouga/mockingbird-service-stripe
2
2
 
3
- ## 2.2.0 (2026-10-05)
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)
4
12
 
5
13
  ### Features
6
14
 
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-CHK6KZV4.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-PNBTN2AA.js.map
877
+ //# sourceMappingURL=chunk-4G5L7P7I.js.map
@@ -2337,7 +2337,7 @@ var stateAdminRoutes = (routes) => {
2337
2337
  };
2338
2338
 
2339
2339
  // ../core/dist/version.js
2340
- var PACKAGE_VERSION = true ? "2.2.0" : UNRELEASED_VERSION;
2340
+ var PACKAGE_VERSION = true ? "2.2.1" : UNRELEASED_VERSION;
2341
2341
 
2342
2342
  // ../core/dist/signing.js
2343
2343
  var encoder = new TextEncoder();
@@ -11817,9 +11817,120 @@ var stripeJs = (base2) => `/* Mockingbird Stripe.js stand-in */
11817
11817
  ["cvc", "CVC", "stripe-mock-cvc", "123"],
11818
11818
  ["zip", "ZIP", "stripe-mock-zip", "94107"],
11819
11819
  ];
11820
- 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) {
11821
11931
  this.type = type;
11822
11932
  this.options = options || {};
11933
+ this.group = group;
11823
11934
  this.node = null;
11824
11935
  this.handlers = {};
11825
11936
  }
@@ -11829,45 +11940,63 @@ var stripeJs = (base2) => `/* Mockingbird Stripe.js stand-in */
11829
11940
  var root = document.createElement("div");
11830
11941
  root.setAttribute("data-testid", "stripe-mock-element");
11831
11942
  root.setAttribute("data-element-type", this.type);
11832
- FIELDS.forEach(function (field) {
11833
- var label = document.createElement("label");
11834
- 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) {
11835
11946
  var input = document.createElement("input");
11836
11947
  input.name = field[0];
11837
11948
  input.setAttribute("data-testid", field[2]);
11949
+ input.setAttribute("aria-label", field[1]);
11838
11950
  input.placeholder = field[3];
11839
- label.appendChild(input);
11840
- 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
+ }
11841
11965
  });
11842
11966
  host.appendChild(root);
11843
11967
  this.node = root;
11844
- var self = this;
11845
- root.addEventListener("input", function () { self.emit("change", { complete: true, empty: false, elementType: self.type }); });
11846
11968
  setTimeout(function () { self.emit("ready", { elementType: self.type }); }, 0);
11847
11969
  return this;
11848
11970
  };
11849
11971
  Element.prototype.value = function (name) {
11850
11972
  var input = this.node && this.node.querySelector('[name="' + name + '"]');
11851
- 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 "";
11852
11980
  };
11853
11981
  Element.prototype.card = function () {
11854
- var exp = (this.value("exp") || "12/34").split("/");
11855
- var year = Number(exp[1] || "34");
11982
+ var exp = String(this.value("exp") || "").split("/");
11983
+ var year = Number(exp[1] || "0");
11856
11984
  return {
11857
- number: (this.value("card") || "4242424242424242").replace(/\\s+/g, ""),
11858
- exp_month: Number(exp[0] || "12"),
11985
+ number: digits(this.value("card")),
11986
+ exp_month: Number(exp[0] || "0"),
11859
11987
  exp_year: year < 100 ? 2000 + year : year,
11860
- cvc: this.value("cvc") || "123",
11988
+ cvc: digits(this.value("cvc")),
11861
11989
  };
11862
11990
  };
11991
+ Element.prototype.validation = function () { return stateOf(this); };
11863
11992
  Element.prototype.on = function (name, handler) { (this.handlers[name] = this.handlers[name] || []).push(handler); return this; };
11864
11993
  Element.prototype.off = function (name, handler) { this.handlers[name] = (this.handlers[name] || []).filter(function (h) { return h !== handler; }); return this; };
11865
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); };
11866
11995
  Element.prototype.emit = function (name, event) { (this.handlers[name] || []).forEach(function (h) { h(event); }); };
11867
- 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)); };
11868
11997
  Element.prototype.focus = function () { var input = this.node && this.node.querySelector("input"); if (input) input.focus(); };
11869
- Element.prototype.blur = function () {};
11870
- 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)); };
11871
12000
  Element.prototype.collapse = function () {};
11872
12001
  Element.prototype.unmount = function () { if (this.node && this.node.parentNode) this.node.parentNode.removeChild(this.node); this.node = null; };
11873
12002
  Element.prototype.destroy = Element.prototype.unmount;
@@ -11883,7 +12012,7 @@ var stripeJs = (base2) => `/* Mockingbird Stripe.js stand-in */
11883
12012
  return {
11884
12013
  _clientSecret: clientSecret,
11885
12014
  _elements: created,
11886
- 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; },
11887
12016
  getElement: function (type) {
11888
12017
  var elementType = type && type.__elementType ? type.__elementType : type;
11889
12018
  return created.filter(function (e) { return e.type === elementType || (e.type === "payment" && type && type.type === "payment"); })[0] || null;
@@ -11914,6 +12043,13 @@ var stripeJs = (base2) => `/* Mockingbird Stripe.js stand-in */
11914
12043
  "4000051230000072": "tok_hsa",
11915
12044
  };
11916
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
+ }
11917
12053
  var card = element && typeof element.card === "function" ? element.card() : null;
11918
12054
  var number = card && String(card.number || "").replace(/[\\s-]/g, "");
11919
12055
  var token = number && CARD_TOKENS[number];
@@ -13427,6 +13563,7 @@ var paymentMethodHandlers = (services) => ({
13427
13563
  render: renderPaymentMethod
13428
13564
  });
13429
13565
  const account = stringOf(params, "customer_account");
13566
+ if (context.query.customer_account === "") throw parameterInvalidEmpty("customer_account");
13430
13567
  if (account !== null && account !== "")
13431
13568
  throw resourceMissing("customer", account, "customer_account", 400);
13432
13569
  return jsonRes(200, page);
@@ -19019,4 +19156,4 @@ export {
19019
19156
  createRuntime2 as createRuntime,
19020
19157
  StripeAPI
19021
19158
  };
19022
- //# sourceMappingURL=chunk-CHK6KZV4.js.map
19159
+ //# sourceMappingURL=chunk-7ZVXTTT4.js.map