@basaltkit/subscriptions 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @basaltkit/subscriptions
2
2
 
3
- Billing for the Basalt framework, in the style of Laravel Cashier/Soulbscription: declarative plans, subscriptions with a trial period, features with usage limits, Stripe integration, and idempotent webhooks. You need this module when your SaaS application charges monthly fees and limits features by plan.
3
+ Billing for the Basalt framework, in the style of Laravel Cashier/Soulbscription: declarative plans, subscriptions with a trial period, features with usage limits, Stripe / Paddle / Lemon Squeezy integration, and idempotent webhooks. You need this module when your SaaS application charges monthly fees and limits features by plan.
4
4
 
5
5
  ## What this module solves
6
6
 
@@ -168,6 +168,39 @@ const gateway = new StripeBillingGateway({
168
168
  export const subscriptions = new Subscriptions({ plans, gateway, fallbackPlan: 'free' })
169
169
  ```
170
170
 
171
+ ### Paddle gateway
172
+
173
+ `PaddleBillingGateway` targets **Paddle Billing** the same way (no SDK, injectable `fetch`), mapping plans to Paddle *Price IDs* (`pri_…`) and billables to *Customer IDs* (`ctm_…`). Paddle is checkout-first, so `createSubscription`/`createCheckoutSession` create a transaction and the durable subscription ref arrives on a `subscription.*` webhook.
174
+
175
+ ```ts
176
+ import { PaddleBillingGateway, Subscriptions } from '@basaltkit/subscriptions'
177
+
178
+ const gateway = new PaddleBillingGateway({
179
+ apiKey: process.env.PADDLE_API_KEY!,
180
+ webhookSecret: process.env.PADDLE_NOTIFICATION_SECRET!, // ntfset_...
181
+ priceId: (plan, period) => ({ pro: { monthly: 'pri_pro_m', yearly: 'pri_pro_y' } })[plan]![period],
182
+ customerId: async (billableId) => getOrCreatePaddleCustomer(billableId),
183
+ })
184
+ ```
185
+
186
+ Webhook signatures use Paddle's `Paddle-Signature` scheme (`ts=…;h1=…`, HMAC-SHA256 over `${ts}:${rawBody}`) — verified by the driver, with the same 5-minute timestamp tolerance as Stripe.
187
+
188
+ ### Lemon Squeezy gateway
189
+
190
+ `LemonSqueezyBillingGateway` targets the Lemon Squeezy REST API (JSON:API, no SDK), mapping plans to *Variant IDs* and using your *Store ID* for checkouts. Also checkout-first; webhook signatures use the `X-Signature` header (a bare HMAC-SHA256 hex of the raw body — no timestamp).
191
+
192
+ ```ts
193
+ import { LemonSqueezyBillingGateway, Subscriptions } from '@basaltkit/subscriptions'
194
+
195
+ const gateway = new LemonSqueezyBillingGateway({
196
+ apiKey: process.env.LEMONSQUEEZY_API_KEY!,
197
+ webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET!,
198
+ storeId: process.env.LEMONSQUEEZY_STORE_ID!,
199
+ variantId: (plan, period) => ({ pro: { monthly: '111', yearly: '222' } })[plan]![period],
200
+ customerId: async (billableId) => getLemonSqueezyCustomer(billableId), // for the portal
201
+ })
202
+ ```
203
+
171
204
  For development and testing there's `FakeBillingGateway`, which records all calls in arrays (`created`, `canceled`, `checkouts`, `portals`, `swaps`) and accepts the webhook signature `'valid'`.
172
205
 
173
206
  ### Gateway webhooks
package/dist/index.d.ts CHANGED
@@ -712,6 +712,127 @@ declare class StripeBillingGateway implements BillingGateway {
712
712
  private request;
713
713
  }
714
714
 
715
+ declare class PaddleRequestError extends BasaltError {
716
+ readonly httpStatus: number;
717
+ constructor(httpStatus: number, message: string);
718
+ }
719
+ interface PaddleGatewayOptions {
720
+ /** Paddle API key (Bearer). */
721
+ apiKey: string;
722
+ /** Notification signing secret (`pdl_ntfset_…` / `ntfset_…`) used to verify webhooks. */
723
+ webhookSecret: string;
724
+ /** Resolves the Paddle Price ID (`pri_…`) for a plan + billing period. */
725
+ priceId: (plan: string, period: BillingPeriod) => string;
726
+ /** Resolves (or ensures) the Paddle Customer ID (`ctm_…`) for a billable entity. */
727
+ customerId: (billableId: string) => string | Promise<string>;
728
+ /**
729
+ * Extracts the billable id from a verified event. Default: reads
730
+ * `data.custom_data.billableId` — which the create/checkout calls set.
731
+ */
732
+ resolveBillableId?: (event: unknown) => string | undefined;
733
+ /** Webhook timestamp tolerance in seconds. Default: 300 (5 minutes). */
734
+ tolerance?: number;
735
+ /** Injected fetch (tests). Default: global fetch. */
736
+ fetch?: typeof fetch;
737
+ /** Clock in ms (tests). Default: Date.now. */
738
+ now?: () => number;
739
+ /** API base, for tests/mocks. Default: https://api.paddle.com */
740
+ apiBase?: string;
741
+ }
742
+ /**
743
+ * Paddle **Billing** gateway targeting the Paddle REST API directly — no SDK.
744
+ * HTTP goes through an injectable fetch; webhook signatures are verified with
745
+ * node:crypto using Paddle's `Paddle-Signature` scheme (`ts=…;h1=…`).
746
+ *
747
+ * Paddle is checkout-first: `createSubscription` and `createCheckoutSession`
748
+ * both create a **transaction** (the subscription materializes once the customer
749
+ * pays, and its id arrives on a `subscription.*` webhook via `gatewayRef`).
750
+ */
751
+ declare class PaddleBillingGateway implements BillingGateway {
752
+ private readonly options;
753
+ readonly name = "paddle";
754
+ private readonly fetch;
755
+ private readonly now;
756
+ private readonly tolerance;
757
+ private readonly apiBase;
758
+ private readonly resolveBillableId;
759
+ constructor(options: PaddleGatewayOptions);
760
+ createSubscription(input: CreateSubscriptionInput): Promise<{
761
+ gatewayRef: string;
762
+ }>;
763
+ cancelSubscription(gatewayRef: string, options: {
764
+ atPeriodEnd: boolean;
765
+ }): Promise<void>;
766
+ createCheckoutSession(input: CheckoutInput): Promise<{
767
+ url: string;
768
+ id: string;
769
+ }>;
770
+ createPortalSession(input: PortalInput): Promise<{
771
+ url: string;
772
+ }>;
773
+ swapSubscription(gatewayRef: string, input: SwapInput): Promise<void>;
774
+ verifyWebhook(rawBody: string, signature: string | undefined): WebhookEvent | null;
775
+ private request;
776
+ }
777
+
778
+ declare class LemonSqueezyRequestError extends BasaltError {
779
+ readonly httpStatus: number;
780
+ constructor(httpStatus: number, message: string);
781
+ }
782
+ interface LemonSqueezyGatewayOptions {
783
+ /** Lemon Squeezy API key (Bearer). */
784
+ apiKey: string;
785
+ /** Webhook signing secret used to verify `X-Signature`. */
786
+ webhookSecret: string;
787
+ /** Lemon Squeezy Store ID (needed to create checkouts). */
788
+ storeId: string;
789
+ /** Resolves the Lemon Squeezy Variant ID for a plan + billing period. */
790
+ variantId: (plan: string, period: BillingPeriod) => string;
791
+ /** Resolves the Lemon Squeezy Customer ID for a billable — required for the portal. */
792
+ customerId?: (billableId: string) => string | Promise<string>;
793
+ /**
794
+ * Extracts the billable id from a verified event. Default: reads
795
+ * `meta.custom_data.billableId` — which the checkout call sets.
796
+ */
797
+ resolveBillableId?: (event: unknown) => string | undefined;
798
+ /** Injected fetch (tests). Default: global fetch. */
799
+ fetch?: typeof fetch;
800
+ /** API base, for tests/mocks. Default: https://api.lemonsqueezy.com/v1 */
801
+ apiBase?: string;
802
+ }
803
+ /**
804
+ * Lemon Squeezy billing gateway targeting the REST API directly (JSON:API) — no
805
+ * SDK. Lemon Squeezy is a merchant-of-record and checkout-first, so
806
+ * `createSubscription`/`createCheckoutSession` create a **checkout**; the durable
807
+ * subscription id arrives on a `subscription_*` webhook via `gatewayRef`. Webhook
808
+ * signatures use the `X-Signature` scheme (HMAC-SHA256 hex over the raw body).
809
+ */
810
+ declare class LemonSqueezyBillingGateway implements BillingGateway {
811
+ private readonly options;
812
+ readonly name = "lemonsqueezy";
813
+ private readonly fetch;
814
+ private readonly apiBase;
815
+ private readonly resolveBillableId;
816
+ constructor(options: LemonSqueezyGatewayOptions);
817
+ createSubscription(input: CreateSubscriptionInput): Promise<{
818
+ gatewayRef: string;
819
+ }>;
820
+ createCheckoutSession(input: CheckoutInput): Promise<{
821
+ url: string;
822
+ id: string;
823
+ }>;
824
+ private checkout;
825
+ cancelSubscription(gatewayRef: string, _options: {
826
+ atPeriodEnd: boolean;
827
+ }): Promise<void>;
828
+ createPortalSession(input: PortalInput): Promise<{
829
+ url: string;
830
+ }>;
831
+ swapSubscription(gatewayRef: string, input: SwapInput): Promise<void>;
832
+ verifyWebhook(rawBody: string, signature: string | undefined): WebhookEvent | null;
833
+ private request;
834
+ }
835
+
715
836
  declare class NotSubscribedError extends BasaltError {
716
837
  readonly status = 402;
717
838
  constructor();
@@ -865,4 +986,4 @@ declare function billingRoutes(options: BillingRoutesOptions): BasaltRoute[];
865
986
  */
866
987
  declare function billingWebhookRoute(gateway: BillingGateway): BasaltRoute;
867
988
 
868
- export { type BillingGateway, type BillingPeriod, type BillingRoutesOptions, type CheckoutInput, type CreateSubscriptionInput, FakeBillingGateway, FakePaymentGateway, FeatureUnavailableError, type FeatureValue, GatewayUnsupportedError, type HandleEventResult, MemoryPaymentStore, MemoryRecurringStore, MemorySubscriptionStore, MemoryUsageStore, MemoryWebhookStore, type Meter, type NewPayment, NotSubscribedError, PaymentAmountMismatchError, type PaymentApplyResult, type PaymentEvent, type PaymentGateway, type PaymentInstruction, PaymentLedger, type PaymentLedgerEvent, type PaymentLedgerEvents, type PaymentLedgerListener, type PaymentLedgerOptions, type PaymentRecord, type PaymentRecordStatus, type PaymentRequest, type PaymentStore, type PlanDefinition, type Plans, type PortalInput, QuotaExceededError, type RecurringBillingOptions, type RecurringInterval, RecurringReferenceBilling, type RecurringStatus, type RecurringStore, type RecurringSubscription, type RedisLike, RedisUsageStore, type RedisUsageStoreOptions, type RedisWebhookClient, RedisWebhookStore, type RedisWebhookStoreOptions, SUBSCRIPTIONS, StripeBillingGateway, type StripeGatewayOptions, StripeRequestError, type SubscribeInput, type SubscriptionRecord, type SubscriptionStatus, type SubscriptionStore, Subscriptions, type SubscriptionsOptions, type SubscriptionsPluginOptions, type SwapInput, UnknownPlanError, type UsageConsumeResult, type UsageStore, type WebhookEvent, WebhookInvalidError, WebhookSecretMissingError, type WebhookStore, addInterval, assertMinorUnits, billingRoutes, billingWebhookRoute, currencyDecimals, definePlans, featureLimit, formatMoney, isMeter, isMinorUnits, meter, planPrice, subscriptionsPlugin, toMajor, toMinor };
989
+ export { type BillingGateway, type BillingPeriod, type BillingRoutesOptions, type CheckoutInput, type CreateSubscriptionInput, FakeBillingGateway, FakePaymentGateway, FeatureUnavailableError, type FeatureValue, GatewayUnsupportedError, type HandleEventResult, LemonSqueezyBillingGateway, type LemonSqueezyGatewayOptions, LemonSqueezyRequestError, MemoryPaymentStore, MemoryRecurringStore, MemorySubscriptionStore, MemoryUsageStore, MemoryWebhookStore, type Meter, type NewPayment, NotSubscribedError, PaddleBillingGateway, type PaddleGatewayOptions, PaddleRequestError, PaymentAmountMismatchError, type PaymentApplyResult, type PaymentEvent, type PaymentGateway, type PaymentInstruction, PaymentLedger, type PaymentLedgerEvent, type PaymentLedgerEvents, type PaymentLedgerListener, type PaymentLedgerOptions, type PaymentRecord, type PaymentRecordStatus, type PaymentRequest, type PaymentStore, type PlanDefinition, type Plans, type PortalInput, QuotaExceededError, type RecurringBillingOptions, type RecurringInterval, RecurringReferenceBilling, type RecurringStatus, type RecurringStore, type RecurringSubscription, type RedisLike, RedisUsageStore, type RedisUsageStoreOptions, type RedisWebhookClient, RedisWebhookStore, type RedisWebhookStoreOptions, SUBSCRIPTIONS, StripeBillingGateway, type StripeGatewayOptions, StripeRequestError, type SubscribeInput, type SubscriptionRecord, type SubscriptionStatus, type SubscriptionStore, Subscriptions, type SubscriptionsOptions, type SubscriptionsPluginOptions, type SwapInput, UnknownPlanError, type UsageConsumeResult, type UsageStore, type WebhookEvent, WebhookInvalidError, WebhookSecretMissingError, type WebhookStore, addInterval, assertMinorUnits, billingRoutes, billingWebhookRoute, currencyDecimals, definePlans, featureLimit, formatMoney, isMeter, isMinorUnits, meter, planPrice, subscriptionsPlugin, toMajor, toMinor };
package/dist/index.js CHANGED
@@ -654,21 +654,264 @@ function formEncode(data) {
654
654
  return Object.entries(data).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
655
655
  }
656
656
 
657
+ // src/drivers/paddle.ts
658
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
659
+ import { BasaltError as BasaltError4 } from "@basaltkit/core";
660
+ var PaddleRequestError = class extends BasaltError4 {
661
+ constructor(httpStatus, message) {
662
+ super("BILLING_GATEWAY_ERROR", `Paddle request failed (${httpStatus}): ${message}`);
663
+ this.httpStatus = httpStatus;
664
+ }
665
+ httpStatus;
666
+ };
667
+ var EVENT_MAP2 = {
668
+ "subscription.canceled": "subscription.canceled",
669
+ "transaction.completed": "payment.succeeded",
670
+ "transaction.paid": "payment.succeeded",
671
+ "transaction.payment_failed": "payment.failed"
672
+ };
673
+ var PRORATION = {
674
+ create_prorations: "prorated_immediately",
675
+ none: "do_not_bill",
676
+ always_invoice: "full_immediately"
677
+ };
678
+ var PaddleBillingGateway = class {
679
+ constructor(options) {
680
+ this.options = options;
681
+ this.fetch = options.fetch ?? globalThis.fetch;
682
+ this.now = options.now ?? Date.now;
683
+ this.tolerance = options.tolerance ?? 300;
684
+ this.apiBase = options.apiBase ?? "https://api.paddle.com";
685
+ this.resolveBillableId = options.resolveBillableId ?? ((event) => event?.data?.custom_data?.["billableId"]);
686
+ }
687
+ options;
688
+ name = "paddle";
689
+ fetch;
690
+ now;
691
+ tolerance;
692
+ apiBase;
693
+ resolveBillableId;
694
+ async createSubscription(input) {
695
+ const customer = await this.options.customerId(input.billableId);
696
+ const created = await this.request("POST", "/transactions", {
697
+ items: [{ price_id: this.options.priceId(input.plan, input.period), quantity: 1 }],
698
+ customer_id: customer,
699
+ collection_mode: "automatic",
700
+ custom_data: { billableId: input.billableId }
701
+ });
702
+ return { gatewayRef: String(created.id) };
703
+ }
704
+ async cancelSubscription(gatewayRef, options) {
705
+ await this.request("POST", `/subscriptions/${gatewayRef}/cancel`, {
706
+ effective_from: options.atPeriodEnd ? "next_billing_period" : "immediately"
707
+ });
708
+ }
709
+ async createCheckoutSession(input) {
710
+ const customer = await this.options.customerId(input.billableId);
711
+ const created = await this.request("POST", "/transactions", {
712
+ items: [{ price_id: this.options.priceId(input.plan, input.period), quantity: 1 }],
713
+ customer_id: customer,
714
+ collection_mode: "automatic",
715
+ custom_data: { billableId: input.billableId },
716
+ checkout: { url: input.successUrl }
717
+ });
718
+ return { url: String(created.checkout?.url), id: String(created.id) };
719
+ }
720
+ async createPortalSession(input) {
721
+ const customer = await this.options.customerId(input.billableId);
722
+ const created = await this.request(
723
+ "POST",
724
+ `/customers/${customer}/portal-sessions`,
725
+ {}
726
+ );
727
+ return { url: String(created.urls?.general?.overview) };
728
+ }
729
+ async swapSubscription(gatewayRef, input) {
730
+ await this.request("PATCH", `/subscriptions/${gatewayRef}`, {
731
+ items: [{ price_id: this.options.priceId(input.plan, input.period), quantity: 1 }],
732
+ proration_billing_mode: PRORATION[input.prorationBehavior ?? "create_prorations"]
733
+ });
734
+ }
735
+ verifyWebhook(rawBody, signature) {
736
+ if (!signature) throw new WebhookInvalidError();
737
+ const parts = Object.fromEntries(
738
+ signature.split(";").map((pair) => {
739
+ const index = pair.indexOf("=");
740
+ return [pair.slice(0, index).trim(), pair.slice(index + 1)];
741
+ })
742
+ );
743
+ const timestamp = Number(parts.ts);
744
+ if (!Number.isFinite(timestamp) || !parts.h1) throw new WebhookInvalidError();
745
+ const expected = createHmac2("sha256", this.options.webhookSecret).update(`${parts.ts}:${rawBody}`).digest("hex");
746
+ const a = Buffer.from(expected);
747
+ const b = Buffer.from(parts.h1);
748
+ if (a.length !== b.length || !timingSafeEqual2(a, b)) throw new WebhookInvalidError();
749
+ if (Math.abs(this.now() / 1e3 - timestamp) > this.tolerance) throw new WebhookInvalidError();
750
+ let event;
751
+ try {
752
+ event = JSON.parse(rawBody);
753
+ } catch {
754
+ throw new WebhookInvalidError();
755
+ }
756
+ const type = event.event_type ? EVENT_MAP2[event.event_type] : void 0;
757
+ if (!type || !event.event_id) return null;
758
+ const billableId = this.resolveBillableId(event);
759
+ if (!billableId) return null;
760
+ const gatewayRef = event.data?.subscription_id ?? event.data?.id;
761
+ return { id: event.event_id, type, billableId, ...gatewayRef ? { gatewayRef } : {} };
762
+ }
763
+ async request(method, path, body) {
764
+ const response = await this.fetch(`${this.apiBase}${path}`, {
765
+ method,
766
+ headers: {
767
+ authorization: `Bearer ${this.options.apiKey}`,
768
+ "content-type": "application/json"
769
+ },
770
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
771
+ });
772
+ const text = await response.text();
773
+ const json = text ? JSON.parse(text) : {};
774
+ if (!response.ok) {
775
+ throw new PaddleRequestError(response.status, json.error?.detail ?? text ?? "unknown error");
776
+ }
777
+ return json.data ?? json;
778
+ }
779
+ };
780
+
781
+ // src/drivers/lemonsqueezy.ts
782
+ import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual3 } from "crypto";
783
+ import { BasaltError as BasaltError5 } from "@basaltkit/core";
784
+ var LemonSqueezyRequestError = class extends BasaltError5 {
785
+ constructor(httpStatus, message) {
786
+ super("BILLING_GATEWAY_ERROR", `Lemon Squeezy request failed (${httpStatus}): ${message}`);
787
+ this.httpStatus = httpStatus;
788
+ }
789
+ httpStatus;
790
+ };
791
+ var EVENT_MAP3 = {
792
+ subscription_cancelled: "subscription.canceled",
793
+ subscription_expired: "subscription.canceled",
794
+ subscription_payment_success: "payment.succeeded",
795
+ subscription_payment_failed: "payment.failed"
796
+ };
797
+ var JSON_API = "application/vnd.api+json";
798
+ var LemonSqueezyBillingGateway = class {
799
+ constructor(options) {
800
+ this.options = options;
801
+ this.fetch = options.fetch ?? globalThis.fetch;
802
+ this.apiBase = options.apiBase ?? "https://api.lemonsqueezy.com/v1";
803
+ this.resolveBillableId = options.resolveBillableId ?? ((event) => event?.meta?.custom_data?.["billableId"]);
804
+ }
805
+ options;
806
+ name = "lemonsqueezy";
807
+ fetch;
808
+ apiBase;
809
+ resolveBillableId;
810
+ async createSubscription(input) {
811
+ const checkout = await this.checkout(input.billableId, input.plan, input.period);
812
+ return { gatewayRef: String(checkout.id) };
813
+ }
814
+ async createCheckoutSession(input) {
815
+ const checkout = await this.checkout(input.billableId, input.plan, input.period, input.successUrl);
816
+ return { url: String(checkout.attributes?.url), id: String(checkout.id) };
817
+ }
818
+ async checkout(billableId, plan, period, redirectUrl) {
819
+ const created = await this.request("POST", "/checkouts", {
820
+ data: {
821
+ type: "checkouts",
822
+ attributes: {
823
+ checkout_data: { custom: { billableId } },
824
+ ...redirectUrl ? { product_options: { redirect_url: redirectUrl } } : {}
825
+ },
826
+ relationships: {
827
+ store: { data: { type: "stores", id: this.options.storeId } },
828
+ variant: { data: { type: "variants", id: this.options.variantId(plan, period) } }
829
+ }
830
+ }
831
+ });
832
+ return created;
833
+ }
834
+ async cancelSubscription(gatewayRef, _options) {
835
+ await this.request("DELETE", `/subscriptions/${gatewayRef}`);
836
+ }
837
+ async createPortalSession(input) {
838
+ if (!this.options.customerId) {
839
+ throw new LemonSqueezyRequestError(500, "customerId resolver is required for the customer portal");
840
+ }
841
+ const customer = await this.options.customerId(input.billableId);
842
+ const found = await this.request("GET", `/customers/${customer}`);
843
+ return { url: String(found.attributes?.urls?.customer_portal) };
844
+ }
845
+ async swapSubscription(gatewayRef, input) {
846
+ const behavior = input.prorationBehavior ?? "create_prorations";
847
+ await this.request("PATCH", `/subscriptions/${gatewayRef}`, {
848
+ data: {
849
+ type: "subscriptions",
850
+ id: gatewayRef,
851
+ attributes: {
852
+ variant_id: this.options.variantId(input.plan, input.period),
853
+ disable_prorations: behavior === "none",
854
+ ...behavior === "always_invoice" ? { invoice_immediately: true } : {}
855
+ }
856
+ }
857
+ });
858
+ }
859
+ verifyWebhook(rawBody, signature) {
860
+ if (!signature) throw new WebhookInvalidError();
861
+ const expected = createHmac3("sha256", this.options.webhookSecret).update(rawBody).digest("hex");
862
+ const a = Buffer.from(expected);
863
+ const b = Buffer.from(signature);
864
+ if (a.length !== b.length || !timingSafeEqual3(a, b)) throw new WebhookInvalidError();
865
+ let event;
866
+ try {
867
+ event = JSON.parse(rawBody);
868
+ } catch {
869
+ throw new WebhookInvalidError();
870
+ }
871
+ const name = event.meta?.event_name;
872
+ const type = name ? EVENT_MAP3[name] : void 0;
873
+ if (!type) return null;
874
+ const billableId = this.resolveBillableId(event);
875
+ if (!billableId) return null;
876
+ const rawRef = event.data?.attributes?.subscription_id ?? event.data?.id;
877
+ const gatewayRef = rawRef !== void 0 ? String(rawRef) : void 0;
878
+ const id = `${name}:${gatewayRef ?? billableId}`;
879
+ return { id, type, billableId, ...gatewayRef ? { gatewayRef } : {} };
880
+ }
881
+ async request(method, path, body) {
882
+ const response = await this.fetch(`${this.apiBase}${path}`, {
883
+ method,
884
+ headers: {
885
+ authorization: `Bearer ${this.options.apiKey}`,
886
+ accept: JSON_API,
887
+ ...body !== void 0 ? { "content-type": JSON_API } : {}
888
+ },
889
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
890
+ });
891
+ const text = await response.text();
892
+ const json = text ? JSON.parse(text) : {};
893
+ if (!response.ok) {
894
+ throw new LemonSqueezyRequestError(response.status, json.errors?.[0]?.detail ?? text ?? "unknown error");
895
+ }
896
+ return json.data ?? json;
897
+ }
898
+ };
899
+
657
900
  // src/subscriptions.ts
658
- import { BasaltError as BasaltError4, parseDuration } from "@basaltkit/core";
659
- var NotSubscribedError = class extends BasaltError4 {
901
+ import { BasaltError as BasaltError6, parseDuration } from "@basaltkit/core";
902
+ var NotSubscribedError = class extends BasaltError6 {
660
903
  status = 402;
661
904
  constructor() {
662
905
  super("BILLING_SUBSCRIPTION_REQUIRED", "An active subscription is required.");
663
906
  }
664
907
  };
665
- var FeatureUnavailableError = class extends BasaltError4 {
908
+ var FeatureUnavailableError = class extends BasaltError6 {
666
909
  status = 403;
667
910
  constructor(feature) {
668
911
  super("BILLING_FEATURE_UNAVAILABLE", `The feature "${feature}" is not available on this plan.`);
669
912
  }
670
913
  };
671
- var QuotaExceededError = class extends BasaltError4 {
914
+ var QuotaExceededError = class extends BasaltError6 {
672
915
  status = 402;
673
916
  constructor(feature, remaining) {
674
917
  super(
@@ -677,7 +920,7 @@ var QuotaExceededError = class extends BasaltError4 {
677
920
  );
678
921
  }
679
922
  };
680
- var GatewayUnsupportedError = class extends BasaltError4 {
923
+ var GatewayUnsupportedError = class extends BasaltError6 {
681
924
  status = 501;
682
925
  constructor(capability) {
683
926
  super("BILLING_GATEWAY_UNSUPPORTED", `The billing gateway does not support "${capability}".`);
@@ -1020,12 +1263,16 @@ export {
1020
1263
  FakePaymentGateway,
1021
1264
  FeatureUnavailableError,
1022
1265
  GatewayUnsupportedError,
1266
+ LemonSqueezyBillingGateway,
1267
+ LemonSqueezyRequestError,
1023
1268
  MemoryPaymentStore,
1024
1269
  MemoryRecurringStore,
1025
1270
  MemorySubscriptionStore,
1026
1271
  MemoryUsageStore,
1027
1272
  MemoryWebhookStore,
1028
1273
  NotSubscribedError,
1274
+ PaddleBillingGateway,
1275
+ PaddleRequestError,
1029
1276
  PaymentAmountMismatchError,
1030
1277
  PaymentLedger,
1031
1278
  QuotaExceededError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basaltkit/subscriptions",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Billing for Basalt, Cashier/Soulbscription-style: declarative plans, subscriptions with trials, feature flags, usage limits, gateway drivers and idempotent webhooks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "dependencies": {
17
17
  "@basaltkit/core": "^1.0.0",
18
- "@basaltkit/fastify": "^1.1.0"
18
+ "@basaltkit/fastify": "^1.4.0"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "zod": "^3.24.0 || ^4.0.0"