@ingram-tech/nk-billing 0.2.1 → 0.3.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/README.md CHANGED
@@ -79,14 +79,27 @@ export async function POST(request: Request) {
79
79
 
80
80
  The ledger takes the DB connection by **injection** and leaves isolation to you —
81
81
  wrap each call in `withTenant(orgId, db => spendCredits(db, …))` under RLS, or a
82
- plain transaction under app-layer filtering. It owns two tables; apply
83
- `migrations/0001_billing.sql` (a Drizzle-composable fragment) and add your own
84
- RLS policy if your stack uses one. See [`src/credits.ts`](./src/credits.ts).
82
+ plain transaction under app-layer filtering. It owns two tables; apply the
83
+ `migrations/*.sql` files in order (Drizzle-composable fragments) and add your
84
+ own RLS policy if your stack uses one. See [`src/credits.ts`](./src/credits.ts).
85
+
86
+ Two semantics to know before wiring it up:
87
+
88
+ - **Entitlement gates the balance.** `spendCredits` requires an active
89
+ subscription or a live trial *before* it looks at credits: a one-time credit
90
+ pack bought by a tenant with no subscription and an expired trial is not
91
+ spendable. If your site sells packs standalone, gate pack checkout on
92
+ entitlement — or pass `activeStatuses` semantics that match your model.
93
+ - **Webhook ordering.** Pass `event.created` as `eventCreated` to
94
+ `recordSubscriptionStatus` (and apply `0002_billing_status_order.sql`) so a
95
+ delayed, older `customer.subscription.updated` can't overwrite the status a
96
+ later `deleted` event already recorded.
85
97
 
86
98
  ## Full surface
87
99
 
88
- See the [nk-billing guide](../../../nk-billing.md) for the exhaustive,
89
- per-export reference and migration recipes for each consuming app.
100
+ The per-export reference is the JSDoc on each export — every entry point in
101
+ [`src/index.ts`](./src/index.ts) and [`src/credits.ts`](./src/credits.ts) is
102
+ documented at the definition.
90
103
 
91
104
  ## License
92
105
 
package/dist/balance.d.ts CHANGED
@@ -16,13 +16,23 @@ export interface Balance {
16
16
  currency: string;
17
17
  }
18
18
  /** Read a customer's available wallet balance (Stripe's negative credit balance,
19
- * sign-flipped to positive). A deleted customer reads as zero. */
19
+ * sign-flipped to positive and clamped at zero a positive Stripe balance is
20
+ * an amount owed, not available credit). A deleted customer reads as zero. */
20
21
  export declare function readBalance(customerId: string, stripe?: Stripe): Promise<Balance>;
21
- /** Debit the wallet by `amountCents`. Stripe rejects a debit that would push the
22
- * balance positive (the customer would owe us), giving a hard over-spend
23
- * guarantee with no pre-flight check. Idempotency-keyed by `idempotencyTag` so
24
- * retries can't double-debit tie the tag to the action that triggered the
25
- * spend (e.g. the operation's own idempotency key). */
22
+ /** Debit the wallet by `amountCents`, throwing {@link BillingError}
23
+ * (`insufficient_credits`) when the customer's credit doesn't cover it.
24
+ *
25
+ * Stripe does NOT reject a debit that pushes the balance positive (a positive
26
+ * balance is simply owed on the next invoice which a PAYG site never
27
+ * issues), so the guarantee is enforced here: after the debit we check the
28
+ * transaction's `ending_balance`, and if the customer would owe money we
29
+ * reverse the debit and throw. Two concurrent overspends both apply, then
30
+ * both observe the positive balance and reverse — the wallet converges to its
31
+ * pre-race state minus at most the covered amount.
32
+ *
33
+ * Idempotency-keyed by `idempotencyTag` so retries can't double-debit — tie
34
+ * the tag to the action that triggered the spend (e.g. the operation's own
35
+ * idempotency key). */
26
36
  export declare function debitBalance(opts: {
27
37
  customerId: string;
28
38
  amountCents: number;
@@ -1 +1 @@
1
- {"version":3,"file":"balance.d.ts","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,MAAM,WAAW,OAAO;IACvB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;mEACmE;AACnE,wBAAsB,WAAW,CAChC,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED;;;;wDAIwD;AACxD,wBAAsB,YAAY,CACjC,IAAI,EAAE;IACL,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAWf"}
1
+ {"version":3,"file":"balance.d.ts","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAKjC,MAAM,WAAW,OAAO;IACvB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED;;+EAE+E;AAC/E,wBAAsB,WAAW,CAChC,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED;;;;;;;;;;;;;wBAawB;AACxB,wBAAsB,YAAY,CACjC,IAAI,EAAE;IACL,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAiCf"}
package/dist/balance.js CHANGED
@@ -10,25 +10,53 @@
10
10
  * so callers never have to remember that a credit is negative.
11
11
  */
12
12
  import { getStripe } from "./client.js";
13
+ import { DEFAULT_CURRENCY } from "./currency.js";
14
+ import { BillingError } from "./errors.js";
13
15
  /** Read a customer's available wallet balance (Stripe's negative credit balance,
14
- * sign-flipped to positive). A deleted customer reads as zero. */
16
+ * sign-flipped to positive and clamped at zero a positive Stripe balance is
17
+ * an amount owed, not available credit). A deleted customer reads as zero. */
15
18
  export async function readBalance(customerId, stripe = getStripe()) {
16
19
  const customer = await stripe.customers.retrieve(customerId);
17
20
  if (customer.deleted)
18
- return { amount: 0, currency: "eur" };
19
- return { amount: -customer.balance, currency: customer.currency ?? "eur" };
21
+ return { amount: 0, currency: DEFAULT_CURRENCY };
22
+ return {
23
+ amount: Math.max(0, -customer.balance),
24
+ currency: customer.currency ?? DEFAULT_CURRENCY,
25
+ };
20
26
  }
21
- /** Debit the wallet by `amountCents`. Stripe rejects a debit that would push the
22
- * balance positive (the customer would owe us), giving a hard over-spend
23
- * guarantee with no pre-flight check. Idempotency-keyed by `idempotencyTag` so
24
- * retries can't double-debit tie the tag to the action that triggered the
25
- * spend (e.g. the operation's own idempotency key). */
27
+ /** Debit the wallet by `amountCents`, throwing {@link BillingError}
28
+ * (`insufficient_credits`) when the customer's credit doesn't cover it.
29
+ *
30
+ * Stripe does NOT reject a debit that pushes the balance positive (a positive
31
+ * balance is simply owed on the next invoice which a PAYG site never
32
+ * issues), so the guarantee is enforced here: after the debit we check the
33
+ * transaction's `ending_balance`, and if the customer would owe money we
34
+ * reverse the debit and throw. Two concurrent overspends both apply, then
35
+ * both observe the positive balance and reverse — the wallet converges to its
36
+ * pre-race state minus at most the covered amount.
37
+ *
38
+ * Idempotency-keyed by `idempotencyTag` so retries can't double-debit — tie
39
+ * the tag to the action that triggered the spend (e.g. the operation's own
40
+ * idempotency key). */
26
41
  export async function debitBalance(opts, stripe = getStripe()) {
27
- await stripe.customers.createBalanceTransaction(opts.customerId, {
42
+ if (!Number.isSafeInteger(opts.amountCents) || opts.amountCents <= 0) {
43
+ // A negative amount would silently *credit* the wallet.
44
+ throw new RangeError(`debitBalance amountCents must be a positive integer, got ${opts.amountCents}`);
45
+ }
46
+ const txn = await stripe.customers.createBalanceTransaction(opts.customerId, {
28
47
  amount: opts.amountCents,
29
48
  currency: opts.currency,
30
49
  description: opts.description,
31
50
  metadata: opts.metadata ?? {},
32
51
  }, { idempotencyKey: `bal_debit_${opts.idempotencyTag}` });
52
+ if (txn.ending_balance > 0) {
53
+ await stripe.customers.createBalanceTransaction(opts.customerId, {
54
+ amount: -opts.amountCents,
55
+ currency: opts.currency,
56
+ description: `Reversal (insufficient balance): ${opts.description}`,
57
+ metadata: opts.metadata ?? {},
58
+ }, { idempotencyKey: `bal_debit_reversal_${opts.idempotencyTag}` });
59
+ throw new BillingError("insufficient_credits", "Insufficient balance for this debit.");
60
+ }
33
61
  }
34
62
  //# sourceMappingURL=balance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"balance.js","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAQxC;mEACmE;AACnE,MAAM,CAAC,KAAK,UAAU,WAAW,CAChC,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC7D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC5D,OAAO,EAAE,MAAM,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,KAAK,EAAE,CAAC;AAC5E,CAAC;AAED;;;;wDAIwD;AACxD,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,IAOC,EACD,SAAiB,SAAS,EAAE;IAE5B,MAAM,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAC9C,IAAI,CAAC,UAAU,EACf;QACC,MAAM,EAAE,IAAI,CAAC,WAAW;QACxB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;KAC7B,EACD,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC,cAAc,EAAE,EAAE,CACtD,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"balance.js","sourceRoot":"","sources":["../src/balance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAQ3C;;+EAE+E;AAC/E,MAAM,CAAC,KAAK,UAAU,WAAW,CAChC,UAAkB,EAClB,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC7D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IACvE,OAAO;QACN,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC;QACtC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,IAAI,gBAAgB;KAC/C,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;wBAawB;AACxB,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,IAOC,EACD,MAAM,GAAW,SAAS,EAAE;IAE5B,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC;QACtE,wDAAwD;QACxD,MAAM,IAAI,UAAU,CACnB,4DAA4D,IAAI,CAAC,WAAW,EAAE,CAC9E,CAAC;IACH,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAC1D,IAAI,CAAC,UAAU,EACf;QACC,MAAM,EAAE,IAAI,CAAC,WAAW;QACxB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;KAC7B,EACD,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC,cAAc,EAAE,EAAE,CACtD,CAAC;IACF,IAAI,GAAG,CAAC,cAAc,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,MAAM,CAAC,SAAS,CAAC,wBAAwB,CAC9C,IAAI,CAAC,UAAU,EACf;YACC,MAAM,EAAE,CAAC,IAAI,CAAC,WAAW;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,WAAW,EAAE,oCAAoC,IAAI,CAAC,WAAW,EAAE;YACnE,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;SAC7B,EACD,EAAE,cAAc,EAAE,sBAAsB,IAAI,CAAC,cAAc,EAAE,EAAE,CAC/D,CAAC;QACF,MAAM,IAAI,YAAY,CACrB,sBAAsB,EACtB,sCAAsC,CACtC,CAAC;IACH,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAEhB,MAAM,gBAAgB,CAAC;AAGxB,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,QAAQ,EAAE,WAAW,CAAC;IACtB,mEAAmE;IACnE,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;uDACmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;oFACgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvD;4DACwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;;gFAIgF;AAChF,wBAAsB,qBAAqB,CAC1C,IAAI,EAAE,eAAe,EACrB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAqCjB;AAED,6EAA6E;AAC7E,wBAAsB,mBAAmB,CACxC,IAAI,EAAE;IACL,QAAQ,EAAE,WAAW,CAAC;IACtB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAWjB"}
1
+ {"version":3,"file":"checkout.d.ts","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,EACN,KAAK,eAAe,EACpB,KAAK,WAAW,EAEhB,MAAM,gBAAgB,CAAC;AAGxB,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,QAAQ,EAAE,WAAW,CAAC;IACtB,mEAAmE;IACnE,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,iDAAiD;IACjD,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,IAAI,EAAE,cAAc,GAAG,SAAS,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;uDACmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gEAAgE;IAChE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;oFACgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IACvD;4DACwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED;;;;gFAIgF;AAChF,wBAAsB,qBAAqB,CAC1C,IAAI,EAAE,eAAe,EACrB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAuCjB;AAED,6EAA6E;AAC7E,wBAAsB,mBAAmB,CACxC,IAAI,EAAE;IACL,QAAQ,EAAE,WAAW,CAAC;IACtB,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,CAWjB"}
package/dist/checkout.js CHANGED
@@ -21,9 +21,11 @@ export async function createCheckoutSession(opts, stripe = getStripe()) {
21
21
  const currency = opts.currency && supportedCurrencies(price).has(opts.currency)
22
22
  ? opts.currency
23
23
  : undefined;
24
+ // The ref tag is spread last: the webhook side resolves the tenant from it,
25
+ // so caller metadata must never be able to overwrite it.
24
26
  const metadata = {
25
- [opts.customer.metadataKey]: opts.customer.id,
26
27
  ...opts.metadata,
28
+ [opts.customer.metadataKey]: opts.customer.id,
27
29
  };
28
30
  const session = await stripe.checkout.sessions.create({
29
31
  mode: opts.mode,
@@ -1 +1 @@
1
- {"version":3,"file":"checkout.js","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAGN,oBAAoB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA8BpE;;;;gFAIgF;AAChF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,IAAqB,EACrB,SAAiB,SAAS,EAAE;IAE5B,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC3C,oBAAoB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC;QACjE,gBAAgB,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC;KACxC,CAAC,CAAC;IAEH,MAAM,QAAQ,GACb,IAAI,CAAC,QAAQ,IAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC7D,CAAC,CAAC,IAAI,CAAC,QAAQ;QACf,CAAC,CAAC,SAAS,CAAC;IAEd,MAAM,QAAQ,GAAG;QAChB,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;QAC7C,GAAG,IAAI,CAAC,QAAQ;KAChB,CAAC;IAEF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;QACrD,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;QAC/D,qBAAqB,EAAE,IAAI,CAAC,mBAAmB,IAAI,IAAI;QACvD,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM;QAC7B,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,IAAI,CAAC,YAAY;YACpB,CAAC,CAAC;gBACA,iBAAiB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;gBACpC,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;aACjC;YACF,CAAC,CAAC,EAAE,CAAC;QACN,WAAW,EAAE,IAAI,CAAC,UAAU;QAC5B,UAAU,EAAE,IAAI,CAAC,SAAS;QAC1B,QAAQ;QACR,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5E,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1E,OAAO,OAAO,CAAC,GAAG,CAAC;AACpB,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,IAIC,EACD,SAAiB,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAC1C,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,eAAe,EACpB,MAAM,CACN,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC1D,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,UAAU,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,GAAG,CAAC;AACpB,CAAC"}
1
+ {"version":3,"file":"checkout.js","sourceRoot":"","sources":["../src/checkout.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAGN,oBAAoB,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AA8BpE;;;;gFAIgF;AAChF,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAC1C,IAAqB,EACrB,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC3C,oBAAoB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC;QACjE,gBAAgB,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC;KACxC,CAAC,CAAC;IAEH,MAAM,QAAQ,GACb,IAAI,CAAC,QAAQ,IAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;QAC7D,CAAC,CAAC,IAAI,CAAC,QAAQ;QACf,CAAC,CAAC,SAAS,CAAC;IAEd,4EAA4E;IAC5E,yDAAyD;IACzD,MAAM,QAAQ,GAAG;QAChB,GAAG,IAAI,CAAC,QAAQ;QAChB,CAAC,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE;KAC7C,CAAC;IAEF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC;QACrD,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,EAAE,CAAC;QAC/D,qBAAqB,EAAE,IAAI,CAAC,mBAAmB,IAAI,IAAI;QACvD,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM;QAC7B,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,IAAI,CAAC,YAAY;YACpB,CAAC,CAAC;gBACA,iBAAiB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;gBACpC,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE;aACjC;YACF,CAAC,CAAC,EAAE,CAAC;QACN,WAAW,EAAE,IAAI,CAAC,UAAU;QAC5B,UAAU,EAAE,IAAI,CAAC,SAAS;QAC1B,QAAQ;QACR,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5E,CAAC,CAAC;IAEH,IAAI,CAAC,OAAO,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1E,OAAO,OAAO,CAAC,GAAG,CAAC;AACpB,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,IAIC,EACD,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CAC1C,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,eAAe,EACpB,MAAM,CACN,CAAC;IACF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC;QAC1D,QAAQ,EAAE,QAAQ,CAAC,EAAE;QACrB,UAAU,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,GAAG,CAAC;AACpB,CAAC"}
package/dist/credits.d.ts CHANGED
@@ -67,10 +67,13 @@ export declare function spendCredits(db: Queryable, opts: SpendOptions): Promise
67
67
  export declare function requireCredits(db: Queryable, opts: SpendOptions): Promise<void>;
68
68
  /** Return previously-spent credits — call when the work a spend paid for did not
69
69
  * run (a dedup hit, a hard failure, an aborted enqueue). No-op for non-positive
70
- * amounts. */
70
+ * amounts. Pass `eventId` (any stable id for the failure being compensated) to
71
+ * make the refund idempotent — a retried failure handler then can't
72
+ * double-refund. Without it, retries are the caller's problem. */
71
73
  export declare function refundCredits(db: Queryable, opts: {
72
74
  tenantId: string;
73
75
  amount: number;
76
+ eventId?: string;
74
77
  }): Promise<void>;
75
78
  /**
76
79
  * Mark a Stripe event processed, idempotently. Returns false if the event was
@@ -92,12 +95,21 @@ export declare function grantCredits(db: Queryable, opts: {
92
95
  stripeCustomerId?: string | null;
93
96
  }): Promise<boolean>;
94
97
  /** Cache a tenant's subscription status from a webhook, idempotently per event.
95
- * Returns false if the event was already processed. */
98
+ * Returns false if the event was already processed.
99
+ *
100
+ * Stripe does not guarantee delivery order, so pass the event's `created`
101
+ * timestamp (Unix seconds — `event.created`): a delayed older event (e.g. an
102
+ * `updated: active` arriving after `deleted: canceled`) is then ignored
103
+ * instead of resurrecting a dead subscription indefinitely. Requires migration
104
+ * `0002_billing_status_order.sql`; without `eventCreated` the last-write-wins
105
+ * legacy behavior is kept. */
96
106
  export declare function recordSubscriptionStatus(db: Queryable, opts: {
97
107
  tenantId: string;
98
108
  eventId: string;
99
109
  eventType: string;
100
110
  status: string;
111
+ /** The Stripe event's `created` (Unix seconds), for out-of-order defense. */
112
+ eventCreated?: number;
101
113
  stripeCustomerId?: string | null;
102
114
  }): Promise<boolean>;
103
115
  export interface BillingSummary {
@@ -1 +1 @@
1
- {"version":3,"file":"credits.d.ts","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"credits.d.ts","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"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"}
package/dist/credits.js CHANGED
@@ -21,6 +21,9 @@
21
21
  import { BillingError } from "./errors.js";
22
22
  import { ACTIVE_SUBSCRIPTION_STATUSES } from "./status.js";
23
23
  export { BillingError, denyMessage } from "./errors.js";
24
+ // `credits_balance` is a Postgres bigint, which `pg` hands back as a string.
25
+ // Coerce before it leaks into arithmetic (string concatenation) or JSON.
26
+ const toBalance = (value) => Number(value);
24
27
  function entitled(row, now, trialWindowMs, activeStatuses) {
25
28
  if (row.subscription_status && activeStatuses.has(row.subscription_status)) {
26
29
  return true;
@@ -44,6 +47,10 @@ function entitled(row, now, trialWindowMs, activeStatuses) {
44
47
  */
45
48
  export async function spendCredits(db, opts) {
46
49
  const activeStatuses = opts.activeStatuses ?? ACTIVE_SUBSCRIPTION_STATUSES;
50
+ if (!Number.isSafeInteger(opts.cost) || opts.cost < 0) {
51
+ // A negative cost would silently credit the tenant; NaN passes `<` checks.
52
+ throw new RangeError(`spendCredits cost must be a non-negative integer, got ${opts.cost}`);
53
+ }
47
54
  // First touch: seed the row (with trial credits + clock when trials are on).
48
55
  // ON CONFLICT DO NOTHING makes the trial a one-time grant.
49
56
  await db.query(`insert into billing_credits (tenant_id, credits_balance, trial_started_at)
@@ -57,14 +64,14 @@ export async function spendCredits(db, opts) {
57
64
  if (!entitled(row, Date.now(), opts.trial?.windowMs, activeStatuses)) {
58
65
  return { ok: false, reason: "not_entitled" };
59
66
  }
60
- if (row.credits_balance < opts.cost) {
67
+ if (toBalance(row.credits_balance) < opts.cost) {
61
68
  return { ok: false, reason: "insufficient_credits" };
62
69
  }
63
70
  const { rows: updated } = await db.query(`update billing_credits
64
71
  set credits_balance = credits_balance - $2, updated_at = now()
65
72
  where tenant_id = $1
66
73
  returning credits_balance`, [opts.tenantId, opts.cost]);
67
- return { ok: true, balance: updated[0]?.credits_balance ?? 0 };
74
+ return { ok: true, balance: toBalance(updated[0]?.credits_balance ?? 0) };
68
75
  }
69
76
  /** Like {@link spendCredits} but throws {@link BillingError} on a denial — use in
70
77
  * API routes that map the error to a 402. */
@@ -75,13 +82,30 @@ export async function requireCredits(db, opts) {
75
82
  }
76
83
  /** Return previously-spent credits — call when the work a spend paid for did not
77
84
  * run (a dedup hit, a hard failure, an aborted enqueue). No-op for non-positive
78
- * amounts. */
85
+ * amounts. Pass `eventId` (any stable id for the failure being compensated) to
86
+ * make the refund idempotent — a retried failure handler then can't
87
+ * double-refund. Without it, retries are the caller's problem. */
79
88
  export async function refundCredits(db, opts) {
80
- if (opts.amount <= 0)
89
+ if (!Number.isSafeInteger(opts.amount) || opts.amount <= 0)
90
+ return;
91
+ if (opts.eventId == null) {
92
+ await db.query(`update billing_credits
93
+ set credits_balance = credits_balance + $2, updated_at = now()
94
+ where tenant_id = $1`, [opts.tenantId, opts.amount]);
81
95
  return;
82
- await db.query(`update billing_credits
83
- set credits_balance = credits_balance + $2, updated_at = now()
84
- where tenant_id = $1`, [opts.tenantId, opts.amount]);
96
+ }
97
+ // Claim the event and apply the refund in a single statement, so the pair is
98
+ // atomic even when `db` is a bare pool (autocommit): a crash can't commit the
99
+ // claim while dropping the refund — which would strand the credits, since the
100
+ // retried handler would see the event already processed and no-op.
101
+ await db.query(`with claim as (
102
+ insert into billing_stripe_events (event_id, type) values ($1, 'credits.refund')
103
+ on conflict (event_id) do nothing
104
+ returning event_id
105
+ )
106
+ update billing_credits
107
+ set credits_balance = credits_balance + $3, updated_at = now()
108
+ where tenant_id = $2 and exists (select 1 from claim)`, [opts.eventId, opts.tenantId, opts.amount]);
85
109
  }
86
110
  /**
87
111
  * Mark a Stripe event processed, idempotently. Returns false if the event was
@@ -100,28 +124,93 @@ export async function claimStripeEvent(db, eventId, type) {
100
124
  * Pass a synthetic `eventId` (e.g. `subgrant:<sub>:<periodStart>`) to grant a
101
125
  * subscription's bundled credits once per billing period. */
102
126
  export async function grantCredits(db, opts) {
103
- if (!(await claimStripeEvent(db, opts.eventId, opts.eventType)))
104
- return false;
105
- await db.query(`insert into billing_credits (tenant_id, credits_balance, stripe_customer_id)
106
- values ($1, $2, $3)
107
- on conflict (tenant_id) do update set
108
- credits_balance = billing_credits.credits_balance + excluded.credits_balance,
109
- stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
110
- updated_at = now()`, [opts.tenantId, opts.amount, opts.stripeCustomerId ?? null]);
111
- return true;
127
+ // Claim the event and increment the balance in one statement, so the pair is
128
+ // atomic even when `db` is a bare pool (autocommit): a crash can't commit the
129
+ // claim while dropping the grant — which would mark the Stripe event processed
130
+ // with the credits never applied, and the retry would no-op.
131
+ const { rows } = await db.query(`with claim as (
132
+ insert into billing_stripe_events (event_id, type) values ($1, $2)
133
+ on conflict (event_id) do nothing
134
+ returning event_id
135
+ ),
136
+ upsert as (
137
+ insert into billing_credits (tenant_id, credits_balance, stripe_customer_id)
138
+ select $3, $4, $5 where exists (select 1 from claim)
139
+ on conflict (tenant_id) do update set
140
+ credits_balance = billing_credits.credits_balance + excluded.credits_balance,
141
+ stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
142
+ updated_at = now()
143
+ )
144
+ select exists (select 1 from claim) as claimed`, [
145
+ opts.eventId,
146
+ opts.eventType,
147
+ opts.tenantId,
148
+ opts.amount,
149
+ opts.stripeCustomerId ?? null,
150
+ ]);
151
+ return rows[0]?.claimed ?? false;
112
152
  }
113
153
  /** Cache a tenant's subscription status from a webhook, idempotently per event.
114
- * Returns false if the event was already processed. */
154
+ * Returns false if the event was already processed.
155
+ *
156
+ * Stripe does not guarantee delivery order, so pass the event's `created`
157
+ * timestamp (Unix seconds — `event.created`): a delayed older event (e.g. an
158
+ * `updated: active` arriving after `deleted: canceled`) is then ignored
159
+ * instead of resurrecting a dead subscription indefinitely. Requires migration
160
+ * `0002_billing_status_order.sql`; without `eventCreated` the last-write-wins
161
+ * legacy behavior is kept. */
115
162
  export async function recordSubscriptionStatus(db, opts) {
116
- if (!(await claimStripeEvent(db, opts.eventId, opts.eventType)))
117
- return false;
118
- await db.query(`insert into billing_credits (tenant_id, subscription_status, stripe_customer_id)
119
- values ($1, $2, $3)
120
- on conflict (tenant_id) do update set
121
- subscription_status = excluded.subscription_status,
122
- stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
123
- updated_at = now()`, [opts.tenantId, opts.status, opts.stripeCustomerId ?? null]);
124
- return true;
163
+ // Claim the event and cache the status in one statement, so the pair is atomic
164
+ // even when `db` is a bare pool (autocommit): a crash can't commit the claim
165
+ // while dropping the status write, which the retry would then never re-apply.
166
+ if (opts.eventCreated == null) {
167
+ const { rows } = await db.query(`with claim as (
168
+ insert into billing_stripe_events (event_id, type) values ($1, $2)
169
+ on conflict (event_id) do nothing
170
+ returning event_id
171
+ ),
172
+ upsert as (
173
+ insert into billing_credits (tenant_id, subscription_status, stripe_customer_id)
174
+ select $3, $4, $5 where exists (select 1 from claim)
175
+ on conflict (tenant_id) do update set
176
+ subscription_status = excluded.subscription_status,
177
+ stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
178
+ updated_at = now()
179
+ )
180
+ select exists (select 1 from claim) as claimed`, [
181
+ opts.eventId,
182
+ opts.eventType,
183
+ opts.tenantId,
184
+ opts.status,
185
+ opts.stripeCustomerId ?? null,
186
+ ]);
187
+ return rows[0]?.claimed ?? false;
188
+ }
189
+ const { rows } = await db.query(`with claim as (
190
+ insert into billing_stripe_events (event_id, type) values ($1, $2)
191
+ on conflict (event_id) do nothing
192
+ returning event_id
193
+ ),
194
+ upsert as (
195
+ insert into billing_credits (tenant_id, subscription_status, subscription_status_at, stripe_customer_id)
196
+ select $3, $4, to_timestamp($5), $6 where exists (select 1 from claim)
197
+ on conflict (tenant_id) do update set
198
+ subscription_status = excluded.subscription_status,
199
+ subscription_status_at = excluded.subscription_status_at,
200
+ stripe_customer_id = coalesce(excluded.stripe_customer_id, billing_credits.stripe_customer_id),
201
+ updated_at = now()
202
+ where billing_credits.subscription_status_at is null
203
+ or excluded.subscription_status_at >= billing_credits.subscription_status_at
204
+ )
205
+ select exists (select 1 from claim) as claimed`, [
206
+ opts.eventId,
207
+ opts.eventType,
208
+ opts.tenantId,
209
+ opts.status,
210
+ opts.eventCreated,
211
+ opts.stripeCustomerId ?? null,
212
+ ]);
213
+ return rows[0]?.claimed ?? false;
125
214
  }
126
215
  /** Read-only entitlement + balance snapshot for the billing UI. Does not create a
127
216
  * row or grant the trial (that happens on the first spend). */
@@ -140,11 +229,14 @@ export async function getBillingSummary(db, opts) {
140
229
  };
141
230
  }
142
231
  const subscribed = !!row.subscription_status && activeStatuses.has(row.subscription_status);
143
- const trialEndsAt = !subscribed && opts.trialWindowMs != null && row.trial_started_at
144
- ? new Date(row.trial_started_at).getTime() + opts.trialWindowMs
232
+ // Guard the parse like `entitled()` does: an unparseable timestamp yields NaN,
233
+ // so return null rather than a NaN `trialEndsAt`.
234
+ const trialStarted = row.trial_started_at != null ? new Date(row.trial_started_at).getTime() : NaN;
235
+ const trialEndsAt = !subscribed && opts.trialWindowMs != null && Number.isFinite(trialStarted)
236
+ ? trialStarted + opts.trialWindowMs
145
237
  : null;
146
238
  return {
147
- creditsBalance: row.credits_balance,
239
+ creditsBalance: toBalance(row.credits_balance),
148
240
  subscriptionStatus: row.subscription_status,
149
241
  stripeCustomerId: row.stripe_customer_id,
150
242
  entitled: entitled(row, Date.now(), opts.trialWindowMs, activeStatuses),
@@ -1 +1 @@
1
- {"version":3,"file":"credits.js","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"credits.js","sourceRoot":"","sources":["../src/credits.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"customers.d.ts","sourceRoot":"","sources":["../src/customers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC3B;oEACgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,GAAG,IAAI,CAAC;CACT;AAED;;gBAEgB;AAChB,wBAAsB,YAAY,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC,CAMjC;AAED;;eAEe;AACf,wBAAsB,oBAAoB,CACzC,GAAG,EAAE,WAAW,EAChB,OAAO,GAAE,eAAoB,EAC7B,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAiC1B;AAED;gEACgE;AAChE,wBAAsB,yBAAyB,CAC9C,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED;8CAC8C;AAC9C,wBAAsB,4BAA4B,CACjD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC;CACF,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAKf"}
1
+ {"version":3,"file":"customers.d.ts","sourceRoot":"","sources":["../src/customers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC3B;oEACgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACxB,GAAG,IAAI,CAAC;CACT;AAQD;;gBAEgB;AAChB,wBAAsB,YAAY,CACjC,GAAG,EAAE,WAAW,EAChB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC,CAMjC;AAED;;eAEe;AACf,wBAAsB,oBAAoB,CACzC,GAAG,EAAE,WAAW,EAChB,OAAO,GAAE,eAAoB,EAC7B,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAuC1B;AAED;gEACgE;AAChE,wBAAsB,yBAAyB,CAC9C,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED;8CAC8C;AAC9C,wBAAsB,4BAA4B,CACjD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC;CACF,EACD,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAKf"}
package/dist/customers.js CHANGED
@@ -9,12 +9,16 @@
9
9
  * baked in here — that is the framework seam.
10
10
  */
11
11
  import { getStripe } from "./client.js";
12
+ // Stripe's search syntax delimits values with single quotes; an unescaped
13
+ // quote in an interpolated value would let a crafted id alter the query (it
14
+ // supports OR) and match another tenant's customer.
15
+ const escapeSearchValue = (value) => value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
12
16
  /** The customer for `ref`, found by metadata search, or null if none exists yet.
13
17
  * Read paths use this so merely viewing a billing page never mints an empty
14
18
  * customer. */
15
19
  export async function findCustomer(ref, stripe = getStripe()) {
16
20
  const found = await stripe.customers.search({
17
- query: `metadata['${ref.metadataKey}']:'${ref.id}'`,
21
+ query: `metadata['${escapeSearchValue(ref.metadataKey)}']:'${escapeSearchValue(ref.id)}'`,
18
22
  limit: 1,
19
23
  });
20
24
  return found.data[0] ?? null;
@@ -39,7 +43,13 @@ export async function findOrCreateCustomer(ref, details = {}, stripe = getStripe
39
43
  country: details.address.country ?? undefined,
40
44
  };
41
45
  }
42
- const customer = await stripe.customers.create(params);
46
+ // Idempotency-keyed on the ref: `customers.search` is eventually consistent
47
+ // (freshly created customers can be invisible for up to ~a minute), so two
48
+ // concurrent find-then-create calls both miss — the key makes Stripe hand
49
+ // both the same customer instead of minting a duplicate.
50
+ const customer = await stripe.customers.create(params, {
51
+ idempotencyKey: `cust_create_${ref.metadataKey}_${ref.id}`,
52
+ });
43
53
  if (details.vatNumber) {
44
54
  try {
45
55
  await stripe.customers.createTaxId(customer.id, {
@@ -1 +1 @@
1
- {"version":3,"file":"customers.js","sourceRoot":"","sources":["../src/customers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAwBxC;;gBAEgB;AAChB,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,GAAgB,EAChB,SAAiB,SAAS,EAAE;IAE5B,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAC3C,KAAK,EAAE,aAAa,GAAG,CAAC,WAAW,OAAO,GAAG,CAAC,EAAE,GAAG;QACnD,KAAK,EAAE,CAAC;KACR,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC9B,CAAC;AAED;;eAEe;AACf,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,GAAgB,EAChB,UAA2B,EAAE,EAC7B,SAAiB,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,MAAM,GAAgC;QAC3C,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,SAAS;QAC/B,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,SAAS;QACjC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE;KACvC,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,EAAE,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACrD,MAAM,CAAC,OAAO,GAAG;YAChB,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,SAAS;YACzC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS;YACvC,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,WAAW,IAAI,SAAS;YACrD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,IAAI,SAAS;SAC7C,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEvD,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,EAAE;gBAC/C,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,OAAO,CAAC,SAAS;aACxB,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,wEAAwE;YACxE,+DAA+D;QAChE,CAAC;IACF,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED;gEACgE;AAChE,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC9C,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC7D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AACzC,CAAC;AAED;8CAC8C;AAC9C,MAAM,CAAC,KAAK,UAAU,4BAA4B,CACjD,UAAkB,EAClB,IAQC,EACD,SAAiB,SAAS,EAAE;IAE5B,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE;QACzC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,IAAI,CAAC,OAAO;KACrB,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"customers.js","sourceRoot":"","sources":["../src/customers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAwBxC,0EAA0E;AAC1E,4EAA4E;AAC5E,oDAAoD;AACpD,MAAM,iBAAiB,GAAG,CAAC,KAAa,EAAU,EAAE,CACnD,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAEnD;;gBAEgB;AAChB,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,GAAgB,EAChB,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC;QAC3C,KAAK,EAAE,aAAa,iBAAiB,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG;QACzF,KAAK,EAAE,CAAC;KACR,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC9B,CAAC;AAED;;eAEe;AACf,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACzC,GAAgB,EAChB,OAAO,GAAoB,EAAE,EAC7B,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,MAAM,GAAgC;QAC3C,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,SAAS;QAC/B,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,SAAS;QACjC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE;KACvC,CAAC;IACF,IAAI,OAAO,CAAC,OAAO,EAAE,KAAK,IAAI,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACrD,MAAM,CAAC,OAAO,GAAG;YAChB,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,SAAS;YACzC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,IAAI,SAAS;YACvC,WAAW,EAAE,OAAO,CAAC,OAAO,CAAC,WAAW,IAAI,SAAS;YACrD,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,IAAI,SAAS;SAC7C,CAAC;IACH,CAAC;IAED,4EAA4E;IAC5E,2EAA2E;IAC3E,0EAA0E;IAC1E,yDAAyD;IACzD,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE;QACtD,cAAc,EAAE,eAAe,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,EAAE,EAAE;KAC1D,CAAC,CAAC;IAEH,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,IAAI,CAAC;YACJ,MAAM,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,EAAE;gBAC/C,IAAI,EAAE,QAAQ;gBACd,KAAK,EAAE,OAAO,CAAC,SAAS;aACxB,CAAC,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACR,wEAAwE;YACxE,+DAA+D;QAChE,CAAC;IACF,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED;gEACgE;AAChE,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC9C,UAAkB,EAClB,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAC7D,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IACnC,OAAO,OAAO,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AACzC,CAAC;AAED;8CAC8C;AAC9C,MAAM,CAAC,KAAK,UAAU,4BAA4B,CACjD,UAAkB,EAClB,IAQC,EACD,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE;QACzC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,OAAO,EAAE,IAAI,CAAC,OAAO;KACrB,CAAC,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+BAA+B;AAC/B,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,sBAAsB,CAAC;AAEjE;6EAC6E;AAC7E,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAItD;AAED;kFACkF;AAClF,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;gBAChB,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,MAA4B;CAKrE"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,+BAA+B;AAC/B,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,sBAAsB,CAAC;AAEjE;6EAC6E;AAC7E,wBAAgB,WAAW,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAItD;AAED;kFACkF;AAClF,qBAAa,YAAa,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,YAAY,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,MAA4B,EAIpE;CACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;6EAC6E;AAC7E,MAAM,UAAU,WAAW,CAAC,MAAkB;IAC7C,OAAO,MAAM,KAAK,cAAc;QAC/B,CAAC,CAAC,+EAA+E;QACjF,CAAC,CAAC,iEAAiE,CAAC;AACtE,CAAC;AAED;kFACkF;AAClF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC7B,MAAM,CAAa;IAC5B,YAAY,MAAkB,EAAE,UAAkB,WAAW,CAAC,MAAM,CAAC;QACpE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH;6EAC6E;AAC7E,MAAM,UAAU,WAAW,CAAC,MAAkB;IAC7C,OAAO,MAAM,KAAK,cAAc;QAC/B,CAAC,CAAC,+EAA+E;QACjF,CAAC,CAAC,iEAAiE,CAAC;AACtE,CAAC;AAED;kFACkF;AAClF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC7B,MAAM,CAAa;IAC5B,YAAY,MAAkB,EAAE,OAAO,GAAW,WAAW,CAAC,MAAM,CAAC;QACpE,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD"}
package/dist/keys.d.ts CHANGED
@@ -9,13 +9,13 @@
9
9
  * Two deployment shapes are supported, picked per call site, never both at once:
10
10
  *
11
11
  * 1. Single-mode (the default for most sites): one account, one key.
12
- * `getStripe()` reads STRIPE_SECRET_KEY and `webhookSecret()` reads
13
- * STRIPE_WEBHOOK_SECRET.
12
+ * `getStripe()` reads STRIPE_SECRET_KEY and `billingEnv().webhookSecret`
13
+ * carries STRIPE_WEBHOOK_SECRET (passed to `verifyStripeWebhook`).
14
14
  *
15
15
  * 2. Dual-mode (a site that is its own merchant of record and runs the Stripe
16
16
  * sandbox beside live): `stripeFor("test"|"live")` reads
17
- * STRIPE_SECRET_KEY_{TEST,LIVE} and `webhookSecret(mode)` reads
18
- * STRIPE_WEBHOOK_SECRET_{TEST,LIVE}.
17
+ * STRIPE_SECRET_KEY_{TEST,LIVE} and `billingEnv().{test,live}WebhookSecret`
18
+ * carries STRIPE_WEBHOOK_SECRET_{TEST,LIVE}.
19
19
  *
20
20
  * Nothing here hardcodes a price ID. Prices are resolved at runtime by their
21
21
  * stable Stripe `lookup_key` (set on the Price out of band, e.g. in your IaC), so
@@ -1 +1 @@
1
- {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAezC,MAAM,WAAW,UAAU;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;2CAE2C;AAC3C,eAAO,MAAM,UAAU,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,UAkBjE,CAAC;AAEF;qEACqE;AACrE,eAAO,MAAM,gBAAgB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OACzC,CAAC;AAEhC,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,GAChC,MAAM,UAAU,EAChB,MAAK,MAAM,CAAC,UAAwB,KAClC,OACgF,CAAC"}
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAezC,MAAM,WAAW,UAAU;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;2CAE2C;AAC3C,eAAO,MAAM,UAAU,SAAS,MAAM,CAAC,UAAU,KAAiB,UAkBjE,CAAC;AAEF;qEACqE;AACrE,eAAO,MAAM,gBAAgB,SAAS,MAAM,CAAC,UAAU,KAAiB,OACzC,CAAC;AAEhC,yDAAyD;AACzD,eAAO,MAAM,oBAAoB,SAC1B,UAAU,QACX,MAAM,CAAC,UAAU,KACpB,OACgF,CAAC"}
package/dist/keys.js CHANGED
@@ -9,13 +9,13 @@
9
9
  * Two deployment shapes are supported, picked per call site, never both at once:
10
10
  *
11
11
  * 1. Single-mode (the default for most sites): one account, one key.
12
- * `getStripe()` reads STRIPE_SECRET_KEY and `webhookSecret()` reads
13
- * STRIPE_WEBHOOK_SECRET.
12
+ * `getStripe()` reads STRIPE_SECRET_KEY and `billingEnv().webhookSecret`
13
+ * carries STRIPE_WEBHOOK_SECRET (passed to `verifyStripeWebhook`).
14
14
  *
15
15
  * 2. Dual-mode (a site that is its own merchant of record and runs the Stripe
16
16
  * sandbox beside live): `stripeFor("test"|"live")` reads
17
- * STRIPE_SECRET_KEY_{TEST,LIVE} and `webhookSecret(mode)` reads
18
- * STRIPE_WEBHOOK_SECRET_{TEST,LIVE}.
17
+ * STRIPE_SECRET_KEY_{TEST,LIVE} and `billingEnv().{test,live}WebhookSecret`
18
+ * carries STRIPE_WEBHOOK_SECRET_{TEST,LIVE}.
19
19
  *
20
20
  * Nothing here hardcodes a price ID. Prices are resolved at runtime by their
21
21
  * stable Stripe `lookup_key` (set on the Price out of band, e.g. in your IaC), so
package/dist/keys.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACvB,eAAe;IACf,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC/C,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACnD,aAAa;IACb,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpD,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpD,0BAA0B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxD,0BAA0B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxD,gEAAgE;IAChE,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAChD,CAAC,CAAC;AAYH;;2CAE2C;AAC3C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAc,EAAE;IAC9E,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAChC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;aAC3D,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,kDAAkD,MAAM,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IACtB,OAAO;QACN,SAAS,EAAE,CAAC,CAAC,iBAAiB;QAC9B,aAAa,EAAE,CAAC,CAAC,qBAAqB;QACtC,aAAa,EAAE,CAAC,CAAC,sBAAsB;QACvC,aAAa,EAAE,CAAC,CAAC,sBAAsB;QACvC,iBAAiB,EAAE,CAAC,CAAC,0BAA0B;QAC/C,iBAAiB,EAAE,CAAC,CAAC,0BAA0B;QAC/C,UAAU,EAAE,CAAC,CAAC,kBAAkB;KAChC,CAAC;AACH,CAAC,CAAC;AAEF;qEACqE;AACrE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAyB,OAAO,CAAC,GAAG,EAAW,EAAE,CACjF,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AAEhC,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CACnC,IAAgB,EAChB,MAAyB,OAAO,CAAC,GAAG,EAC1B,EAAE,CACZ,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC"}
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACvB,eAAe;IACf,iBAAiB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IAC/C,qBAAqB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACnD,aAAa;IACb,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpD,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACpD,0BAA0B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxD,0BAA0B,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;IACxD,gEAAgE;IAChE,kBAAkB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAChD,CAAC,CAAC;AAYH;;2CAE2C;AAC3C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG,EAAc,EAAE;IAC9E,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM;aAChC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;aAC3D,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,kDAAkD,MAAM,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC;IACtB,OAAO;QACN,SAAS,EAAE,CAAC,CAAC,iBAAiB;QAC9B,aAAa,EAAE,CAAC,CAAC,qBAAqB;QACtC,aAAa,EAAE,CAAC,CAAC,sBAAsB;QACvC,aAAa,EAAE,CAAC,CAAC,sBAAsB;QACvC,iBAAiB,EAAE,CAAC,CAAC,0BAA0B;QAC/C,iBAAiB,EAAE,CAAC,CAAC,0BAA0B;QAC/C,UAAU,EAAE,CAAC,CAAC,kBAAkB;KAChC,CAAC;AACH,CAAC,CAAC;AAEF;qEACqE;AACrE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG,EAAW,EAAE,CACjF,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AAEhC,yDAAyD;AACzD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CACnC,IAAgB,EAChB,GAAG,GAAsB,OAAO,CAAC,GAAG,EAC1B,EAAE,CACZ,OAAO,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"prices.js","sourceRoot":"","sources":["../src/prices.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;2DAG2D;AAC3D,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,GAAW,EACX,SAAiB,SAAS,EAAE;IAE5B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;QACvC,WAAW,EAAE,CAAC,GAAG,CAAC;QAClB,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC,uBAAuB,CAAC;KACjC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACd,0CAA0C,GAAG,gDAAgD,CAC7F,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,mBAAmB,CAAC,KAAmB;IACtD,OAAO,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;yCAGyC;AACzC,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,GAAW,EACX,QAAgB,EAChB,SAAiB,SAAS,EAAE;IAE5B,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAClD,IAAI,QAAQ,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,KAAK,CAAC,gBAAgB,EAAE,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;QAC5D,IAAI,GAAG,IAAI,IAAI;YAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;QAClD,gEAAgE;IACjE,CAAC;IACD,IAAI,KAAK,CAAC,WAAW,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC3C,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;AAChE,CAAC"}
1
+ {"version":3,"file":"prices.js","sourceRoot":"","sources":["../src/prices.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC;;;2DAG2D;AAC3D,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,GAAW,EACX,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;QACvC,WAAW,EAAE,CAAC,GAAG,CAAC;QAClB,MAAM,EAAE,IAAI;QACZ,KAAK,EAAE,CAAC;QACR,MAAM,EAAE,CAAC,uBAAuB,CAAC;KACjC,CAAC,CAAC;IACH,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACd,0CAA0C,GAAG,gDAAgD,CAC7F,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,mBAAmB,CAAC,KAAmB;IACtD,OAAO,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;yCAGyC;AACzC,MAAM,CAAC,KAAK,UAAU,aAAa,CAClC,GAAW,EACX,QAAgB,EAChB,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAClD,IAAI,QAAQ,KAAK,KAAK,CAAC,QAAQ,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,KAAK,CAAC,gBAAgB,EAAE,CAAC,QAAQ,CAAC,EAAE,WAAW,CAAC;QAC5D,IAAI,GAAG,IAAI,IAAI;YAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;QAClD,gEAAgE;IACjE,CAAC;IACD,IAAI,KAAK,CAAC,WAAW,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC3C,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,WAAW,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC;AAChE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAIjC,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3E;oDACoD;AACpD,MAAM,WAAW,mBAAmB;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,sEAAsE;IACtE,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACtB;AAKD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,YAAY,GAAG,mBAAmB,CAYnF;AAED;;;sDAGsD;AACtD,wBAAsB,4BAA4B,CACjD,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAQrC;AAED,uFAAuF;AACvF,wBAAsB,wBAAwB,CAC7C,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAGrC"}
1
+ {"version":3,"file":"subscription.d.ts","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAIjC,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3E;oDACoD;AACpD,MAAM,WAAW,mBAAmB;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,sEAAsE;IACtE,gBAAgB,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACtB;AAKD,4EAA4E;AAC5E,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,YAAY,GAAG,mBAAmB,CAYnF;AAED;;;sDAGsD;AACtD,wBAAsB,4BAA4B,CACjD,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,MAAM,CAAC,YAAY,GAAG,IAAI,CAAC,CAUrC;AAED,uFAAuF;AACvF,wBAAsB,wBAAwB,CAC7C,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAoB,GAC1B,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAGrC"}
@@ -34,7 +34,9 @@ export async function fetchSubscriptionForCustomer(customerId, stripe = getStrip
34
34
  const subs = await stripe.subscriptions.list({
35
35
  customer: customerId,
36
36
  status: "all",
37
- limit: 10,
37
+ // Stripe's max: an active subscription older than the N most recent ones
38
+ // must still be found ahead of the canceled ones.
39
+ limit: 100,
38
40
  expand: ["data.items.data.price"],
39
41
  });
40
42
  return subs.data.find((s) => isActiveStatus(s.status)) ?? subs.data[0] ?? null;
@@ -1 +1 @@
1
- {"version":3,"file":"subscription.js","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgB3E,MAAM,MAAM,GAAG,CAAC,OAAkC,EAAe,EAAE,CAClE,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,GAAwB;IAC7D,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/B,OAAO;QACN,cAAc,EAAE,GAAG,CAAC,EAAE;QACtB,UAAU,EAAE,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;QAC7E,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,IAAI;QAC/B,gBAAgB,EAAE,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAClD,iBAAiB,EAAE,GAAG,CAAC,oBAAoB;QAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;KAC/B,CAAC;AACH,CAAC;AAED;;;sDAGsD;AACtD,MAAM,CAAC,KAAK,UAAU,4BAA4B,CACjD,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;QAC5C,QAAQ,EAAE,UAAU;QACpB,MAAM,EAAE,KAAK;QACb,KAAK,EAAE,EAAE;QACT,MAAM,EAAE,CAAC,uBAAuB,CAAC;KACjC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAChF,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC7C,UAAkB,EAClB,SAAiB,SAAS,EAAE;IAE5B,MAAM,GAAG,GAAG,MAAM,4BAA4B,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACnE,OAAO,GAAG,CAAC,CAAC,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC"}
1
+ {"version":3,"file":"subscription.js","sourceRoot":"","sources":["../src/subscription.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,4BAA4B,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAgB3E,MAAM,MAAM,GAAG,CAAC,OAAkC,EAAe,EAAE,CAClE,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CAAC,GAAwB;IAC7D,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC/B,OAAO;QACN,cAAc,EAAE,GAAG,CAAC,EAAE;QACtB,UAAU,EAAE,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE;QAC7E,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,MAAM,EAAE,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,IAAI,IAAI;QAC/B,gBAAgB,EAAE,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC;QAClD,iBAAiB,EAAE,GAAG,CAAC,oBAAoB;QAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;KAC/B,CAAC;AACH,CAAC;AAED;;;sDAGsD;AACtD,MAAM,CAAC,KAAK,UAAU,4BAA4B,CACjD,UAAkB,EAClB,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;QAC5C,QAAQ,EAAE,UAAU;QACpB,MAAM,EAAE,KAAK;QACb,yEAAyE;QACzE,kDAAkD;QAClD,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,CAAC,uBAAuB,CAAC;KACjC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAChF,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC7C,UAAkB,EAClB,MAAM,GAAW,SAAS,EAAE;IAE5B,MAAM,GAAG,GAAG,MAAM,4BAA4B,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACnE,OAAO,GAAG,CAAC,CAAC,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AASxC;;mCAEmC;AACnC,MAAM,UAAU,mBAAmB,CAClC,IAAY,EACZ,SAAwB,EACxB,MAAc,EACd,SAAiB,SAAS,EAAE;IAE5B,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAChF,IAAI,CAAC;QACJ,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QACtE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;IACjE,CAAC;AACF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,OAAgB,EAChB,MAAc,EACd,SAAiB,SAAS,EAAE;IAE5B,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACtE,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;IAClC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC1D,OAAO,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7D,CAAC"}
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AASxC;;mCAEmC;AACnC,MAAM,UAAU,mBAAmB,CAClC,IAAY,EACZ,SAAwB,EACxB,MAAc,EACd,MAAM,GAAW,SAAS,EAAE;IAE5B,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAChF,IAAI,CAAC;QACJ,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;QACtE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;IACjE,CAAC;AACF,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,OAAgB,EAChB,MAAc,EACd,MAAM,GAAW,SAAS,EAAE;IAE5B,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,wBAAwB,EAAE,CAAC;IACtE,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;IAClC,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;IAC1D,OAAO,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,10 @@
1
+ -- @ingram-tech/nk-billing — out-of-order webhook defense.
2
+ --
3
+ -- Stripe does not guarantee event delivery order. `recordSubscriptionStatus`
4
+ -- stamps the Stripe event's `created` timestamp here (when the caller passes
5
+ -- `eventCreated`) and refuses to overwrite the cached status with an older
6
+ -- event, so a delayed `customer.subscription.updated` can't resurrect a
7
+ -- subscription that a later `customer.subscription.deleted` already canceled.
8
+
9
+ alter table billing_credits
10
+ add column if not exists subscription_status_at timestamptz;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingram-tech/nk-billing",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "The Ingram billing foundation: composable Stripe primitives (client, customers, prices, currency, checkout, subscriptions, webhooks) plus an injection-based Postgres credit ledger and event-dedup store.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,9 +37,9 @@
37
37
  "zod": "^4.4.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@ingram-tech/nk-dev": "0.2.4",
41
- "@types/node": "^26.0.1",
42
- "typescript": "^6.0.3",
43
- "vitest": "^4.1.9"
40
+ "@ingram-tech/nk-dev": "0.5.0",
41
+ "@types/node": "^26.1.1",
42
+ "typescript": "^7.0.2",
43
+ "vitest": "^4.1.10"
44
44
  }
45
45
  }