@faire-auth/stripe 1.0.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/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +1578 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +22 -0
- package/LICENSE +661 -0
- package/dist/client.cjs +15 -0
- package/dist/client.d.cts +25 -0
- package/dist/client.d.mts +25 -0
- package/dist/client.mjs +14 -0
- package/dist/index-6UTHxaqg.d.cts +1110 -0
- package/dist/index-CYV4awaB.d.mts +1110 -0
- package/dist/index.cjs +992 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +968 -0
- package/overrides.d.ts +32 -0
- package/package.json +69 -0
- package/src/client.ts +35 -0
- package/src/hooks.ts +265 -0
- package/src/index.ts +1533 -0
- package/src/schema.ts +92 -0
- package/src/stripe.test.ts +1425 -0
- package/src/types.ts +355 -0
- package/src/utils.ts +70 -0
- package/tsconfig.json +17 -0
- package/tsdown.config.ts +16 -0
- package/vitest.config.ts +9 -0
package/overrides.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthRouteConfig,
|
|
3
|
+
JSONRenderRespond,
|
|
4
|
+
// LiteralStringUnion,
|
|
5
|
+
} from "@faire-auth/core/types";
|
|
6
|
+
import type { AuthContext } from "faire-auth";
|
|
7
|
+
|
|
8
|
+
declare module "hono" {
|
|
9
|
+
export interface ContextRenderer extends JSONRenderRespond {}
|
|
10
|
+
export interface ContextVariableMap {
|
|
11
|
+
/**
|
|
12
|
+
* Gets set early before hitting route handler
|
|
13
|
+
*/
|
|
14
|
+
// path: LiteralStringUnion<Configs["path"]>;
|
|
15
|
+
/**
|
|
16
|
+
* Gets set at route handler for intraroute consistency
|
|
17
|
+
*/
|
|
18
|
+
config?: AuthRouteConfig;
|
|
19
|
+
/**
|
|
20
|
+
* Gets set by initContext middleware
|
|
21
|
+
*/
|
|
22
|
+
context: AuthContext;
|
|
23
|
+
/**
|
|
24
|
+
* Gets set in endpoint Route Apps
|
|
25
|
+
*/
|
|
26
|
+
isServer?: true;
|
|
27
|
+
/**
|
|
28
|
+
* Gets set by initContext middleware
|
|
29
|
+
*/
|
|
30
|
+
// api: DefaultAPI;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@faire-auth/stripe",
|
|
3
|
+
"author": "Bereket Engida",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"stripe",
|
|
9
|
+
"auth",
|
|
10
|
+
"stripe"
|
|
11
|
+
],
|
|
12
|
+
"main": "dist/index.cjs",
|
|
13
|
+
"module": "dist/index.mjs",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/igoforth/faire-auth.git",
|
|
17
|
+
"directory": "packages/stripe"
|
|
18
|
+
},
|
|
19
|
+
"description": "Stripe plugin for Faire Auth",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"dev-source": "./src/index.ts",
|
|
26
|
+
"import": {
|
|
27
|
+
"types": "./dist/index.d.mts",
|
|
28
|
+
"default": "./dist/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"types": "./dist/index.d.cts",
|
|
32
|
+
"default": "./dist/index.cjs"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./client": {
|
|
36
|
+
"dev-source": "./src/client.ts",
|
|
37
|
+
"import": {
|
|
38
|
+
"types": "./dist/client.d.mts",
|
|
39
|
+
"default": "./dist/client.mjs"
|
|
40
|
+
},
|
|
41
|
+
"require": {
|
|
42
|
+
"types": "./dist/client.d.cts",
|
|
43
|
+
"default": "./dist/client.cjs"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"zod": "4.1.4"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"stripe": "^18 || ^19",
|
|
52
|
+
"hono": ">=4.3.6",
|
|
53
|
+
"@faire-auth/core": "1.0.0",
|
|
54
|
+
"faire-auth": "1.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"stripe": "^18.5.0",
|
|
58
|
+
"tsdown": "^0.16.0",
|
|
59
|
+
"publint": "^0.3.15",
|
|
60
|
+
"@faire-auth/core": "1.0.0",
|
|
61
|
+
"faire-auth": "1.0.0"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"test": "vitest",
|
|
65
|
+
"build": "tsdown",
|
|
66
|
+
"dev": "tsdown --watch",
|
|
67
|
+
"typecheck": "tsc --project tsconfig.json"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FaireAuthClientPlugin } from "faire-auth";
|
|
2
|
+
import type { stripe } from "./index";
|
|
3
|
+
|
|
4
|
+
export const stripeClient = <
|
|
5
|
+
O extends {
|
|
6
|
+
subscription: boolean;
|
|
7
|
+
},
|
|
8
|
+
>(
|
|
9
|
+
options?: O,
|
|
10
|
+
) => {
|
|
11
|
+
return {
|
|
12
|
+
id: "stripe-client",
|
|
13
|
+
$InferServerPlugin: {} as ReturnType<
|
|
14
|
+
typeof stripe<
|
|
15
|
+
O["subscription"] extends true
|
|
16
|
+
? {
|
|
17
|
+
stripeClient: any;
|
|
18
|
+
stripeWebhookSecret: string;
|
|
19
|
+
subscription: {
|
|
20
|
+
enabled: true;
|
|
21
|
+
plans: [];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
: {
|
|
25
|
+
stripeClient: any;
|
|
26
|
+
stripeWebhookSecret: string;
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
>,
|
|
30
|
+
pathMethods: {
|
|
31
|
+
"/subscription/restore": "POST",
|
|
32
|
+
"/subscription/billing-portal": "POST",
|
|
33
|
+
},
|
|
34
|
+
} satisfies FaireAuthClientPlugin;
|
|
35
|
+
};
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { Context } from "@faire-auth/core/types";
|
|
2
|
+
import { logger, type ContextVars } from "faire-auth";
|
|
3
|
+
import type Stripe from "stripe";
|
|
4
|
+
import type { InputSubscription, StripeOptions, Subscription } from "./types";
|
|
5
|
+
import { getPlanByPriceInfo } from "./utils";
|
|
6
|
+
|
|
7
|
+
export async function onCheckoutSessionCompleted(
|
|
8
|
+
ctx: Context<ContextVars>,
|
|
9
|
+
options: StripeOptions,
|
|
10
|
+
event: Stripe.Event,
|
|
11
|
+
) {
|
|
12
|
+
try {
|
|
13
|
+
const client = options.stripeClient;
|
|
14
|
+
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
|
15
|
+
if (checkoutSession.mode === "setup" || !options.subscription?.enabled) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const subscription = await client.subscriptions.retrieve(
|
|
19
|
+
checkoutSession.subscription as string,
|
|
20
|
+
);
|
|
21
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
22
|
+
const priceLookupKey = subscription.items.data[0]?.price.lookup_key || null;
|
|
23
|
+
const plan = await getPlanByPriceInfo(
|
|
24
|
+
options,
|
|
25
|
+
priceId as string,
|
|
26
|
+
priceLookupKey,
|
|
27
|
+
);
|
|
28
|
+
if (plan) {
|
|
29
|
+
const referenceId =
|
|
30
|
+
checkoutSession?.client_reference_id ||
|
|
31
|
+
checkoutSession?.metadata?.referenceId;
|
|
32
|
+
const subscriptionId = checkoutSession?.metadata?.subscriptionId;
|
|
33
|
+
const seats = subscription.items.data[0]!.quantity;
|
|
34
|
+
if (referenceId && subscriptionId) {
|
|
35
|
+
const trial =
|
|
36
|
+
subscription.trial_start && subscription.trial_end
|
|
37
|
+
? {
|
|
38
|
+
trialStart: new Date(subscription.trial_start * 1000),
|
|
39
|
+
trialEnd: new Date(subscription.trial_end * 1000),
|
|
40
|
+
}
|
|
41
|
+
: {};
|
|
42
|
+
|
|
43
|
+
let dbSubscription = await ctx
|
|
44
|
+
.get("context")
|
|
45
|
+
.adapter.update<InputSubscription>({
|
|
46
|
+
model: "subscription",
|
|
47
|
+
update: {
|
|
48
|
+
plan: plan.name.toLowerCase(),
|
|
49
|
+
status: subscription.status,
|
|
50
|
+
updatedAt: new Date(),
|
|
51
|
+
periodStart: new Date(
|
|
52
|
+
subscription.items.data[0]!.current_period_start * 1000,
|
|
53
|
+
),
|
|
54
|
+
periodEnd: new Date(
|
|
55
|
+
subscription.items.data[0]!.current_period_end * 1000,
|
|
56
|
+
),
|
|
57
|
+
stripeSubscriptionId: checkoutSession.subscription as string,
|
|
58
|
+
seats,
|
|
59
|
+
...trial,
|
|
60
|
+
},
|
|
61
|
+
where: [
|
|
62
|
+
{
|
|
63
|
+
field: "id",
|
|
64
|
+
value: subscriptionId,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (trial.trialStart && plan.freeTrial?.onTrialStart) {
|
|
70
|
+
await plan.freeTrial.onTrialStart(dbSubscription as Subscription);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!dbSubscription) {
|
|
74
|
+
dbSubscription = await ctx
|
|
75
|
+
.get("context")
|
|
76
|
+
.adapter.findOne<Subscription>({
|
|
77
|
+
model: "subscription",
|
|
78
|
+
where: [
|
|
79
|
+
{
|
|
80
|
+
field: "id",
|
|
81
|
+
value: subscriptionId,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
await options.subscription?.onSubscriptionComplete?.(
|
|
87
|
+
{
|
|
88
|
+
event,
|
|
89
|
+
subscription: dbSubscription as Subscription,
|
|
90
|
+
stripeSubscription: subscription,
|
|
91
|
+
plan,
|
|
92
|
+
},
|
|
93
|
+
ctx,
|
|
94
|
+
);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch (e: any) {
|
|
99
|
+
logger.error(`Stripe webhook failed. Error: ${e.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function onSubscriptionUpdated(
|
|
104
|
+
ctx: Context<ContextVars>,
|
|
105
|
+
options: StripeOptions,
|
|
106
|
+
event: Stripe.Event,
|
|
107
|
+
) {
|
|
108
|
+
try {
|
|
109
|
+
if (!options.subscription?.enabled) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const subscriptionUpdated = event.data.object as Stripe.Subscription;
|
|
113
|
+
const priceId = subscriptionUpdated.items.data[0]!.price.id;
|
|
114
|
+
const priceLookupKey =
|
|
115
|
+
subscriptionUpdated.items.data[0]!.price.lookup_key || null;
|
|
116
|
+
const plan = await getPlanByPriceInfo(options, priceId, priceLookupKey);
|
|
117
|
+
|
|
118
|
+
const subscriptionId = subscriptionUpdated.metadata?.subscriptionId;
|
|
119
|
+
const customerId = subscriptionUpdated.customer?.toString();
|
|
120
|
+
let subscription = await ctx.get("context").adapter.findOne<Subscription>({
|
|
121
|
+
model: "subscription",
|
|
122
|
+
where: subscriptionId
|
|
123
|
+
? [{ field: "id", value: subscriptionId }]
|
|
124
|
+
: [{ field: "stripeSubscriptionId", value: subscriptionUpdated.id }],
|
|
125
|
+
});
|
|
126
|
+
if (!subscription) {
|
|
127
|
+
const subs = await ctx.get("context").adapter.findMany<Subscription>({
|
|
128
|
+
model: "subscription",
|
|
129
|
+
where: [{ field: "stripeCustomerId", value: customerId }],
|
|
130
|
+
});
|
|
131
|
+
if (subs.length > 1) {
|
|
132
|
+
const activeSub = subs.find(
|
|
133
|
+
(sub: Subscription) =>
|
|
134
|
+
sub.status === "active" || sub.status === "trialing",
|
|
135
|
+
);
|
|
136
|
+
if (!activeSub) {
|
|
137
|
+
logger.warn(
|
|
138
|
+
`Stripe webhook error: Multiple subscriptions found for customerId: ${customerId} and no active subscription is found`,
|
|
139
|
+
);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
subscription = activeSub;
|
|
143
|
+
} else {
|
|
144
|
+
subscription = subs[0]!;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const seats = subscriptionUpdated.items.data[0]!.quantity;
|
|
149
|
+
await ctx.get("context").adapter.update({
|
|
150
|
+
model: "subscription",
|
|
151
|
+
update: {
|
|
152
|
+
...(plan
|
|
153
|
+
? {
|
|
154
|
+
plan: plan.name.toLowerCase(),
|
|
155
|
+
limits: plan.limits,
|
|
156
|
+
}
|
|
157
|
+
: {}),
|
|
158
|
+
updatedAt: new Date(),
|
|
159
|
+
status: subscriptionUpdated.status,
|
|
160
|
+
periodStart: new Date(
|
|
161
|
+
subscriptionUpdated.items.data[0]!.current_period_start * 1000,
|
|
162
|
+
),
|
|
163
|
+
periodEnd: new Date(
|
|
164
|
+
subscriptionUpdated.items.data[0]!.current_period_end * 1000,
|
|
165
|
+
),
|
|
166
|
+
cancelAtPeriodEnd: subscriptionUpdated.cancel_at_period_end,
|
|
167
|
+
seats,
|
|
168
|
+
stripeSubscriptionId: subscriptionUpdated.id,
|
|
169
|
+
},
|
|
170
|
+
where: [
|
|
171
|
+
{
|
|
172
|
+
field: "id",
|
|
173
|
+
value: subscription.id,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
const subscriptionCanceled =
|
|
178
|
+
subscriptionUpdated.status === "active" &&
|
|
179
|
+
subscriptionUpdated.cancel_at_period_end &&
|
|
180
|
+
!subscription.cancelAtPeriodEnd; //if this is true, it means the subscription was canceled before the event was triggered
|
|
181
|
+
if (subscriptionCanceled) {
|
|
182
|
+
await options.subscription.onSubscriptionCancel?.({
|
|
183
|
+
subscription,
|
|
184
|
+
...(subscriptionUpdated.cancellation_details !== undefined && {
|
|
185
|
+
cancellationDetails: subscriptionUpdated.cancellation_details,
|
|
186
|
+
}),
|
|
187
|
+
stripeSubscription: subscriptionUpdated,
|
|
188
|
+
event,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
await options.subscription.onSubscriptionUpdate?.({
|
|
192
|
+
event,
|
|
193
|
+
subscription,
|
|
194
|
+
});
|
|
195
|
+
if (plan) {
|
|
196
|
+
if (
|
|
197
|
+
subscriptionUpdated.status === "active" &&
|
|
198
|
+
subscription.status === "trialing" &&
|
|
199
|
+
plan.freeTrial?.onTrialEnd
|
|
200
|
+
) {
|
|
201
|
+
await plan.freeTrial.onTrialEnd({ subscription }, ctx);
|
|
202
|
+
}
|
|
203
|
+
if (
|
|
204
|
+
subscriptionUpdated.status === "incomplete_expired" &&
|
|
205
|
+
subscription.status === "trialing" &&
|
|
206
|
+
plan.freeTrial?.onTrialExpired
|
|
207
|
+
) {
|
|
208
|
+
await plan.freeTrial.onTrialExpired(subscription, ctx);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
} catch (error: any) {
|
|
212
|
+
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function onSubscriptionDeleted(
|
|
217
|
+
ctx: Context<ContextVars>,
|
|
218
|
+
options: StripeOptions,
|
|
219
|
+
event: Stripe.Event,
|
|
220
|
+
) {
|
|
221
|
+
if (!options.subscription?.enabled) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const subscriptionDeleted = event.data.object as Stripe.Subscription;
|
|
226
|
+
const subscriptionId = subscriptionDeleted.id;
|
|
227
|
+
const subscription = await ctx
|
|
228
|
+
.get("context")
|
|
229
|
+
.adapter.findOne<Subscription>({
|
|
230
|
+
model: "subscription",
|
|
231
|
+
where: [
|
|
232
|
+
{
|
|
233
|
+
field: "stripeSubscriptionId",
|
|
234
|
+
value: subscriptionId,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
});
|
|
238
|
+
if (subscription) {
|
|
239
|
+
await ctx.get("context").adapter.update({
|
|
240
|
+
model: "subscription",
|
|
241
|
+
where: [
|
|
242
|
+
{
|
|
243
|
+
field: "id",
|
|
244
|
+
value: subscription.id,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
update: {
|
|
248
|
+
status: "canceled",
|
|
249
|
+
updatedAt: new Date(),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
await options.subscription.onSubscriptionDeleted?.({
|
|
253
|
+
event,
|
|
254
|
+
stripeSubscription: subscriptionDeleted,
|
|
255
|
+
subscription,
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
logger.warn(
|
|
259
|
+
`Stripe webhook error: Subscription not found for subscriptionId: ${subscriptionId}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
} catch (error: any) {
|
|
263
|
+
logger.error(`Stripe webhook failed. Error: ${error}`);
|
|
264
|
+
}
|
|
265
|
+
}
|