paykitjs 0.1.0-alpha.2

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_virtual/_rolldown/runtime.js +13 -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/check.js +92 -0
  8. package/dist/cli/commands/init.js +264 -0
  9. package/dist/cli/commands/push.js +73 -0
  10. package/dist/cli/commands/telemetry.js +16 -0
  11. package/dist/cli/index.d.ts +1 -0
  12. package/dist/cli/index.js +21 -0
  13. package/dist/cli/templates/index.js +64 -0
  14. package/dist/cli/utils/detect.js +67 -0
  15. package/dist/cli/utils/format.js +58 -0
  16. package/dist/cli/utils/get-config.js +117 -0
  17. package/dist/cli/utils/telemetry.js +103 -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 +52 -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/webhook/webhook.api.js +29 -0
  69. package/dist/webhook/webhook.service.js +143 -0
  70. package/package.json +72 -0
@@ -0,0 +1,58 @@
1
+ import StripeSdk from "stripe";
2
+ import picocolors from "picocolors";
3
+ //#region src/cli/utils/format.ts
4
+ async function getStripeAccountInfo(secretKey) {
5
+ const mode = stripeMode(secretKey);
6
+ try {
7
+ const account = await new StripeSdk(secretKey).accounts.retrieve();
8
+ return {
9
+ displayName: account.settings?.dashboard?.display_name || account.business_profile?.name || account.id,
10
+ mode
11
+ };
12
+ } catch {
13
+ return {
14
+ displayName: "unknown",
15
+ mode
16
+ };
17
+ }
18
+ }
19
+ function maskConnectionString(url) {
20
+ try {
21
+ const parsed = new URL(url);
22
+ if (parsed.password) parsed.password = "****";
23
+ return parsed.toString().replace(/\/$/, "");
24
+ } catch {
25
+ return url;
26
+ }
27
+ }
28
+ function formatPrice(amountCents, interval) {
29
+ const dollars = amountCents / 100;
30
+ const formatted = dollars % 1 === 0 ? `$${dollars}` : `$${dollars.toFixed(2)}`;
31
+ if (!interval) return formatted;
32
+ if (interval === "month") return `${formatted}/mo`;
33
+ if (interval === "year") return `${formatted}/yr`;
34
+ return `${formatted}/${interval}`;
35
+ }
36
+ function stripeMode(secretKey) {
37
+ return secretKey.startsWith("sk_test_") || secretKey.startsWith("rk_test_") ? "test mode" : "live mode";
38
+ }
39
+ function getConnectionString(pool) {
40
+ const opts = pool.options;
41
+ if (opts?.connectionString) return maskConnectionString(opts.connectionString);
42
+ if (opts?.host) {
43
+ const user = opts.user ?? "postgres";
44
+ const port = opts.port ?? 5432;
45
+ const db = opts.database ?? "postgres";
46
+ return `postgresql://${user}@${opts.host}:${String(port)}/${db}`;
47
+ }
48
+ return "postgresql://localhost:5432/postgres";
49
+ }
50
+ function formatPlanLine(action, name, price) {
51
+ switch (action) {
52
+ case "created": return picocolors.green(` + ${name} (${price}) new`);
53
+ case "updated": return picocolors.yellow(` ~ ${name} (${price}) updated`);
54
+ case "unchanged": return picocolors.dim(` = ${name} (${price}) unchanged`);
55
+ }
56
+ }
57
+ //#endregion
58
+ export { formatPlanLine, formatPrice, getConnectionString, getStripeAccountInfo };
@@ -0,0 +1,117 @@
1
+ import { isPayKitInstance } from "../../core/create-paykit.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import dotenv from "dotenv";
5
+ import { createJiti } from "jiti";
6
+ import * as ts from "typescript";
7
+ //#region src/cli/utils/get-config.ts
8
+ const CONFIG_FILENAMES = [
9
+ "paykit.ts",
10
+ "paykit.tsx",
11
+ "paykit.config.ts",
12
+ "paykit.config.tsx",
13
+ "paykit/index.ts",
14
+ "paykit/index.tsx",
15
+ "paykit.js",
16
+ "paykit.jsx",
17
+ "paykit.config.js",
18
+ "paykit.config.jsx",
19
+ "paykit/index.js",
20
+ "paykit/index.jsx"
21
+ ];
22
+ const possiblePayKitConfigPaths = [
23
+ "",
24
+ "lib/server",
25
+ "server/paykit",
26
+ "server",
27
+ "paykit",
28
+ "lib",
29
+ "src",
30
+ "src/lib",
31
+ "src/server",
32
+ "app"
33
+ ].flatMap((dir) => CONFIG_FILENAMES.map((name) => dir ? `${dir}/${name}` : name));
34
+ function resolveReferencePath(configDir, refPath) {
35
+ const resolvedPath = path.resolve(configDir, refPath);
36
+ if (refPath.endsWith(".json")) return resolvedPath;
37
+ if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isFile()) return resolvedPath;
38
+ return path.resolve(configDir, refPath, "tsconfig.json");
39
+ }
40
+ function getPathAliasesRecursive(tsconfigPath, visited = /* @__PURE__ */ new Set()) {
41
+ if (visited.has(tsconfigPath) || !fs.existsSync(tsconfigPath)) return {};
42
+ visited.add(tsconfigPath);
43
+ const readResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
44
+ if (readResult.error) throw new Error(ts.flattenDiagnosticMessageText(readResult.error.messageText, "\n"));
45
+ const parsed = ts.parseJsonConfigFileContent(readResult.config, ts.sys, path.dirname(tsconfigPath));
46
+ const result = {};
47
+ const paths = parsed.options.paths ?? {};
48
+ const baseUrl = parsed.options.baseUrl ?? path.dirname(tsconfigPath);
49
+ for (const [alias, aliasPaths] of Object.entries(paths)) for (const aliasPath of aliasPaths) {
50
+ const finalAlias = alias.endsWith("*") ? alias.slice(0, -1) : alias;
51
+ const finalAliasPath = aliasPath.endsWith("*") ? aliasPath.slice(0, -1) : aliasPath;
52
+ result[finalAlias] = path.resolve(baseUrl, finalAliasPath);
53
+ }
54
+ const references = readResult.config.references;
55
+ if (!references) return result;
56
+ for (const reference of references) {
57
+ const referencedAliases = getPathAliasesRecursive(resolveReferencePath(path.dirname(tsconfigPath), reference.path), visited);
58
+ for (const [alias, aliasPath] of Object.entries(referencedAliases)) result[alias] ??= aliasPath;
59
+ }
60
+ return result;
61
+ }
62
+ function getPathAliases(cwd) {
63
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
64
+ if (fs.existsSync(tsconfigPath)) return getPathAliasesRecursive(tsconfigPath);
65
+ const jsconfigPath = path.join(cwd, "jsconfig.json");
66
+ if (fs.existsSync(jsconfigPath)) return getPathAliasesRecursive(jsconfigPath);
67
+ return {};
68
+ }
69
+ function loadDotEnv(cwd) {
70
+ dotenv.config({
71
+ path: path.join(cwd, ".env"),
72
+ quiet: true
73
+ });
74
+ dotenv.config({
75
+ override: true,
76
+ path: path.join(cwd, ".env.local"),
77
+ quiet: true
78
+ });
79
+ }
80
+ async function loadModule(cwd, configPath) {
81
+ loadDotEnv(cwd);
82
+ return createJiti(configPath, {
83
+ alias: getPathAliases(cwd),
84
+ interopDefault: false,
85
+ jsx: true,
86
+ moduleCache: false
87
+ }).import(configPath);
88
+ }
89
+ function getPayKit(moduleValue) {
90
+ if (!moduleValue || typeof moduleValue !== "object") return null;
91
+ const moduleObject = moduleValue;
92
+ return [moduleObject.paykit, moduleObject.default].find((value) => isPayKitInstance(value) || isPayKitLike(value)) ?? null;
93
+ }
94
+ function isPayKitLike(value) {
95
+ if (!value || typeof value !== "object") return false;
96
+ const paykit = value;
97
+ return typeof paykit.handler === "function" && typeof paykit.subscribe === "function" && typeof paykit.handleWebhook === "function" && "options" in paykit;
98
+ }
99
+ async function getPayKitConfig({ cwd, configPath }) {
100
+ if (configPath) return loadConfiguredPayKit(cwd, path.isAbsolute(configPath) ? configPath : path.resolve(cwd, configPath));
101
+ for (const possiblePath of possiblePayKitConfigPaths) {
102
+ const resolvedPath = path.join(cwd, possiblePath);
103
+ if (!fs.existsSync(resolvedPath)) continue;
104
+ return loadConfiguredPayKit(cwd, resolvedPath);
105
+ }
106
+ throw new Error("No PayKit configuration file found. Add a `paykit.ts` file to your project or pass the path with `--config`.");
107
+ }
108
+ async function loadConfiguredPayKit(cwd, resolvedPath) {
109
+ const paykit = getPayKit(await loadModule(cwd, resolvedPath));
110
+ if (!paykit) throw new Error(`Couldn't read your PayKit instance in ${resolvedPath}. Export your PayKit instance as \`paykit\` or default export the result of \`createPayKit(...)\`.`);
111
+ return {
112
+ path: resolvedPath,
113
+ options: paykit.options
114
+ };
115
+ }
116
+ //#endregion
117
+ export { getPayKitConfig };
@@ -0,0 +1,103 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { PostHog } from "posthog-node";
6
+ //#region src/cli/utils/telemetry.ts
7
+ const POSTHOG_KEY = "phc_y3p2bSweenGX17D90noG8YQUyqVSgHqGAY88KvjEKaJ";
8
+ const POSTHOG_HOST = "https://us.i.posthog.com";
9
+ const CONFIG_DIR = path.join(os.homedir(), ".paykit");
10
+ const TELEMETRY_FILE = path.join(CONFIG_DIR, "telemetry.json");
11
+ const FLUSH_TIMEOUT_MS = 1e3;
12
+ function readConfig() {
13
+ try {
14
+ const raw = fs.readFileSync(TELEMETRY_FILE, "utf-8");
15
+ return JSON.parse(raw);
16
+ } catch {
17
+ return {
18
+ enabled: true,
19
+ notified: false
20
+ };
21
+ }
22
+ }
23
+ function writeConfig(config) {
24
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
25
+ fs.writeFileSync(TELEMETRY_FILE, JSON.stringify(config, null, 2));
26
+ }
27
+ function isDisabledByEnv() {
28
+ return process.env.PAYKIT_TELEMETRY_DISABLED === "1" || process.env.DO_NOT_TRACK === "1";
29
+ }
30
+ function getAnonymousId() {
31
+ const raw = `${os.hostname()}:${os.userInfo().username}`;
32
+ return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
33
+ }
34
+ function getContext() {
35
+ return {
36
+ os: process.platform,
37
+ arch: process.arch,
38
+ nodeVersion: process.version
39
+ };
40
+ }
41
+ let client = null;
42
+ let anonymousId = null;
43
+ function isEnabled() {
44
+ if (isDisabledByEnv()) return false;
45
+ return readConfig().enabled;
46
+ }
47
+ function ensureClient() {
48
+ if (!isEnabled()) return null;
49
+ if (!client) {
50
+ client = new PostHog(POSTHOG_KEY, {
51
+ host: POSTHOG_HOST,
52
+ flushAt: 1,
53
+ flushInterval: 0
54
+ });
55
+ anonymousId = getAnonymousId();
56
+ }
57
+ return client;
58
+ }
59
+ function showFirstRunNotice() {
60
+ const config = readConfig();
61
+ if (config.notified) return;
62
+ console.log("\n PayKit collects anonymous usage data to improve the CLI.\n Run `paykitjs telemetry disable` to opt out.\n");
63
+ writeConfig({
64
+ ...config,
65
+ notified: true
66
+ });
67
+ }
68
+ function capture(event, properties) {
69
+ const posthog = ensureClient();
70
+ if (!posthog || !anonymousId) return;
71
+ showFirstRunNotice();
72
+ posthog.capture({
73
+ distinctId: anonymousId,
74
+ event,
75
+ properties: {
76
+ ...getContext(),
77
+ ...properties
78
+ }
79
+ });
80
+ }
81
+ function captureError(command, error) {
82
+ capture("cli_error", {
83
+ command,
84
+ error: error instanceof Error ? error.message : String(error),
85
+ stack: error instanceof Error ? error.stack : void 0
86
+ });
87
+ }
88
+ async function flush() {
89
+ if (!client) return;
90
+ try {
91
+ await Promise.race([client.shutdown(), new Promise((resolve) => setTimeout(resolve, FLUSH_TIMEOUT_MS))]);
92
+ } catch {}
93
+ client = null;
94
+ }
95
+ function setEnabled(enabled) {
96
+ writeConfig({
97
+ ...readConfig(),
98
+ enabled,
99
+ notified: true
100
+ });
101
+ }
102
+ //#endregion
103
+ export { capture, captureError, flush, setEnabled };
@@ -0,0 +1,25 @@
1
+ import { PayKitClientApiCarrier } from "../types/instance.js";
2
+ import { clientMethods } from "../api/methods.js";
3
+
4
+ //#region src/client/index.d.ts
5
+ type RequiresIdentify = {
6
+ options: {
7
+ identify: (...args: never[]) => unknown;
8
+ };
9
+ };
10
+ interface PayKitClientOptions {
11
+ baseURL?: string;
12
+ }
13
+ declare function createPayKitClient<Instance extends RequiresIdentify>(options?: PayKitClientOptions): InferClientAPI<Instance>;
14
+ type CamelCase<S extends string> = S extends `${infer P1}-${infer P2}${infer P3}` ? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}` : Lowercase<S>;
15
+ type PathToMethod<Path extends string, Fn> = Path extends `/${infer Segment}/${infer Rest}` ? { [K in CamelCase<Segment>]: PathToMethod<`/${Rest}`, Fn> } : Path extends `/${infer Segment}` ? { [K in CamelCase<Segment>]: Fn } : never;
16
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
17
+ type InferBody<E> = E extends ((input: infer TInput) => Promise<unknown>) ? TInput : never;
18
+ type InferReturn<E> = E extends ((input: infer _TInput) => Promise<infer TResult>) ? TResult : never;
19
+ type InferClientAPI<Instance> = Instance extends PayKitClientApiCarrier<infer API> ? UnionToIntersection<{ [K in keyof API]: API[K] extends {
20
+ path: infer P;
21
+ } ? P extends string ? PathToMethod<P, (body: InferBody<API[K]>) => Promise<InferReturn<API[K]>>> : never : never }[keyof API]> : typeof clientMethods extends infer API ? UnionToIntersection<{ [K in keyof API]: API[K] extends {
22
+ path: infer P;
23
+ } ? P extends string ? PathToMethod<P, (body: InferBody<API[K]>) => Promise<InferReturn<API[K]>>> : never : never }[keyof API]> : never;
24
+ //#endregion
25
+ export { PayKitClientOptions, createPayKitClient };
@@ -0,0 +1,27 @@
1
+ import { createFetch } from "@better-fetch/fetch";
2
+ //#region src/client/index.ts
3
+ function createPayKitClient(options) {
4
+ const $fetch = createFetch({
5
+ baseURL: options?.baseURL ?? "/paykit/api",
6
+ throw: true,
7
+ ...typeof globalThis.Request !== "undefined" && "credentials" in Request.prototype ? { credentials: "include" } : {}
8
+ });
9
+ function createProxy(path = []) {
10
+ return new Proxy(function() {}, {
11
+ get(_, prop) {
12
+ if (typeof prop !== "string") return void 0;
13
+ if (prop === "then" || prop === "catch" || prop === "finally") return void 0;
14
+ return createProxy([...path, prop]);
15
+ },
16
+ apply: async (_, __, args) => {
17
+ return $fetch("/" + path.map((s) => s.replace(/[A-Z]/g, (l) => `-${l.toLowerCase()}`)).join("/"), {
18
+ method: "POST",
19
+ body: args[0] ?? {}
20
+ });
21
+ }
22
+ });
23
+ }
24
+ return createProxy();
25
+ }
26
+ //#endregion
27
+ export { createPayKitClient };
@@ -0,0 +1,17 @@
1
+ import { StripeProviderConfig, StripeRuntime } from "../providers/provider.js";
2
+ import { PayKitDatabase } from "../database/index.js";
3
+ import { NormalizedSchema } from "../types/schema.js";
4
+ import { PayKitOptions } from "../types/options.js";
5
+ import { PayKitInternalLogger } from "./logger.js";
6
+
7
+ //#region src/core/context.d.ts
8
+ interface PayKitContext {
9
+ options: PayKitOptions;
10
+ database: PayKitDatabase;
11
+ provider: StripeProviderConfig;
12
+ stripe: StripeRuntime;
13
+ plans: NormalizedSchema;
14
+ logger: PayKitInternalLogger;
15
+ }
16
+ //#endregion
17
+ export { PayKitContext };
@@ -0,0 +1,23 @@
1
+ import { PAYKIT_ERROR_CODES, PayKitError } from "./errors.js";
2
+ import { createPayKitLogger } from "./logger.js";
3
+ import { createDatabase } from "../database/index.js";
4
+ import { createStripeRuntime } from "../providers/stripe.js";
5
+ import { normalizeSchema } from "../types/schema.js";
6
+ import { Pool } from "pg";
7
+ //#region src/core/context.ts
8
+ async function createContext(options) {
9
+ if (!options.provider) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_REQUIRED);
10
+ if (options.basePath && !options.basePath.startsWith("/")) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.BASEPATH_INVALID, `basePath must start with "/", received "${options.basePath}"`);
11
+ const database = await createDatabase(typeof options.database === "string" ? new Pool({ connectionString: options.database }) : options.database);
12
+ const stripe = options.provider.runtime ?? createStripeRuntime(options.provider);
13
+ return {
14
+ options,
15
+ database,
16
+ provider: options.provider,
17
+ stripe,
18
+ plans: normalizeSchema(options.plans),
19
+ logger: createPayKitLogger(options.logging)
20
+ };
21
+ }
22
+ //#endregion
23
+ export { createContext };
@@ -0,0 +1,7 @@
1
+ import { PayKitOptions } from "../types/options.js";
2
+ import { PayKitInstance } from "../types/instance.js";
3
+
4
+ //#region src/core/create-paykit.d.ts
5
+ declare function createPayKit<const TOptions extends PayKitOptions>(options: TOptions): PayKitInstance<TOptions>;
6
+ //#endregion
7
+ export { createPayKit };
@@ -0,0 +1,52 @@
1
+ import { createPayKitRouter, getApi } from "../api/methods.js";
2
+ import { dryRunSyncProducts } from "../product/product-sync.service.js";
3
+ import { createContext } from "./context.js";
4
+ //#region src/core/create-paykit.ts
5
+ const payKitInstanceSymbol = Symbol.for("paykit.instance");
6
+ function isPayKitInstance(value) {
7
+ return value !== null && typeof value === "object" && value[payKitInstanceSymbol] === true;
8
+ }
9
+ async function initContext(options) {
10
+ const ctx = await createContext(options);
11
+ if (process.env.NODE_ENV !== "production") try {
12
+ const outOfSync = (await dryRunSyncProducts(ctx)).filter((r) => r.action !== "unchanged");
13
+ if (outOfSync.length > 0) ctx.logger.error(`${outOfSync.length} plan(s) out of sync: ${outOfSync.map((r) => r.id).join(", ")}. Run \`paykitjs push\` to update.`);
14
+ } catch {
15
+ ctx.logger.debug("Skipped plan sync check (database may not be initialized yet)");
16
+ }
17
+ return ctx;
18
+ }
19
+ function createPayKit(options) {
20
+ let contextPromise;
21
+ const getContext = () => {
22
+ contextPromise ??= initContext(options);
23
+ return contextPromise;
24
+ };
25
+ const paykit = {
26
+ options,
27
+ async handler(request) {
28
+ const ctx = await getContext();
29
+ const basePath = options.basePath ?? "/paykit";
30
+ const url = new URL(request.url);
31
+ if (request.method === "GET" && (url.pathname === basePath || url.pathname.startsWith(`${basePath}/`)) && !url.pathname.startsWith(`${basePath}/api`)) {
32
+ url.pathname = `${basePath}/api/dash`;
33
+ request = new Request(url, request);
34
+ }
35
+ return createPayKitRouter(ctx, options).handler(request);
36
+ },
37
+ ...getApi(getContext(), options),
38
+ get $context() {
39
+ return getContext();
40
+ },
41
+ $infer: void 0
42
+ };
43
+ Object.defineProperty(paykit, payKitInstanceSymbol, {
44
+ configurable: false,
45
+ enumerable: false,
46
+ value: true,
47
+ writable: false
48
+ });
49
+ return paykit;
50
+ }
51
+ //#endregion
52
+ export { createPayKit, isPayKitInstance };
@@ -0,0 +1,12 @@
1
+ //#region src/core/error-codes.d.ts
2
+ type UpperLetter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z";
3
+ type IsValidUpperSnakeCase<S extends string> = S extends `${infer F}${infer R}` ? F extends UpperLetter | "_" ? IsValidUpperSnakeCase<R> : false : true;
4
+ type InvalidKeyError<K extends string> = `Invalid error code key: "${K}" — must only contain uppercase letters (A-Z) and underscores (_)`;
5
+ type ValidateErrorCodes<T> = { [K in keyof T]: K extends string ? IsValidUpperSnakeCase<K> extends false ? InvalidKeyError<K> : T[K] : T[K] };
6
+ type RawError<K extends string = string> = {
7
+ readonly code: K;
8
+ message: string;
9
+ };
10
+ declare function defineErrorCodes<const T extends Record<string, string>, R extends { [K in keyof T & string]: RawError<K> }>(codes: ValidateErrorCodes<T>): R;
11
+ //#endregion
12
+ export { RawError, defineErrorCodes };
@@ -0,0 +1,10 @@
1
+ //#region src/core/error-codes.ts
2
+ function defineErrorCodes(codes) {
3
+ return Object.fromEntries(Object.entries(codes).map(([key, value]) => [key, {
4
+ code: key,
5
+ message: value,
6
+ toString: () => key
7
+ }]));
8
+ }
9
+ //#endregion
10
+ export { defineErrorCodes };
@@ -0,0 +1,41 @@
1
+ import { RawError } from "./error-codes.js";
2
+ import { APIError } from "better-call/error";
3
+
4
+ //#region src/core/errors.d.ts
5
+ declare const PAYKIT_ERROR_CODES: {
6
+ CUSTOMER_NOT_FOUND: RawError<"CUSTOMER_NOT_FOUND">;
7
+ CUSTOMER_CREATE_FAILED: RawError<"CUSTOMER_CREATE_FAILED">;
8
+ CUSTOMER_UPDATE_FAILED: RawError<"CUSTOMER_UPDATE_FAILED">;
9
+ PLAN_NOT_FOUND: RawError<"PLAN_NOT_FOUND">;
10
+ PLAN_NOT_SYNCED: RawError<"PLAN_NOT_SYNCED">;
11
+ PLAN_SYNC_FAILED: RawError<"PLAN_SYNC_FAILED">;
12
+ SUBSCRIPTION_CREATE_FAILED: RawError<"SUBSCRIPTION_CREATE_FAILED">;
13
+ SUBSCRIPTION_NOT_FOUND: RawError<"SUBSCRIPTION_NOT_FOUND">;
14
+ INVOICE_UPSERT_FAILED: RawError<"INVOICE_UPSERT_FAILED">;
15
+ FEATURE_UPSERT_FAILED: RawError<"FEATURE_UPSERT_FAILED">;
16
+ PROVIDER_REQUIRED: RawError<"PROVIDER_REQUIRED">;
17
+ PROVIDER_CUSTOMER_NOT_FOUND: RawError<"PROVIDER_CUSTOMER_NOT_FOUND">;
18
+ PROVIDER_SESSION_INVALID: RawError<"PROVIDER_SESSION_INVALID">;
19
+ PROVIDER_SIGNATURE_MISSING: RawError<"PROVIDER_SIGNATURE_MISSING">;
20
+ PROVIDER_SUBSCRIPTION_MISSING_ITEMS: RawError<"PROVIDER_SUBSCRIPTION_MISSING_ITEMS">;
21
+ PROVIDER_SUBSCRIPTION_MISSING_PERIOD: RawError<"PROVIDER_SUBSCRIPTION_MISSING_PERIOD">;
22
+ PROVIDER_PRICE_REQUIRED: RawError<"PROVIDER_PRICE_REQUIRED">;
23
+ PROVIDER_TEST_KEY_REQUIRED: RawError<"PROVIDER_TEST_KEY_REQUIRED">;
24
+ PROVIDER_WEBHOOK_INVALID: RawError<"PROVIDER_WEBHOOK_INVALID">;
25
+ IDENTIFY_REQUIRED: RawError<"IDENTIFY_REQUIRED">;
26
+ CUSTOMER_ID_MISMATCH: RawError<"CUSTOMER_ID_MISMATCH">;
27
+ CUSTOMER_ID_REQUIRED: RawError<"CUSTOMER_ID_REQUIRED">;
28
+ SUCCESS_URL_REQUIRED: RawError<"SUCCESS_URL_REQUIRED">;
29
+ BASEPATH_INVALID: RawError<"BASEPATH_INVALID">;
30
+ TESTING_NOT_ENABLED: RawError<"TESTING_NOT_ENABLED">;
31
+ TEST_CLOCK_NOT_FOUND: RawError<"TEST_CLOCK_NOT_FOUND">;
32
+ };
33
+ type PayKitErrorCode = keyof typeof PAYKIT_ERROR_CODES;
34
+ type APIErrorStatus = ConstructorParameters<typeof APIError>[0];
35
+ declare class PayKitError extends APIError {
36
+ code: string;
37
+ constructor(status: APIErrorStatus, error: RawError, message?: string);
38
+ static from(status: APIErrorStatus, error: RawError, message?: string): PayKitError;
39
+ }
40
+ //#endregion
41
+ export { PAYKIT_ERROR_CODES, PayKitError, PayKitErrorCode };
@@ -0,0 +1,47 @@
1
+ import { defineErrorCodes } from "./error-codes.js";
2
+ import { APIError } from "better-call/error";
3
+ //#region src/core/errors.ts
4
+ const PAYKIT_ERROR_CODES = defineErrorCodes({
5
+ CUSTOMER_NOT_FOUND: "Customer not found",
6
+ CUSTOMER_CREATE_FAILED: "Failed to create customer",
7
+ CUSTOMER_UPDATE_FAILED: "Failed to update customer",
8
+ PLAN_NOT_FOUND: "Plan not found. Run: paykitjs push",
9
+ PLAN_NOT_SYNCED: "Plan is not synced with provider. Run: paykitjs push",
10
+ PLAN_SYNC_FAILED: "Failed to sync plan",
11
+ SUBSCRIPTION_CREATE_FAILED: "Failed to create subscription",
12
+ SUBSCRIPTION_NOT_FOUND: "Subscription not found",
13
+ INVOICE_UPSERT_FAILED: "Failed to upsert invoice",
14
+ FEATURE_UPSERT_FAILED: "Failed to upsert feature",
15
+ PROVIDER_REQUIRED: "A provider is required",
16
+ PROVIDER_CUSTOMER_NOT_FOUND: "Customer not found in provider",
17
+ PROVIDER_SESSION_INVALID: "Provider session did not include a URL",
18
+ PROVIDER_SIGNATURE_MISSING: "Missing provider webhook signature",
19
+ PROVIDER_SUBSCRIPTION_MISSING_ITEMS: "Provider subscription did not include any items",
20
+ PROVIDER_SUBSCRIPTION_MISSING_PERIOD: "Provider subscription did not include period end",
21
+ PROVIDER_PRICE_REQUIRED: "A provider price ID is required",
22
+ PROVIDER_TEST_KEY_REQUIRED: "Testing mode requires a Stripe test secret key",
23
+ PROVIDER_WEBHOOK_INVALID: "Provider webhook payload is invalid",
24
+ IDENTIFY_REQUIRED: "identify must be configured to use HTTP API routes",
25
+ CUSTOMER_ID_MISMATCH: "customerId does not match authenticated user",
26
+ CUSTOMER_ID_REQUIRED: "No customerId provided and no identify configured",
27
+ SUCCESS_URL_REQUIRED: "A successUrl is required when subscribe is called without a request context",
28
+ BASEPATH_INVALID: "basePath must start with a leading slash",
29
+ TESTING_NOT_ENABLED: "Testing mode is not enabled",
30
+ TEST_CLOCK_NOT_FOUND: "Customer does not have a test clock"
31
+ });
32
+ var PayKitError = class PayKitError extends APIError {
33
+ code;
34
+ constructor(status, error, message) {
35
+ super(status, {
36
+ message: message ?? error.message,
37
+ code: error.code
38
+ });
39
+ this.code = error.code;
40
+ this.name = "PayKitError";
41
+ }
42
+ static from(status, error, message) {
43
+ return new PayKitError(status, error, message);
44
+ }
45
+ };
46
+ //#endregion
47
+ export { PAYKIT_ERROR_CODES, PayKitError };
@@ -0,0 +1,11 @@
1
+ import pino from "pino";
2
+ import pretty from "pino-pretty";
3
+
4
+ //#region src/core/logger.d.ts
5
+ interface PayKitInternalLogger extends pino.Logger {
6
+ trace: pino.Logger["trace"] & {
7
+ run: <T>(prefix: string, fn: () => T | Promise<T>) => T | Promise<T>;
8
+ };
9
+ }
10
+ //#endregion
11
+ export { PayKitInternalLogger };
@@ -0,0 +1,51 @@
1
+ import { generateId } from "./utils.js";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import pino from "pino";
4
+ import pretty from "pino-pretty";
5
+ //#region src/core/logger.ts
6
+ const storage = new AsyncLocalStorage();
7
+ const PRETTY_LOG_IGNORE_FIELDS = "pid,hostname";
8
+ const PRETTY_LOG_TIMESTAMP = "SYS:yyyy-mm-dd HH:MM:ss.l";
9
+ const DEFAULT_LOG_LEVEL = "info";
10
+ function getTraceId() {
11
+ return (storage.getStore()?.bindings())?.traceId;
12
+ }
13
+ function shouldUsePrettyLogs(environment = {}) {
14
+ const { nodeEnv = process.env.NODE_ENV } = environment;
15
+ return nodeEnv !== "production";
16
+ }
17
+ function getDefaultLoggerOptions(logging) {
18
+ return {
19
+ level: logging?.level ?? DEFAULT_LOG_LEVEL,
20
+ name: "paykit",
21
+ timestamp: pino.stdTimeFunctions.isoTime
22
+ };
23
+ }
24
+ function getPrettyLoggerOptions() {
25
+ return {
26
+ colorize: true,
27
+ ignore: PRETTY_LOG_IGNORE_FIELDS,
28
+ levelFirst: true,
29
+ translateTime: PRETTY_LOG_TIMESTAMP
30
+ };
31
+ }
32
+ function createPayKitLogger(logging, environment = {}) {
33
+ const base = logging?.logger ?? (shouldUsePrettyLogs(environment) ? pino(getDefaultLoggerOptions(logging), pretty(getPrettyLoggerOptions())) : pino(getDefaultLoggerOptions(logging)));
34
+ return new Proxy(base, { get(target, prop, receiver) {
35
+ if (prop === "trace") {
36
+ const current = storage.getStore() ?? target;
37
+ const traceFn = current.trace.bind(current);
38
+ traceFn.run = (prefix, fn) => {
39
+ const child = current.child({ traceId: generateId(prefix, 12) });
40
+ return storage.run(child, fn);
41
+ };
42
+ return traceFn;
43
+ }
44
+ const current = storage.getStore() ?? target;
45
+ const value = Reflect.get(current, prop, receiver);
46
+ if (typeof value === "function") return value.bind(current);
47
+ return value;
48
+ } });
49
+ }
50
+ //#endregion
51
+ export { createPayKitLogger, getTraceId };
@@ -0,0 +1,21 @@
1
+ import { webcrypto } from "node:crypto";
2
+ //#region src/core/utils.ts
3
+ const ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
4
+ const MAX_RANDOM_BYTE = 256 - 256 % 62;
5
+ function generateRandomString(length) {
6
+ let result = "";
7
+ const bufferSize = length + 16;
8
+ while (result.length < length) {
9
+ const bytes = webcrypto.getRandomValues(new Uint8Array(bufferSize));
10
+ for (const byte of bytes) if (byte < MAX_RANDOM_BYTE) {
11
+ result += ID_ALPHABET[byte % 62];
12
+ if (result.length === length) return result;
13
+ }
14
+ }
15
+ return result;
16
+ }
17
+ function generateId(prefix, length = 24) {
18
+ return `${prefix}_${generateRandomString(length)}`;
19
+ }
20
+ //#endregion
21
+ export { generateId };