paykitjs 0.0.1-alpha.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_virtual/_rolldown/runtime.js +14 -0
  3. package/dist/api/define-route.d.ts +94 -0
  4. package/dist/api/define-route.js +153 -0
  5. package/dist/api/methods.d.ts +422 -0
  6. package/dist/api/methods.js +67 -0
  7. package/dist/cli/commands/init.js +264 -0
  8. package/dist/cli/commands/push.js +71 -0
  9. package/dist/cli/commands/status.js +84 -0
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +45 -0
  12. package/dist/cli/templates/index.js +64 -0
  13. package/dist/cli/utils/detect.js +73 -0
  14. package/dist/cli/utils/format.js +58 -0
  15. package/dist/cli/utils/get-config.js +117 -0
  16. package/dist/cli/utils/shared.js +81 -0
  17. package/dist/cli/utils/telemetry.js +63 -0
  18. package/dist/client/index.d.ts +25 -0
  19. package/dist/client/index.js +27 -0
  20. package/dist/core/context.d.ts +17 -0
  21. package/dist/core/context.js +23 -0
  22. package/dist/core/create-paykit.d.ts +7 -0
  23. package/dist/core/create-paykit.js +67 -0
  24. package/dist/core/error-codes.d.ts +12 -0
  25. package/dist/core/error-codes.js +10 -0
  26. package/dist/core/errors.d.ts +41 -0
  27. package/dist/core/errors.js +47 -0
  28. package/dist/core/logger.d.ts +11 -0
  29. package/dist/core/logger.js +51 -0
  30. package/dist/core/utils.js +21 -0
  31. package/dist/customer/customer.api.js +47 -0
  32. package/dist/customer/customer.service.js +342 -0
  33. package/dist/customer/customer.types.d.ts +31 -0
  34. package/dist/database/index.d.ts +8 -0
  35. package/dist/database/index.js +32 -0
  36. package/dist/database/migrations/0000_init.sql +157 -0
  37. package/dist/database/migrations/meta/0000_snapshot.json +1222 -0
  38. package/dist/database/migrations/meta/_journal.json +13 -0
  39. package/dist/database/schema.d.ts +1767 -0
  40. package/dist/database/schema.js +150 -0
  41. package/dist/entitlement/entitlement.api.js +33 -0
  42. package/dist/entitlement/entitlement.service.d.ts +17 -0
  43. package/dist/entitlement/entitlement.service.js +123 -0
  44. package/dist/handlers/next.d.ts +9 -0
  45. package/dist/handlers/next.js +9 -0
  46. package/dist/index.d.ts +14 -0
  47. package/dist/index.js +6 -0
  48. package/dist/invoice/invoice.service.js +54 -0
  49. package/dist/payment/payment.service.js +49 -0
  50. package/dist/payment-method/payment-method.service.js +78 -0
  51. package/dist/product/product-sync.service.js +111 -0
  52. package/dist/product/product.service.js +127 -0
  53. package/dist/providers/provider.d.ts +159 -0
  54. package/dist/providers/stripe.js +547 -0
  55. package/dist/subscription/subscription.api.js +24 -0
  56. package/dist/subscription/subscription.service.js +896 -0
  57. package/dist/subscription/subscription.types.d.ts +18 -0
  58. package/dist/subscription/subscription.types.js +11 -0
  59. package/dist/testing/testing.api.js +29 -0
  60. package/dist/testing/testing.service.js +49 -0
  61. package/dist/types/events.d.ts +181 -0
  62. package/dist/types/instance.d.ts +88 -0
  63. package/dist/types/models.d.ts +11 -0
  64. package/dist/types/options.d.ts +32 -0
  65. package/dist/types/plugin.d.ts +11 -0
  66. package/dist/types/schema.d.ts +99 -0
  67. package/dist/types/schema.js +192 -0
  68. package/dist/utilities/dependencies/check-dependencies.js +16 -0
  69. package/dist/utilities/dependencies/get-dependencies.js +68 -0
  70. package/dist/utilities/dependencies/index.js +8 -0
  71. package/dist/utilities/dependencies/paykit-package-list.js +8 -0
  72. package/dist/webhook/webhook.api.js +29 -0
  73. package/dist/webhook/webhook.service.js +143 -0
  74. package/package.json +76 -0
@@ -0,0 +1,192 @@
1
+ import * as z from "zod";
2
+ import { createHash } from "node:crypto";
3
+ //#region src/types/schema.ts
4
+ const payKitFeatureSymbol = Symbol.for("paykit.feature");
5
+ const payKitFeatureIncludeSymbol = Symbol.for("paykit.feature_include");
6
+ const payKitPlanSymbol = Symbol.for("paykit.plan");
7
+ const entityIdSchema = z.string().min(1, "Id must not be empty").max(64, "Id must be 64 characters or fewer").regex(/^[a-z0-9_-]+$/, "Id must be lowercase alphanumeric with dashes or underscores");
8
+ const planNameSchema = z.string().min(1, "Plan name must not be empty");
9
+ const planGroupSchema = z.string().min(1, "Plan group must not be empty").max(64);
10
+ const priceSchema = z.object({
11
+ amount: z.number().positive("Price amount must be positive").max(999999.99, "Price amount must not exceed $999,999.99"),
12
+ interval: z.enum(["month", "year"])
13
+ });
14
+ const meteredFeatureConfigSchema = z.object({
15
+ limit: z.number().int().positive("Feature limit must be a positive integer"),
16
+ reset: z.enum([
17
+ "day",
18
+ "week",
19
+ "month",
20
+ "year"
21
+ ])
22
+ });
23
+ function formatValidationError(entityType, id, messages) {
24
+ return /* @__PURE__ */ new Error(`Invalid ${entityType} "${id}":\n${messages.map((message) => ` - ${message}`).join("\n")}`);
25
+ }
26
+ function deriveNameFromId(id) {
27
+ return id.split(/[-_]/u).filter(Boolean).map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
28
+ }
29
+ function defineHiddenBrand(target, symbol) {
30
+ Object.defineProperty(target, symbol, {
31
+ configurable: false,
32
+ enumerable: false,
33
+ value: true,
34
+ writable: false
35
+ });
36
+ }
37
+ function isPayKitFeatureInclude(value) {
38
+ return value !== null && typeof value === "object" && value[payKitFeatureIncludeSymbol] === true;
39
+ }
40
+ function isPayKitPlan(value) {
41
+ return value !== null && typeof value === "object" && value[payKitPlanSymbol] === true;
42
+ }
43
+ function feature(definition) {
44
+ const parsedId = entityIdSchema.safeParse(definition.id);
45
+ if (!parsedId.success) throw formatValidationError("feature", typeof definition.id === "string" ? definition.id : "<unknown>", parsedId.error.issues.map((issue) => issue.message));
46
+ const featureType = definition.type === "boolean" || definition.type === "metered" ? definition.type : null;
47
+ if (!featureType) throw formatValidationError("feature", parsedId.data, ["Feature type must be boolean or metered"]);
48
+ const featureDefinition = Object.freeze({
49
+ id: parsedId.data,
50
+ type: featureType
51
+ });
52
+ const featureFactory = ((config) => {
53
+ if (featureDefinition.type === "boolean") {
54
+ if (config !== void 0) throw formatValidationError("feature include", featureDefinition.id, [`Boolean feature "${featureDefinition.id}" does not accept config`]);
55
+ const include = {
56
+ config: void 0,
57
+ feature: featureDefinition
58
+ };
59
+ defineHiddenBrand(include, payKitFeatureIncludeSymbol);
60
+ return Object.freeze(include);
61
+ }
62
+ const parsedConfig = meteredFeatureConfigSchema.safeParse(config);
63
+ if (!parsedConfig.success) throw formatValidationError("feature include", featureDefinition.id, parsedConfig.error.issues.map((issue) => issue.message));
64
+ const include = {
65
+ config: parsedConfig.data,
66
+ feature: featureDefinition
67
+ };
68
+ defineHiddenBrand(include, payKitFeatureIncludeSymbol);
69
+ return Object.freeze(include);
70
+ });
71
+ Object.defineProperties(featureFactory, {
72
+ id: {
73
+ configurable: false,
74
+ enumerable: true,
75
+ value: featureDefinition.id,
76
+ writable: false
77
+ },
78
+ type: {
79
+ configurable: false,
80
+ enumerable: true,
81
+ value: featureDefinition.type,
82
+ writable: false
83
+ }
84
+ });
85
+ defineHiddenBrand(featureFactory, payKitFeatureSymbol);
86
+ return featureFactory;
87
+ }
88
+ function plan(config) {
89
+ const result = z.object({
90
+ default: z.boolean().optional(),
91
+ group: planGroupSchema.optional(),
92
+ id: entityIdSchema,
93
+ includes: z.array(z.unknown()).optional(),
94
+ name: planNameSchema.optional(),
95
+ price: priceSchema.optional()
96
+ }).safeParse(config);
97
+ if (!result.success) throw formatValidationError("plan", typeof config?.id === "string" ? config.id : "<unknown>", result.error.issues.map((issue) => issue.message));
98
+ const parsed = result.data;
99
+ const includes = parsed.includes ?? [];
100
+ if (includes.find((include) => !isPayKitFeatureInclude(include))) throw formatValidationError("plan", parsed.id, ["Includes must contain values returned by feature(...)"]);
101
+ if (parsed.default && !parsed.group) throw formatValidationError("plan", parsed.id, ["Default plans must define a \"group\""]);
102
+ const builtPlan = {
103
+ ...parsed,
104
+ includes
105
+ };
106
+ defineHiddenBrand(builtPlan, payKitPlanSymbol);
107
+ return Object.freeze(builtPlan);
108
+ }
109
+ function computePlanHash(plan) {
110
+ const payload = JSON.stringify({
111
+ group: plan.group,
112
+ isDefault: plan.isDefault,
113
+ priceAmount: plan.priceAmount,
114
+ priceInterval: plan.priceInterval,
115
+ features: plan.includes.map((f) => ({
116
+ id: f.id,
117
+ limit: f.limit,
118
+ resetInterval: f.resetInterval,
119
+ config: f.config
120
+ }))
121
+ });
122
+ return createHash("sha256").update(payload).digest("hex").slice(0, 16);
123
+ }
124
+ function normalizeSchema(plans) {
125
+ if (!plans) return {
126
+ features: [],
127
+ plans: [],
128
+ planMap: /* @__PURE__ */ new Map()
129
+ };
130
+ const exportedPlans = Array.isArray(plans) ? plans.filter(isPayKitPlan) : Object.values(plans).filter(isPayKitPlan);
131
+ const features = /* @__PURE__ */ new Map();
132
+ const defaultPlansByGroup = /* @__PURE__ */ new Map();
133
+ const plansById = /* @__PURE__ */ new Map();
134
+ for (const exportedPlan of exportedPlans) {
135
+ if (plansById.has(exportedPlan.id)) throw new Error(`Duplicate plan id "${exportedPlan.id}" found in plans exports.`);
136
+ const group = exportedPlan.group ?? "";
137
+ const isDefault = exportedPlan.default ?? false;
138
+ if (isDefault) {
139
+ const existingDefaultPlanId = defaultPlansByGroup.get(group);
140
+ if (existingDefaultPlanId) throw new Error(`Group "${group}" has multiple default plans: "${existingDefaultPlanId}" and "${exportedPlan.id}".`);
141
+ defaultPlansByGroup.set(group, exportedPlan.id);
142
+ }
143
+ const sortedIncludes = [...exportedPlan.includes.map((include) => {
144
+ const existingFeature = features.get(include.feature.id);
145
+ if (existingFeature && existingFeature.type !== include.feature.type) throw new Error(`Feature "${include.feature.id}" is declared with conflicting types: "${existingFeature.type}" and "${include.feature.type}".`);
146
+ features.set(include.feature.id, {
147
+ id: include.feature.id,
148
+ type: include.feature.type
149
+ });
150
+ if (include.feature.type === "metered") {
151
+ const config = include.config;
152
+ if (!config) throw new Error(`Metered feature "${include.feature.id}" requires config.`);
153
+ return {
154
+ config,
155
+ id: include.feature.id,
156
+ limit: config.limit,
157
+ resetInterval: config.reset,
158
+ type: include.feature.type
159
+ };
160
+ }
161
+ return {
162
+ config: null,
163
+ id: include.feature.id,
164
+ limit: null,
165
+ resetInterval: null,
166
+ type: include.feature.type
167
+ };
168
+ })].sort((left, right) => left.id.localeCompare(right.id));
169
+ const planData = {
170
+ group,
171
+ id: exportedPlan.id,
172
+ includes: sortedIncludes,
173
+ isDefault,
174
+ name: exportedPlan.name ?? deriveNameFromId(exportedPlan.id),
175
+ priceAmount: exportedPlan.price ? Math.round(exportedPlan.price.amount * 100) : null,
176
+ priceInterval: exportedPlan.price?.interval ?? null,
177
+ trialDays: null
178
+ };
179
+ plansById.set(exportedPlan.id, {
180
+ ...planData,
181
+ hash: computePlanHash(planData)
182
+ });
183
+ }
184
+ const sortedPlans = [...plansById.values()].sort((left, right) => left.id.localeCompare(right.id));
185
+ return {
186
+ features: [...features.values()].sort((left, right) => left.id.localeCompare(right.id)),
187
+ plans: sortedPlans,
188
+ planMap: plansById
189
+ };
190
+ }
191
+ //#endregion
192
+ export { feature, normalizeSchema, plan };
@@ -0,0 +1,16 @@
1
+ import { getDependencies } from "./get-dependencies.js";
2
+ import picocolors from "picocolors";
3
+ //#region src/utilities/dependencies/check-dependencies.ts
4
+ async function checkDependencies(dependencies, targetVersionDependency) {
5
+ const { resolved } = await getDependencies(process.cwd(), dependencies);
6
+ const foundVersions = {};
7
+ for (const [pkg, { version }] of resolved) if (!(version in foundVersions)) foundVersions[version] = pkg;
8
+ if (Object.keys(foundVersions).length <= 1) return;
9
+ const targetVersion = resolved.get(targetVersionDependency)?.version;
10
+ const mismatched = targetVersion ? Object.entries(foundVersions).filter(([version]) => version !== targetVersion) : Object.entries(foundVersions);
11
+ const lines = mismatched.map(([version, pkg]) => targetVersion ? ` ${pkg}@${version} (expected ${targetVersion})` : ` ${pkg}@${version}`);
12
+ const fixPkgs = mismatched.map(([_, pkg]) => targetVersion ? `${pkg}@${targetVersion}` : pkg).join(" ");
13
+ console.warn(`${picocolors.red("[paykit]")} Mismatching dependency versions:\n${lines.join("\n")}\n Run ${picocolors.bold(`pnpm install ${fixPkgs}`)} to fix.`);
14
+ }
15
+ //#endregion
16
+ export { checkDependencies };
@@ -0,0 +1,68 @@
1
+ import { createRequire } from "node:module";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ //#region src/utilities/dependencies/get-dependencies.ts
5
+ function resolveFrom(fromDirectory, moduleId) {
6
+ try {
7
+ fromDirectory = fs.realpathSync(fromDirectory);
8
+ } catch {
9
+ fromDirectory = path.resolve(fromDirectory);
10
+ }
11
+ const fromFile = path.join(fromDirectory, "noop.js");
12
+ const Module = createRequire(import.meta.url)("module");
13
+ try {
14
+ return Module._resolveFilename(moduleId, {
15
+ id: fromFile,
16
+ filename: fromFile,
17
+ paths: Module._nodeModulePaths(fromDirectory)
18
+ });
19
+ } catch {
20
+ return;
21
+ }
22
+ }
23
+ function findPackageJson(startDir) {
24
+ const { root } = path.parse(startDir);
25
+ let dir = startDir;
26
+ while (dir !== root) {
27
+ const candidate = path.join(dir, "package.json");
28
+ try {
29
+ fs.accessSync(candidate);
30
+ return candidate;
31
+ } catch {
32
+ dir = path.dirname(dir);
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ async function getDependencies(baseDir, packages) {
38
+ const resolved = /* @__PURE__ */ new Map();
39
+ const missing = [];
40
+ await Promise.all(packages.map(async (pkg) => {
41
+ try {
42
+ const pkgEntryPath = resolveFrom(baseDir, pkg);
43
+ if (!pkgEntryPath) {
44
+ missing.push(pkg);
45
+ return;
46
+ }
47
+ const realPath = fs.realpathSync(pkgEntryPath);
48
+ const packageJsonPath = findPackageJson(path.dirname(realPath));
49
+ if (!packageJsonPath) {
50
+ missing.push(pkg);
51
+ return;
52
+ }
53
+ const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, "utf-8"));
54
+ resolved.set(pkg, {
55
+ path: packageJsonPath,
56
+ version: packageJson.version
57
+ });
58
+ } catch {
59
+ missing.push(pkg);
60
+ }
61
+ }));
62
+ return {
63
+ missing,
64
+ resolved
65
+ };
66
+ }
67
+ //#endregion
68
+ export { getDependencies };
@@ -0,0 +1,8 @@
1
+ import { checkDependencies } from "./check-dependencies.js";
2
+ import { PAYKIT_PACKAGE_LIST } from "./paykit-package-list.js";
3
+ //#region src/utilities/dependencies/index.ts
4
+ function checkPayKitDependencies() {
5
+ return checkDependencies([...PAYKIT_PACKAGE_LIST], "paykitjs");
6
+ }
7
+ //#endregion
8
+ export { checkPayKitDependencies };
@@ -0,0 +1,8 @@
1
+ //#region src/utilities/dependencies/paykit-package-list.ts
2
+ const PAYKIT_PACKAGE_LIST = [
3
+ "paykitjs",
4
+ "@paykitjs/stripe",
5
+ "@paykitjs/dash"
6
+ ];
7
+ //#endregion
8
+ export { PAYKIT_PACKAGE_LIST };
@@ -0,0 +1,29 @@
1
+ import { PAYKIT_ERROR_CODES, PayKitError } from "../core/errors.js";
2
+ import { definePayKitMethod } from "../api/define-route.js";
3
+ import { handleWebhook } from "./webhook.service.js";
4
+ //#region src/webhook/webhook.api.ts
5
+ function headersToRecord(headers) {
6
+ const result = {};
7
+ headers.forEach((value, key) => {
8
+ result[key] = value;
9
+ });
10
+ return result;
11
+ }
12
+ /** Applies an incoming provider webhook payload. */
13
+ const receiveWebhook = definePayKitMethod({ route: {
14
+ disableBody: true,
15
+ method: "POST",
16
+ path: "/webhook/:providerId",
17
+ requireHeaders: true,
18
+ requireRequest: true,
19
+ resolveInput: async (ctx) => ({
20
+ body: await ctx.request.text(),
21
+ headers: headersToRecord(ctx.headers ?? new Headers())
22
+ })
23
+ } }, async (ctx) => {
24
+ const providerId = ctx.params.providerId;
25
+ if (providerId && providerId !== ctx.paykit.provider.id) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_WEBHOOK_INVALID, "Webhook provider does not match this PayKit instance");
26
+ return handleWebhook(ctx.paykit, ctx.input);
27
+ });
28
+ //#endregion
29
+ export { receiveWebhook };
@@ -0,0 +1,143 @@
1
+ import { webhookEvent } from "../database/schema.js";
2
+ import { generateId } from "../core/utils.js";
3
+ import { applyInvoiceWebhookAction } from "../invoice/invoice.service.js";
4
+ import { applyPaymentMethodWebhookAction } from "../payment-method/payment-method.service.js";
5
+ import { applySubscriptionWebhookAction, handleSubscribeCheckoutCompleted, prepareSubscribeCheckoutCompleted } from "../subscription/subscription.service.js";
6
+ import { applyCustomerWebhookAction, emitCustomerUpdated } from "../customer/customer.service.js";
7
+ import { getTraceId } from "../core/logger.js";
8
+ import { applyPaymentWebhookAction } from "../payment/payment.service.js";
9
+ import { and, eq, sql } from "drizzle-orm";
10
+ //#region src/webhook/webhook.service.ts
11
+ async function beginWebhookEvent(ctx, input) {
12
+ try {
13
+ await ctx.database.insert(webhookEvent).values({
14
+ error: null,
15
+ id: generateId("evt"),
16
+ payload: input.payload,
17
+ processedAt: null,
18
+ providerEventId: input.providerEventId,
19
+ providerId: ctx.provider.id,
20
+ receivedAt: /* @__PURE__ */ new Date(),
21
+ status: "processing",
22
+ traceId: getTraceId(),
23
+ type: input.type
24
+ });
25
+ return true;
26
+ } catch (error) {
27
+ if ((error.code ?? error.cause?.code) !== "23505") throw error;
28
+ return (await ctx.database.update(webhookEvent).set({
29
+ error: null,
30
+ processedAt: null,
31
+ status: "processing"
32
+ }).where(and(eq(webhookEvent.providerId, ctx.provider.id), eq(webhookEvent.providerEventId, input.providerEventId), sql`(${webhookEvent.status} = 'failed' OR (${webhookEvent.status} = 'processing' AND ${webhookEvent.receivedAt} < now() - interval '5 minutes'))`)).returning({ id: webhookEvent.id })).length > 0;
33
+ }
34
+ }
35
+ async function finishWebhookEvent(ctx, input) {
36
+ await ctx.database.update(webhookEvent).set({
37
+ error: input.error ?? null,
38
+ processedAt: /* @__PURE__ */ new Date(),
39
+ status: input.status
40
+ }).where(and(eq(webhookEvent.providerId, ctx.provider.id), eq(webhookEvent.providerEventId, input.providerEventId)));
41
+ }
42
+ function getProviderEventId(event, index, parentEventId) {
43
+ const providerEventId = event.payload.providerEventId;
44
+ if (typeof providerEventId === "string" && providerEventId.length > 0) return providerEventId;
45
+ return `${parentEventId ?? "unknown"}:${event.name}:${index}`;
46
+ }
47
+ function getParentProviderEventId(events) {
48
+ for (const event of events) {
49
+ const payload = event.payload;
50
+ if (typeof payload.providerEventId === "string" && payload.providerEventId.length > 0) return payload.providerEventId;
51
+ }
52
+ return null;
53
+ }
54
+ async function applyAction(ctx, action) {
55
+ switch (action.type) {
56
+ case "customer.upsert":
57
+ case "customer.delete": return applyCustomerWebhookAction(ctx.database, action);
58
+ case "payment_method.upsert":
59
+ case "payment_method.delete": return applyPaymentMethodWebhookAction(ctx, action);
60
+ case "payment.upsert": return applyPaymentWebhookAction(ctx, action);
61
+ case "subscription.upsert":
62
+ case "subscription.delete": return applySubscriptionWebhookAction(ctx, action);
63
+ case "invoice.upsert": return applyInvoiceWebhookAction(ctx, action);
64
+ }
65
+ }
66
+ async function processWebhookEvent(ctx, event, providerEventId, startTime) {
67
+ if (!await beginWebhookEvent(ctx, {
68
+ payload: event.payload,
69
+ providerEventId,
70
+ type: event.name
71
+ })) {
72
+ ctx.logger.info({
73
+ event: event.name,
74
+ providerEventId
75
+ }, "webhook skipped (duplicate)");
76
+ return;
77
+ }
78
+ try {
79
+ const checkoutCompletion = event.name === "checkout.completed" ? await prepareSubscribeCheckoutCompleted(ctx, event) : null;
80
+ const customerIds = await ctx.database.transaction(async (tx) => {
81
+ const txCtx = {
82
+ ...ctx,
83
+ database: tx
84
+ };
85
+ const ids = /* @__PURE__ */ new Set();
86
+ if (checkoutCompletion) {
87
+ const customerId = await handleSubscribeCheckoutCompleted(txCtx, checkoutCompletion);
88
+ if (customerId) ids.add(customerId);
89
+ }
90
+ for (const action of event.actions ?? []) {
91
+ ctx.logger.info({ actionType: action.type }, "applying action");
92
+ const customerId = await applyAction(txCtx, action);
93
+ if (customerId) ids.add(customerId);
94
+ }
95
+ return ids;
96
+ });
97
+ for (const customerId of customerIds) await emitCustomerUpdated(ctx, customerId);
98
+ const duration = Date.now() - startTime;
99
+ ctx.logger.info({
100
+ event: event.name,
101
+ duration
102
+ }, "webhook processed");
103
+ await finishWebhookEvent(ctx, {
104
+ providerEventId,
105
+ status: "processed"
106
+ });
107
+ } catch (error) {
108
+ const duration = Date.now() - startTime;
109
+ const errorDetail = error instanceof Error ? error.stack ?? error.message : String(error);
110
+ ctx.logger.error({
111
+ event: event.name,
112
+ duration,
113
+ err: error
114
+ }, "webhook failed");
115
+ await finishWebhookEvent(ctx, {
116
+ error: errorDetail,
117
+ providerEventId,
118
+ status: "failed"
119
+ });
120
+ throw error;
121
+ }
122
+ }
123
+ async function handleWebhook(ctx, input) {
124
+ return ctx.logger.trace.run("wh", async () => {
125
+ const startTime = Date.now();
126
+ const events = await ctx.stripe.handleWebhook({
127
+ body: input.body,
128
+ headers: input.headers
129
+ });
130
+ const parentEventId = getParentProviderEventId(events);
131
+ for (const [index, event] of events.entries()) {
132
+ const providerEventId = getProviderEventId(event, index, parentEventId);
133
+ ctx.logger.info({
134
+ event: event.name,
135
+ providerEventId
136
+ }, "webhook received");
137
+ await processWebhookEvent(ctx, event, providerEventId, startTime);
138
+ }
139
+ return { received: true };
140
+ });
141
+ }
142
+ //#endregion
143
+ export { handleWebhook };
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "paykitjs",
3
+ "version": "0.0.1-alpha.1",
4
+ "description": "TypeScript-first payments orchestration framework for modern SaaS",
5
+ "keywords": [
6
+ "creem",
7
+ "orchestration",
8
+ "paddle",
9
+ "payments",
10
+ "paypal",
11
+ "polar",
12
+ "stripe",
13
+ "subscriptions",
14
+ "typescript"
15
+ ],
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/getpaykit/paykit.git"
20
+ },
21
+ "bin": {
22
+ "paykitjs": "./dist/cli/index.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "type": "module",
28
+ "main": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./handlers/next": {
36
+ "types": "./dist/handlers/next.d.ts",
37
+ "default": "./dist/handlers/next.js"
38
+ },
39
+ "./client": {
40
+ "types": "./dist/client/index.d.ts",
41
+ "default": "./dist/client/index.js"
42
+ },
43
+ "./database": {
44
+ "types": "./dist/database/schema.d.ts",
45
+ "default": "./dist/database/schema.js"
46
+ }
47
+ },
48
+ "dependencies": {
49
+ "@better-fetch/fetch": "^1.1.21",
50
+ "@clack/prompts": "^1.1.0",
51
+ "better-call": "^2.0.2",
52
+ "commander": "^12.1.0",
53
+ "dotenv": "^17.3.1",
54
+ "drizzle-orm": "^0.45.1",
55
+ "jiti": "^2.6.1",
56
+ "pg": "^8.20.0",
57
+ "picocolors": "^1.1.1",
58
+ "pino": "^10.3.1",
59
+ "pino-pretty": "^13.1.3",
60
+ "posthog-node": "^5.28.8",
61
+ "stripe": "^19.1.0",
62
+ "typescript": "^5.9.2",
63
+ "zod": "^4.0.0"
64
+ },
65
+ "devDependencies": {
66
+ "@types/pg": "^8.18.0",
67
+ "drizzle-kit": "^0.31.5",
68
+ "tsdown": "^0.21.1",
69
+ "vitest": "^4.0.18"
70
+ },
71
+ "scripts": {
72
+ "build": "tsdown --config tsdown.config.ts",
73
+ "typecheck": "tsc --build",
74
+ "db:generate": "drizzle-kit generate --config src/database/drizzle.config.ts"
75
+ }
76
+ }