@revojs/billing 0.2.45 → 0.2.47
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/dist/index.d.ts +133 -0
- package/dist/index.js +133 -0
- package/dist/module/index.d.ts +2 -0
- package/dist/module/index.js +26 -0
- package/dist/plugins/index.server.d.ts +4 -0
- package/dist/plugins/index.server.js +17 -0
- package/dist/providers/stripe.d.ts +68 -0
- package/dist/providers/stripe.js +194 -0
- package/dist/routes/billing/[provider]/post.d.ts +4 -0
- package/dist/routes/billing/[provider]/post.js +13 -0
- package/dist/storage/mysql.d.ts +131 -0
- package/dist/storage/mysql.js +59 -0
- package/dist/storage/postgresql.d.ts +131 -0
- package/dist/storage/postgresql.js +59 -0
- package/dist/storage/sqlite.d.ts +131 -0
- package/dist/storage/sqlite.js +59 -0
- package/dist/types/index.d.ts +8 -0
- package/package.json +55 -55
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { type Resolvable, type Scope, type Source, type SourceScope, type SourceValue } from "revojs";
|
|
2
|
+
import type { Awaitable } from "revojs/shared";
|
|
3
|
+
export type SubscriptionStatus = "incomplete" | "expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | "paused";
|
|
4
|
+
export interface Customer {
|
|
5
|
+
id: string;
|
|
6
|
+
provider: string;
|
|
7
|
+
externalId: string;
|
|
8
|
+
reference: string;
|
|
9
|
+
email: string;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
}
|
|
12
|
+
export interface Subscription {
|
|
13
|
+
id: string;
|
|
14
|
+
provider: string;
|
|
15
|
+
externalId: string;
|
|
16
|
+
status: SubscriptionStatus;
|
|
17
|
+
price: string;
|
|
18
|
+
quantity: number;
|
|
19
|
+
currentPeriodEnd: Date;
|
|
20
|
+
cancelAtPeriodEnd: boolean;
|
|
21
|
+
updatedAt: Date;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
customerId: string;
|
|
24
|
+
}
|
|
25
|
+
export type SubscriptionValues = Pick<Subscription, "status" | "price" | "quantity" | "currentPeriodEnd" | "cancelAtPeriodEnd">;
|
|
26
|
+
export interface BillingStorage {
|
|
27
|
+
getCustomerByReference: (provider: string, reference: string) => Promise<Customer | undefined>;
|
|
28
|
+
getCustomerByExternalId: (provider: string, externalId: string) => Promise<Customer | undefined>;
|
|
29
|
+
createCustomer: (customer: Pick<Customer, "provider" | "externalId" | "reference" | "email">) => Promise<void>;
|
|
30
|
+
getSubscriptionByExternalId: (provider: string, externalId: string) => Promise<Subscription | undefined>;
|
|
31
|
+
getSubscriptions: (customerId: string) => Promise<Array<Subscription>>;
|
|
32
|
+
createSubscription: (subscription: SubscriptionValues & Pick<Subscription, "provider" | "externalId" | "customerId">) => Promise<void>;
|
|
33
|
+
updateSubscription: (id: string, values: SubscriptionValues) => Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export type CheckoutItem = {
|
|
36
|
+
price: string;
|
|
37
|
+
quantity?: number;
|
|
38
|
+
} | {
|
|
39
|
+
name: string;
|
|
40
|
+
amount: number;
|
|
41
|
+
currency: string;
|
|
42
|
+
quantity?: number;
|
|
43
|
+
};
|
|
44
|
+
export interface CheckoutOptions {
|
|
45
|
+
reference: string;
|
|
46
|
+
email: string;
|
|
47
|
+
name?: string;
|
|
48
|
+
mode: "payment" | "subscription";
|
|
49
|
+
items: Array<CheckoutItem>;
|
|
50
|
+
successUrl: string;
|
|
51
|
+
cancelUrl: string;
|
|
52
|
+
metadata?: Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
export interface Checkout {
|
|
55
|
+
id: string;
|
|
56
|
+
provider: string;
|
|
57
|
+
paid: boolean;
|
|
58
|
+
reference: string | null;
|
|
59
|
+
customer: string | null;
|
|
60
|
+
subscription: string | null;
|
|
61
|
+
amount: number | null;
|
|
62
|
+
currency: string | null;
|
|
63
|
+
metadata: Record<string, string>;
|
|
64
|
+
}
|
|
65
|
+
export interface ProviderSubscription extends SubscriptionValues {
|
|
66
|
+
id: string;
|
|
67
|
+
customer: string;
|
|
68
|
+
}
|
|
69
|
+
export interface BillingEvent {
|
|
70
|
+
id: string;
|
|
71
|
+
provider: string;
|
|
72
|
+
type: string;
|
|
73
|
+
checkout?: string;
|
|
74
|
+
subscription?: string;
|
|
75
|
+
data: unknown;
|
|
76
|
+
}
|
|
77
|
+
export interface BillingProvider {
|
|
78
|
+
createCustomer: (input: {
|
|
79
|
+
reference: string;
|
|
80
|
+
email: string;
|
|
81
|
+
name?: string;
|
|
82
|
+
}) => Promise<{
|
|
83
|
+
id: string;
|
|
84
|
+
}>;
|
|
85
|
+
createCheckout: (input: Omit<CheckoutOptions, "email" | "name"> & {
|
|
86
|
+
customer: string;
|
|
87
|
+
}) => Promise<{
|
|
88
|
+
id: string;
|
|
89
|
+
url: string;
|
|
90
|
+
}>;
|
|
91
|
+
createPortal: (input: {
|
|
92
|
+
customer: string;
|
|
93
|
+
returnUrl: string;
|
|
94
|
+
}) => Promise<{
|
|
95
|
+
url: string;
|
|
96
|
+
}>;
|
|
97
|
+
getCheckout: (id: string) => Promise<Omit<Checkout, "provider">>;
|
|
98
|
+
getSubscription: (id: string) => Promise<ProviderSubscription>;
|
|
99
|
+
receiveWebhook: (request: Request) => Promise<Omit<BillingEvent, "provider"> | undefined>;
|
|
100
|
+
}
|
|
101
|
+
export declare class Billing {
|
|
102
|
+
readonly name: string;
|
|
103
|
+
readonly provider: BillingProvider;
|
|
104
|
+
protected $scope: Scope;
|
|
105
|
+
constructor(scope: Scope, name: string, provider: BillingProvider);
|
|
106
|
+
ensureCustomer(input: {
|
|
107
|
+
reference: string;
|
|
108
|
+
email: string;
|
|
109
|
+
name?: string;
|
|
110
|
+
}): Promise<Customer>;
|
|
111
|
+
getCustomer(reference: string): Promise<Customer | undefined>;
|
|
112
|
+
createCheckout(options: CheckoutOptions): Promise<{
|
|
113
|
+
id: string;
|
|
114
|
+
url: string;
|
|
115
|
+
}>;
|
|
116
|
+
createPortal(options: {
|
|
117
|
+
reference: string;
|
|
118
|
+
returnUrl: string;
|
|
119
|
+
}): Promise<{
|
|
120
|
+
url: string;
|
|
121
|
+
}>;
|
|
122
|
+
getSubscriptions(reference: string): Promise<Subscription[]>;
|
|
123
|
+
syncSubscription(externalId: string): Promise<Subscription | undefined>;
|
|
124
|
+
handleWebhook(request: Request): Promise<boolean>;
|
|
125
|
+
}
|
|
126
|
+
export declare function useBillingStorage(scope: Scope): Promise<BillingStorage>;
|
|
127
|
+
export declare function useBillingContext(scope: Scope): Record<string, Record<string, Billing>>;
|
|
128
|
+
export declare function useBilling<TSource extends Source<Resolvable<Awaitable<BillingProvider>>> = typeof import("#billing").default, TScope extends SourceScope<TSource> = SourceScope<TSource>>(scope: Scope, scopeName: TScope): Record<SourceValue<TSource, TScope>, Billing>;
|
|
129
|
+
export declare function findBilling(scope: Scope, name: string): Billing;
|
|
130
|
+
export declare const BILLING_CONTEXT: import("revojs").Descriptor<Record<string, Record<string, Billing>>>;
|
|
131
|
+
export declare const CHECKOUT_PAID_HOOK: import("revojs").Descriptor<import("revojs").Hook<Readonly<Checkout>, Awaitable<void>>>;
|
|
132
|
+
export declare const SUBSCRIPTION_UPDATED_HOOK: import("revojs").Descriptor<import("revojs").Hook<Readonly<Subscription>, Awaitable<void>>>;
|
|
133
|
+
export declare const BILLING_EVENT_HOOK: import("revojs").Descriptor<import("revojs").Hook<Readonly<BillingEvent>, Awaitable<void>>>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useStorageProvider } from "@revojs/storage";
|
|
2
|
+
import { defineContext, defineHook, dispatchHook, getContext, setContext } from "revojs";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
var Billing = class {
|
|
5
|
+
name;
|
|
6
|
+
provider;
|
|
7
|
+
$scope;
|
|
8
|
+
constructor(scope, name, provider) {
|
|
9
|
+
this.$scope = scope;
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.provider = provider;
|
|
12
|
+
}
|
|
13
|
+
async ensureCustomer(input) {
|
|
14
|
+
const storage = await useBillingStorage(this.$scope);
|
|
15
|
+
const existing = await storage.getCustomerByReference(this.name, input.reference);
|
|
16
|
+
if (existing) return existing;
|
|
17
|
+
const customer = await this.provider.createCustomer(input);
|
|
18
|
+
try {
|
|
19
|
+
await storage.createCustomer({
|
|
20
|
+
provider: this.name,
|
|
21
|
+
externalId: customer.id,
|
|
22
|
+
reference: input.reference,
|
|
23
|
+
email: input.email
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
const concurrent = await storage.getCustomerByReference(this.name, input.reference);
|
|
27
|
+
if (concurrent) return concurrent;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const created = await storage.getCustomerByReference(this.name, input.reference);
|
|
31
|
+
if (!created) throw new Error(`The ${this.name} customer for reference "${input.reference}" could not be stored.`);
|
|
32
|
+
return created;
|
|
33
|
+
}
|
|
34
|
+
async getCustomer(reference) {
|
|
35
|
+
return (await useBillingStorage(this.$scope)).getCustomerByReference(this.name, reference);
|
|
36
|
+
}
|
|
37
|
+
async createCheckout(options) {
|
|
38
|
+
const customer = await this.ensureCustomer(options);
|
|
39
|
+
return this.provider.createCheckout({
|
|
40
|
+
customer: customer.externalId,
|
|
41
|
+
reference: options.reference,
|
|
42
|
+
mode: options.mode,
|
|
43
|
+
items: options.items,
|
|
44
|
+
successUrl: options.successUrl,
|
|
45
|
+
cancelUrl: options.cancelUrl,
|
|
46
|
+
metadata: options.metadata
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async createPortal(options) {
|
|
50
|
+
const customer = await this.getCustomer(options.reference);
|
|
51
|
+
if (!customer) throw new Error(`No ${this.name} customer exists for reference "${options.reference}".`);
|
|
52
|
+
return this.provider.createPortal({
|
|
53
|
+
customer: customer.externalId,
|
|
54
|
+
returnUrl: options.returnUrl
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async getSubscriptions(reference) {
|
|
58
|
+
const storage = await useBillingStorage(this.$scope);
|
|
59
|
+
const customer = await storage.getCustomerByReference(this.name, reference);
|
|
60
|
+
if (!customer) return [];
|
|
61
|
+
return storage.getSubscriptions(customer.id);
|
|
62
|
+
}
|
|
63
|
+
async syncSubscription(externalId) {
|
|
64
|
+
const subscription = await this.provider.getSubscription(externalId);
|
|
65
|
+
const storage = await useBillingStorage(this.$scope);
|
|
66
|
+
const customer = await storage.getCustomerByExternalId(this.name, subscription.customer);
|
|
67
|
+
if (!customer) return;
|
|
68
|
+
const values = {
|
|
69
|
+
status: subscription.status,
|
|
70
|
+
price: subscription.price,
|
|
71
|
+
quantity: subscription.quantity,
|
|
72
|
+
currentPeriodEnd: subscription.currentPeriodEnd,
|
|
73
|
+
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd
|
|
74
|
+
};
|
|
75
|
+
const existing = await storage.getSubscriptionByExternalId(this.name, subscription.id);
|
|
76
|
+
await (existing ? storage.updateSubscription(existing.id, values) : storage.createSubscription({
|
|
77
|
+
provider: this.name,
|
|
78
|
+
externalId: subscription.id,
|
|
79
|
+
customerId: customer.id,
|
|
80
|
+
...values
|
|
81
|
+
}));
|
|
82
|
+
return storage.getSubscriptionByExternalId(this.name, subscription.id);
|
|
83
|
+
}
|
|
84
|
+
async handleWebhook(request) {
|
|
85
|
+
const received = await this.provider.receiveWebhook(request);
|
|
86
|
+
if (!received) return false;
|
|
87
|
+
const event = {
|
|
88
|
+
...received,
|
|
89
|
+
provider: this.name
|
|
90
|
+
};
|
|
91
|
+
if (event.checkout) {
|
|
92
|
+
const checkout = await this.provider.getCheckout(event.checkout);
|
|
93
|
+
if (checkout.paid) await dispatchHook(this.$scope, CHECKOUT_PAID_HOOK, {
|
|
94
|
+
...checkout,
|
|
95
|
+
provider: this.name
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (event.subscription) {
|
|
99
|
+
const subscription = await this.syncSubscription(event.subscription);
|
|
100
|
+
if (subscription) await dispatchHook(this.$scope, SUBSCRIPTION_UPDATED_HOOK, subscription);
|
|
101
|
+
}
|
|
102
|
+
await dispatchHook(this.$scope, BILLING_EVENT_HOOK, event);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
async function useBillingStorage(scope) {
|
|
107
|
+
return useStorageProvider(scope, await Promise.all([
|
|
108
|
+
import("./storage/sqlite.js"),
|
|
109
|
+
import("./storage/postgresql.js"),
|
|
110
|
+
import("./storage/mysql.js")
|
|
111
|
+
]), (provider) => provider.billingSchema).useBillingStorage(scope);
|
|
112
|
+
}
|
|
113
|
+
function useBillingContext(scope) {
|
|
114
|
+
return getContext(scope, BILLING_CONTEXT);
|
|
115
|
+
}
|
|
116
|
+
function useBilling(scope, scopeName) {
|
|
117
|
+
const billings = useBillingContext(scope);
|
|
118
|
+
const billing = billings[scopeName] ?? {};
|
|
119
|
+
billings[scopeName] = billing;
|
|
120
|
+
setContext(scope, BILLING_CONTEXT, billings);
|
|
121
|
+
return billing;
|
|
122
|
+
}
|
|
123
|
+
function findBilling(scope, name) {
|
|
124
|
+
const matches = Object.values(useBillingContext(scope)).map((entries) => entries[name]).filter((billing) => billing !== void 0);
|
|
125
|
+
if (matches.length > 1) throw new Error(`The billing provider name "${name}" is defined in more than one package.`);
|
|
126
|
+
return matches[0];
|
|
127
|
+
}
|
|
128
|
+
var BILLING_CONTEXT = defineContext("BILLING_CONTEXT");
|
|
129
|
+
var CHECKOUT_PAID_HOOK = defineHook("CHECKOUT_PAID_HOOK");
|
|
130
|
+
var SUBSCRIPTION_UPDATED_HOOK = defineHook("SUBSCRIPTION_UPDATED_HOOK");
|
|
131
|
+
var BILLING_EVENT_HOOK = defineHook("BILLING_EVENT_HOOK");
|
|
132
|
+
//#endregion
|
|
133
|
+
export { BILLING_CONTEXT, BILLING_EVENT_HOOK, Billing, CHECKOUT_PAID_HOOK, SUBSCRIPTION_UPDATED_HOOK, findBilling, useBilling, useBillingContext, useBillingStorage };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import server, { addRoutes } from "@revojs/server/module";
|
|
2
|
+
import { defineModule, useModule } from "revojs/module";
|
|
3
|
+
//#region src/module/index.ts
|
|
4
|
+
var module_default = defineModule({
|
|
5
|
+
name: "billing",
|
|
6
|
+
modules: [server],
|
|
7
|
+
setup(config) {
|
|
8
|
+
const { fromModule } = useModule(import.meta.url);
|
|
9
|
+
addRoutes(config, fromModule("../routes"));
|
|
10
|
+
return { sources: {
|
|
11
|
+
billing: {
|
|
12
|
+
from: ["./billing"],
|
|
13
|
+
match: [
|
|
14
|
+
"**/*.js",
|
|
15
|
+
"**/*.ts",
|
|
16
|
+
"!**/*.d.ts"
|
|
17
|
+
],
|
|
18
|
+
format: "object",
|
|
19
|
+
consumers: ["server"]
|
|
20
|
+
},
|
|
21
|
+
plugins: { from: [fromModule("../plugins")] }
|
|
22
|
+
} };
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
//#endregion
|
|
26
|
+
export { module_default as default };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BILLING_CONTEXT, Billing, useBillingContext } from "../index.js";
|
|
2
|
+
import { definePlugin, setContext } from "revojs";
|
|
3
|
+
//#region src/plugins/index.server.ts
|
|
4
|
+
var index_server_default = definePlugin({ async setup(scope) {
|
|
5
|
+
const billings = useBillingContext(scope);
|
|
6
|
+
for (const [scopeName, files] of Object.entries((await import("#billing")).default)) {
|
|
7
|
+
const entries = billings[scopeName] ?? {};
|
|
8
|
+
for (const [name, resolve] of Object.entries(files)) {
|
|
9
|
+
if (entries[name]) continue;
|
|
10
|
+
entries[name] = new Billing(scope, name, await (await resolve())(scope));
|
|
11
|
+
}
|
|
12
|
+
billings[scopeName] = entries;
|
|
13
|
+
}
|
|
14
|
+
setContext(scope, BILLING_CONTEXT, billings);
|
|
15
|
+
} });
|
|
16
|
+
//#endregion
|
|
17
|
+
export { index_server_default as default };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { BillingEvent, BillingProvider, SubscriptionStatus } from "../index";
|
|
2
|
+
export type StripeParameter = string | number | boolean | null | undefined | Array<StripeParameter> | {
|
|
3
|
+
[key: string]: StripeParameter;
|
|
4
|
+
};
|
|
5
|
+
export interface StripeOptions {
|
|
6
|
+
secretKey: string;
|
|
7
|
+
webhookSecret: string;
|
|
8
|
+
tolerance?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface StripeRequestOptions {
|
|
11
|
+
parameters?: Record<string, StripeParameter>;
|
|
12
|
+
idempotencyKey?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface StripeEvent {
|
|
15
|
+
id: string;
|
|
16
|
+
type: string;
|
|
17
|
+
created: number;
|
|
18
|
+
livemode: boolean;
|
|
19
|
+
data: {
|
|
20
|
+
object: {
|
|
21
|
+
id?: string;
|
|
22
|
+
object?: string;
|
|
23
|
+
} & Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export interface StripeCheckoutSession {
|
|
27
|
+
id: string;
|
|
28
|
+
mode: "payment" | "subscription" | "setup";
|
|
29
|
+
status: "open" | "complete" | "expired" | null;
|
|
30
|
+
payment_status: "paid" | "unpaid" | "no_payment_required";
|
|
31
|
+
client_reference_id: string | null;
|
|
32
|
+
customer: string | null;
|
|
33
|
+
subscription: string | null;
|
|
34
|
+
amount_total: number | null;
|
|
35
|
+
currency: string | null;
|
|
36
|
+
metadata: Record<string, string> | null;
|
|
37
|
+
url: string | null;
|
|
38
|
+
}
|
|
39
|
+
export interface StripeSubscription {
|
|
40
|
+
id: string;
|
|
41
|
+
status: Exclude<SubscriptionStatus, "expired"> | "incomplete_expired";
|
|
42
|
+
customer: string;
|
|
43
|
+
cancel_at_period_end: boolean;
|
|
44
|
+
items: {
|
|
45
|
+
data: Array<{
|
|
46
|
+
price: {
|
|
47
|
+
id: string;
|
|
48
|
+
};
|
|
49
|
+
quantity?: number;
|
|
50
|
+
current_period_end: number;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export declare class StripeError extends Error {
|
|
55
|
+
status: number;
|
|
56
|
+
code: string | undefined;
|
|
57
|
+
type: string | undefined;
|
|
58
|
+
constructor(message: string, status: number, code?: string, type?: string);
|
|
59
|
+
}
|
|
60
|
+
export declare function toStripeForm(parameters: Record<string, StripeParameter>): URLSearchParams;
|
|
61
|
+
export declare function requestStripe<T>(secretKey: string, method: "GET" | "POST" | "DELETE", path: string, options?: StripeRequestOptions): Promise<T>;
|
|
62
|
+
export declare function verifyStripeSignature(payload: string, header: string | null, secret: string, tolerance: number, now?: number): Promise<boolean>;
|
|
63
|
+
export declare function toStripeEvent(event: StripeEvent): Omit<BillingEvent, "provider">;
|
|
64
|
+
export declare function stripe(options: StripeOptions): BillingProvider;
|
|
65
|
+
export declare const STRIPE_API_URL = "https://api.stripe.com/";
|
|
66
|
+
export declare const STRIPE_API_VERSION = "2025-03-31.basil";
|
|
67
|
+
export declare const STRIPE_CHECKOUT_EVENTS: Set<string>;
|
|
68
|
+
export declare const STRIPE_SUBSCRIPTION_EVENTS: Set<string>;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { equalsConstantTime } from "revojs";
|
|
2
|
+
//#region src/providers/stripe.ts
|
|
3
|
+
var StripeError = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
type;
|
|
7
|
+
constructor(message, status, code, type) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "StripeError";
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.type = type;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function toStripeForm(parameters) {
|
|
16
|
+
const form = new URLSearchParams();
|
|
17
|
+
const append = (key, value) => {
|
|
18
|
+
if (value === void 0) return;
|
|
19
|
+
if (value === null) {
|
|
20
|
+
form.append(key, "");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
for (const [index, item] of value.entries()) append(`${key}[${index}]`, item);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "object") {
|
|
28
|
+
for (const [name, item] of Object.entries(value)) append(`${key}[${name}]`, item);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
form.append(key, String(value));
|
|
32
|
+
};
|
|
33
|
+
for (const [key, value] of Object.entries(parameters)) append(key, value);
|
|
34
|
+
return form;
|
|
35
|
+
}
|
|
36
|
+
async function requestStripe(secretKey, method, path, options = {}) {
|
|
37
|
+
const url = new URL(path, STRIPE_API_URL);
|
|
38
|
+
const headers = new Headers({
|
|
39
|
+
authorization: `Bearer ${secretKey}`,
|
|
40
|
+
"stripe-version": STRIPE_API_VERSION
|
|
41
|
+
});
|
|
42
|
+
if (options.idempotencyKey) headers.set("idempotency-key", options.idempotencyKey);
|
|
43
|
+
const form = toStripeForm(options.parameters ?? {});
|
|
44
|
+
const init = {
|
|
45
|
+
method,
|
|
46
|
+
headers,
|
|
47
|
+
redirect: "manual"
|
|
48
|
+
};
|
|
49
|
+
if (method === "GET") url.search = form.toString();
|
|
50
|
+
else init.body = form;
|
|
51
|
+
const response = await fetch(url, init);
|
|
52
|
+
const result = await response.json().catch(() => void 0);
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
const error = result?.error;
|
|
55
|
+
throw new StripeError(`Stripe rejected ${method} ${url.pathname} with HTTP ${response.status}: ${error?.message ?? response.statusText}`, response.status, error?.code, error?.type);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
async function verifyStripeSignature(payload, header, secret, tolerance, now = Date.now()) {
|
|
60
|
+
if (!header) return false;
|
|
61
|
+
let timestamp;
|
|
62
|
+
const signatures = new Array();
|
|
63
|
+
for (const part of header.split(",")) {
|
|
64
|
+
const separator = part.indexOf("=");
|
|
65
|
+
const key = part.slice(0, separator).trim();
|
|
66
|
+
const value = part.slice(separator + 1).trim();
|
|
67
|
+
if (separator === -1 || !value) continue;
|
|
68
|
+
if (key === "t") timestamp = Number(value);
|
|
69
|
+
if (key === "v1") signatures.push(value);
|
|
70
|
+
}
|
|
71
|
+
if (timestamp === void 0 || !Number.isInteger(timestamp) || signatures.length === 0) return false;
|
|
72
|
+
if (Math.abs(now / 1e3 - timestamp) > tolerance) return false;
|
|
73
|
+
const encoder = new TextEncoder();
|
|
74
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
75
|
+
name: "HMAC",
|
|
76
|
+
hash: "SHA-256"
|
|
77
|
+
}, false, ["sign"]);
|
|
78
|
+
const digest = await crypto.subtle.sign("HMAC", key, encoder.encode(`${timestamp}.${payload}`));
|
|
79
|
+
const expected = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
80
|
+
return signatures.some((signature) => equalsConstantTime(signature, expected));
|
|
81
|
+
}
|
|
82
|
+
function toStripeEvent(event) {
|
|
83
|
+
const id = event.data.object.id;
|
|
84
|
+
return {
|
|
85
|
+
id: event.id,
|
|
86
|
+
type: event.type,
|
|
87
|
+
checkout: id && STRIPE_CHECKOUT_EVENTS.has(event.type) ? id : void 0,
|
|
88
|
+
subscription: id && STRIPE_SUBSCRIPTION_EVENTS.has(event.type) ? id : void 0,
|
|
89
|
+
data: event
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function stripe(options) {
|
|
93
|
+
return {
|
|
94
|
+
async createCustomer(input) {
|
|
95
|
+
return requestStripe(options.secretKey, "POST", "/v1/customers", {
|
|
96
|
+
parameters: {
|
|
97
|
+
email: input.email,
|
|
98
|
+
name: input.name,
|
|
99
|
+
metadata: { reference: input.reference }
|
|
100
|
+
},
|
|
101
|
+
idempotencyKey: `customer:${input.reference}`
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
async createCheckout(input) {
|
|
105
|
+
const session = await requestStripe(options.secretKey, "POST", "/v1/checkout/sessions", { parameters: {
|
|
106
|
+
mode: input.mode,
|
|
107
|
+
customer: input.customer,
|
|
108
|
+
client_reference_id: input.reference,
|
|
109
|
+
success_url: input.successUrl,
|
|
110
|
+
cancel_url: input.cancelUrl,
|
|
111
|
+
line_items: input.items.map(toLineItem),
|
|
112
|
+
metadata: input.metadata,
|
|
113
|
+
billing_address_collection: "required",
|
|
114
|
+
tax_id_collection: { enabled: true },
|
|
115
|
+
customer_update: {
|
|
116
|
+
address: "auto",
|
|
117
|
+
name: "auto"
|
|
118
|
+
},
|
|
119
|
+
invoice_creation: input.mode === "payment" ? { enabled: true } : void 0,
|
|
120
|
+
payment_intent_data: input.mode === "payment" ? { metadata: input.metadata } : void 0,
|
|
121
|
+
subscription_data: input.mode === "subscription" ? { metadata: input.metadata } : void 0
|
|
122
|
+
} });
|
|
123
|
+
if (!session.url) throw new Error(`Stripe returned checkout session ${session.id} without a URL.`);
|
|
124
|
+
return {
|
|
125
|
+
id: session.id,
|
|
126
|
+
url: session.url
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
async createPortal(input) {
|
|
130
|
+
return { url: (await requestStripe(options.secretKey, "POST", "/v1/billing_portal/sessions", { parameters: {
|
|
131
|
+
customer: input.customer,
|
|
132
|
+
return_url: input.returnUrl
|
|
133
|
+
} })).url };
|
|
134
|
+
},
|
|
135
|
+
async getCheckout(id) {
|
|
136
|
+
const session = await requestStripe(options.secretKey, "GET", `/v1/checkout/sessions/${encodeURIComponent(id)}`);
|
|
137
|
+
return {
|
|
138
|
+
id: session.id,
|
|
139
|
+
paid: session.payment_status === "paid" || session.payment_status === "no_payment_required",
|
|
140
|
+
reference: session.client_reference_id,
|
|
141
|
+
customer: session.customer,
|
|
142
|
+
subscription: session.subscription,
|
|
143
|
+
amount: session.amount_total,
|
|
144
|
+
currency: session.currency,
|
|
145
|
+
metadata: session.metadata ?? {}
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
async getSubscription(id) {
|
|
149
|
+
const subscription = await requestStripe(options.secretKey, "GET", `/v1/subscriptions/${encodeURIComponent(id)}`);
|
|
150
|
+
const [item] = subscription.items.data;
|
|
151
|
+
if (!item) throw new Error(`Stripe subscription ${subscription.id} has no items.`);
|
|
152
|
+
return {
|
|
153
|
+
id: subscription.id,
|
|
154
|
+
customer: subscription.customer,
|
|
155
|
+
status: subscription.status === "incomplete_expired" ? "expired" : subscription.status,
|
|
156
|
+
price: item.price.id,
|
|
157
|
+
quantity: item.quantity ?? 1,
|
|
158
|
+
currentPeriodEnd: /* @__PURE__ */ new Date(item.current_period_end * 1e3),
|
|
159
|
+
cancelAtPeriodEnd: subscription.cancel_at_period_end
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
async receiveWebhook(request) {
|
|
163
|
+
const payload = await request.text();
|
|
164
|
+
if (!await verifyStripeSignature(payload, request.headers.get("stripe-signature"), options.webhookSecret, options.tolerance ?? 300)) return;
|
|
165
|
+
return toStripeEvent(JSON.parse(payload));
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function toLineItem(item) {
|
|
170
|
+
if ("price" in item) return {
|
|
171
|
+
price: item.price,
|
|
172
|
+
quantity: item.quantity ?? 1
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
price_data: {
|
|
176
|
+
currency: item.currency,
|
|
177
|
+
unit_amount: item.amount,
|
|
178
|
+
product_data: { name: item.name }
|
|
179
|
+
},
|
|
180
|
+
quantity: item.quantity ?? 1
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
var STRIPE_API_URL = "https://api.stripe.com/";
|
|
184
|
+
var STRIPE_API_VERSION = "2025-03-31.basil";
|
|
185
|
+
var STRIPE_CHECKOUT_EVENTS = /* @__PURE__ */ new Set(["checkout.session.completed", "checkout.session.async_payment_succeeded"]);
|
|
186
|
+
var STRIPE_SUBSCRIPTION_EVENTS = /* @__PURE__ */ new Set([
|
|
187
|
+
"customer.subscription.created",
|
|
188
|
+
"customer.subscription.updated",
|
|
189
|
+
"customer.subscription.deleted",
|
|
190
|
+
"customer.subscription.paused",
|
|
191
|
+
"customer.subscription.resumed"
|
|
192
|
+
]);
|
|
193
|
+
//#endregion
|
|
194
|
+
export { STRIPE_API_URL, STRIPE_API_VERSION, STRIPE_CHECKOUT_EVENTS, STRIPE_SUBSCRIPTION_EVENTS, StripeError, requestStripe, stripe, toStripeEvent, toStripeForm, verifyStripeSignature };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { findBilling } from "../../../index.js";
|
|
2
|
+
import { defineRoute, sendBadRequest, sendNotFound, sendOk, useParameters, useServer } from "@revojs/server";
|
|
3
|
+
//#region src/routes/billing/[provider]/post.ts
|
|
4
|
+
var post_default = defineRoute({ async fetch(scope) {
|
|
5
|
+
const { provider } = useParameters(scope);
|
|
6
|
+
const { request } = useServer(scope);
|
|
7
|
+
const billing = provider ? findBilling(scope, provider) : void 0;
|
|
8
|
+
if (!billing) return sendNotFound(scope);
|
|
9
|
+
if (!await billing.handleWebhook(request)) return sendBadRequest(scope);
|
|
10
|
+
return sendOk(scope);
|
|
11
|
+
} });
|
|
12
|
+
//#endregion
|
|
13
|
+
export { post_default as default };
|