@flopay/shared 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -188,6 +188,18 @@ Country-aware postcode format validation, a **faithful mirror of the billing API
188
188
  | `getPostalCodeExample(country)` | The example postcode the backend embeds in its 400 message (US `12345 or 12345-6789`, GB `SW1A 1AA`, CA `A1A 1A1`, …), or `undefined` when there is none. Kept verbatim in sync with the backend's `POSTAL_CODE_EXAMPLES` so the SDK's inline hint and the server's rejection message never contradict |
189
189
  | `getStateFromPostalCode(country, postalCode)` | Derives a US state / CA province code from a postcode for AVS enrichment; `null` for other countries or a malformed code |
190
190
 
191
+ ### Idempotency Helpers
192
+
193
+ Primitives for the stable checkout-create `Idempotency-Key` (TeamFloPay/backend#972). `@flopay/js` and `@flopay/react` use these automatically; import them only if you build your own create flow.
194
+
195
+ | Export | Description |
196
+ |--------|-------------|
197
+ | `IDEMPOTENCY_KEY_HEADER` | The header name (`'Idempotency-Key'`) the SDK sends on `POST /v1/checkouts/sessions`. Optional on the backend — absent means the legacy, non-idempotent path. |
198
+ | `MAX_IDEMPOTENCY_KEY_LENGTH` | `255` — the documented max length a supplied key may have. |
199
+ | `IDEMPOTENCY_IN_PROGRESS_CODE` | Backend `code` (`'IdempotencyKeyInProgress'`) marking a **retryable** in-progress replay: retry with the *same* key. Distinct from a payload-conflict (`409`), which is non-retryable. |
200
+ | `generateIdempotencyKey()` | Returns a cryptographically random, high-entropy key (`crypto.randomUUID()`, else 16 random bytes). Returns `undefined` when no secure RNG exists, so callers omit the header rather than emit a weak key. Never derived from request data. |
201
+ | `resolveIdempotencyKey(supplied?)` | Returns a supplied key unchanged after validating it is non-empty and ≤ `MAX_IDEMPOTENCY_KEY_LENGTH` (throws `FloPayError('validation_error')` otherwise, never echoing the value), or a freshly generated key when none is supplied. |
202
+
191
203
  ```ts
192
204
  import { isPostalCodeSupported, isValidPostalCode, getPostalCodeExample } from '@flopay/shared';
193
205
 
package/dist/index.cjs CHANGED
@@ -58,6 +58,9 @@ __export(index_exports, {
58
58
  FloPayError: () => FloPayError,
59
59
  GLASS_DARK_APPEARANCE: () => GLASS_DARK_APPEARANCE,
60
60
  GLASS_LIGHT_APPEARANCE: () => GLASS_LIGHT_APPEARANCE,
61
+ IDEMPOTENCY_IN_PROGRESS_CODE: () => IDEMPOTENCY_IN_PROGRESS_CODE,
62
+ IDEMPOTENCY_KEY_HEADER: () => IDEMPOTENCY_KEY_HEADER,
63
+ MAX_IDEMPOTENCY_KEY_LENGTH: () => MAX_IDEMPOTENCY_KEY_LENGTH,
61
64
  MODERN_DARK_APPEARANCE: () => MODERN_DARK_APPEARANCE,
62
65
  MODERN_LIGHT_APPEARANCE: () => MODERN_LIGHT_APPEARANCE,
63
66
  NIGHT_APPEARANCE: () => NIGHT_APPEARANCE,
@@ -81,6 +84,7 @@ __export(index_exports, {
81
84
  filterStripeMethodsByCountry: () => filterStripeMethodsByCountry,
82
85
  filterStripeMethodsByCurrency: () => filterStripeMethodsByCurrency,
83
86
  foldIntoProducts: () => foldIntoProducts,
87
+ generateIdempotencyKey: () => generateIdempotencyKey,
84
88
  getConfiguredBillingApiUrl: () => getConfiguredBillingApiUrl,
85
89
  getCountryByCode: () => getCountryByCode,
86
90
  getCurrencyByCountry: () => getCurrencyByCountry,
@@ -107,6 +111,7 @@ __export(index_exports, {
107
111
  resolveAVSConfig: () => resolveAVSConfig,
108
112
  resolveBillingApiUrl: () => resolveBillingApiUrl,
109
113
  resolveButtonsLayoutTheme: () => resolveButtonsLayoutTheme,
114
+ resolveIdempotencyKey: () => resolveIdempotencyKey,
110
115
  resolveSessionCurrency: () => resolveSessionCurrency,
111
116
  resolveStripeMethodBrandVariant: () => resolveStripeMethodBrandVariant,
112
117
  resolveTheme: () => resolveTheme,
@@ -196,7 +201,7 @@ var PAYMENT_METHOD_LOGOS = {
196
201
  };
197
202
 
198
203
  // src/constants.ts
199
- var SDK_VERSION = "1.3.2";
204
+ var SDK_VERSION = "1.3.4";
200
205
  var FLO_SDK_VERSION_HEADER = "x-flo-sdk-version";
201
206
  var BILLING_API_URL_STAGING = "https://api.stage.flopay.com";
202
207
  var BILLING_API_URL_PRODUCTION = "https://api.flopay.com";
@@ -2274,6 +2279,49 @@ function isValidSecretKey(key) {
2274
2279
  function isSetupIntentClientSecret(clientSecret) {
2275
2280
  return typeof clientSecret === "string" && clientSecret.startsWith("seti_");
2276
2281
  }
2282
+
2283
+ // src/idempotency.ts
2284
+ var IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
2285
+ var MAX_IDEMPOTENCY_KEY_LENGTH = 255;
2286
+ var IDEMPOTENCY_IN_PROGRESS_CODE = "IdempotencyKeyInProgress";
2287
+ function getSecureRandomSource() {
2288
+ const source = globalThis.crypto;
2289
+ if (!source) return void 0;
2290
+ if (typeof source.randomUUID === "function" || typeof source.getRandomValues === "function") {
2291
+ return source;
2292
+ }
2293
+ return void 0;
2294
+ }
2295
+ function generateIdempotencyKey() {
2296
+ const source = getSecureRandomSource();
2297
+ if (!source) return void 0;
2298
+ if (typeof source.randomUUID === "function") {
2299
+ return source.randomUUID();
2300
+ }
2301
+ const bytes = new Uint8Array(16);
2302
+ source.getRandomValues(bytes);
2303
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
2304
+ }
2305
+ function resolveIdempotencyKey(supplied) {
2306
+ if (supplied === void 0) {
2307
+ return generateIdempotencyKey();
2308
+ }
2309
+ if (typeof supplied !== "string" || supplied.trim() === "") {
2310
+ throw new FloPayError(
2311
+ "idempotencyKey must be a non-empty string identifying exactly one checkout creation.",
2312
+ "validation_error",
2313
+ { code: "InvalidIdempotencyKey", param: "idempotencyKey" }
2314
+ );
2315
+ }
2316
+ if (supplied.length > MAX_IDEMPOTENCY_KEY_LENGTH) {
2317
+ throw new FloPayError(
2318
+ `idempotencyKey must be at most ${MAX_IDEMPOTENCY_KEY_LENGTH} characters.`,
2319
+ "validation_error",
2320
+ { code: "InvalidIdempotencyKey", param: "idempotencyKey" }
2321
+ );
2322
+ }
2323
+ return supplied;
2324
+ }
2277
2325
  // Annotate the CommonJS export names for ESM import in node:
2278
2326
  0 && (module.exports = {
2279
2327
  BILLING_API_URL,
@@ -2304,6 +2352,9 @@ function isSetupIntentClientSecret(clientSecret) {
2304
2352
  FloPayError,
2305
2353
  GLASS_DARK_APPEARANCE,
2306
2354
  GLASS_LIGHT_APPEARANCE,
2355
+ IDEMPOTENCY_IN_PROGRESS_CODE,
2356
+ IDEMPOTENCY_KEY_HEADER,
2357
+ MAX_IDEMPOTENCY_KEY_LENGTH,
2307
2358
  MODERN_DARK_APPEARANCE,
2308
2359
  MODERN_LIGHT_APPEARANCE,
2309
2360
  NIGHT_APPEARANCE,
@@ -2327,6 +2378,7 @@ function isSetupIntentClientSecret(clientSecret) {
2327
2378
  filterStripeMethodsByCountry,
2328
2379
  filterStripeMethodsByCurrency,
2329
2380
  foldIntoProducts,
2381
+ generateIdempotencyKey,
2330
2382
  getConfiguredBillingApiUrl,
2331
2383
  getCountryByCode,
2332
2384
  getCurrencyByCountry,
@@ -2353,6 +2405,7 @@ function isSetupIntentClientSecret(clientSecret) {
2353
2405
  resolveAVSConfig,
2354
2406
  resolveBillingApiUrl,
2355
2407
  resolveButtonsLayoutTheme,
2408
+ resolveIdempotencyKey,
2356
2409
  resolveSessionCurrency,
2357
2410
  resolveStripeMethodBrandVariant,
2358
2411
  resolveTheme,