@0x0501/mystack 0.0.0-stage → 0.1.0-bootstrap.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.
Files changed (211) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -2
  3. package/assets/auth.sqlite.schema.ts +479 -0
  4. package/assets/bun-sqlite.test.ts +46 -0
  5. package/assets/d1.test.ts +41 -0
  6. package/assets/drizzle.config.ts +2 -0
  7. package/assets/generate-auth-schema.mjs +25 -0
  8. package/assets/platform.sqlite.schema.ts +46 -0
  9. package/assets/polar-checkout.ts +10 -0
  10. package/assets/polar-route.ts +19 -0
  11. package/assets/polar.test.ts +30 -0
  12. package/assets/polar.ts +12 -0
  13. package/assets/purchase.bun-sqlite.ts +38 -0
  14. package/assets/purchase.sqlite.ts +39 -0
  15. package/assets/sqlite-migrations/0000_thin_phalanx.sql +295 -0
  16. package/assets/sqlite-migrations/meta/0000_snapshot.json +2058 -0
  17. package/assets/sqlite-migrations/meta/_journal.json +13 -0
  18. package/assets/turso.test.ts +40 -0
  19. package/package.json +44 -5
  20. package/src/adapters/database.mjs +448 -0
  21. package/src/adapters/deployment.mjs +101 -0
  22. package/src/adapters/email.mjs +75 -0
  23. package/src/adapters/payments.mjs +270 -0
  24. package/src/catalog.mjs +128 -0
  25. package/src/cli.mjs +294 -0
  26. package/src/generate.mjs +183 -0
  27. package/src/template.mjs +93 -0
  28. package/template/.env.docker +16 -0
  29. package/template/.env.example +67 -0
  30. package/template/DESIGN.md +58 -0
  31. package/template/LICENSE +21 -0
  32. package/template/biome.json +46 -0
  33. package/template/docker-compose.yml +47 -0
  34. package/template/docs/project-guide.md +61 -0
  35. package/template/drizzle/0000_create-app-role.sql +54 -0
  36. package/template/drizzle/0001_add-rls-context.sql +21 -0
  37. package/template/drizzle/0002_platform_tables.sql +320 -0
  38. package/template/drizzle/meta/0002_snapshot.json +2125 -0
  39. package/template/drizzle/meta/_journal.json +27 -0
  40. package/template/drizzle.config.ts +25 -0
  41. package/template/e2e/authenticated-ui.spec.ts +40 -0
  42. package/template/e2e/headless-design.spec.ts +66 -0
  43. package/template/e2e/navigation-loading.spec.ts +70 -0
  44. package/template/e2e/theme-responsive.spec.ts +260 -0
  45. package/template/gitignore +24 -0
  46. package/template/messages/de.json +212 -0
  47. package/template/messages/en.json +212 -0
  48. package/template/package.json +101 -0
  49. package/template/paraglide.config.ts +49 -0
  50. package/template/playwright.config.ts +58 -0
  51. package/template/project.inlang/settings.json +12 -0
  52. package/template/public/drizzle.svg +1 -0
  53. package/template/public/favicon.ico +0 -0
  54. package/template/public/logo192.png +0 -0
  55. package/template/public/logo512.png +0 -0
  56. package/template/public/manifest.json +25 -0
  57. package/template/public/robots.txt +3 -0
  58. package/template/scripts/check-client-bundle.ts +150 -0
  59. package/template/scripts/check-server-fns.ts +290 -0
  60. package/template/scripts/check-sql-raw.ts +50 -0
  61. package/template/scripts/compile-paraglide.ts +12 -0
  62. package/template/scripts/postgres/README.md +76 -0
  63. package/template/scripts/postgres/create-app-role.sh +60 -0
  64. package/template/scripts/seed-local.ts +331 -0
  65. package/template/scripts/validate-prod-env.ts +53 -0
  66. package/template/src/__tests__/README.md +9 -0
  67. package/template/src/__tests__/account-security.test.ts +664 -0
  68. package/template/src/__tests__/admin-http-surface.test.ts +311 -0
  69. package/template/src/__tests__/admin-peer-protection.test.ts +178 -0
  70. package/template/src/__tests__/auth-harness.ts +104 -0
  71. package/template/src/__tests__/auth-rate-limit.test.ts +43 -0
  72. package/template/src/__tests__/auth-redirect.test.ts +120 -0
  73. package/template/src/__tests__/body-limit.test.ts +47 -0
  74. package/template/src/__tests__/creem.test.ts +62 -0
  75. package/template/src/__tests__/demo-checkout.test.ts +141 -0
  76. package/template/src/__tests__/email-locale.test.ts +110 -0
  77. package/template/src/__tests__/global-setup.ts +80 -0
  78. package/template/src/__tests__/input-rules.test.ts +0 -0
  79. package/template/src/__tests__/message-parity.test.ts +38 -0
  80. package/template/src/__tests__/navigation-loading.test.tsx +252 -0
  81. package/template/src/__tests__/nowpayments.test.ts +106 -0
  82. package/template/src/__tests__/paraglide-url-contract.test.ts +87 -0
  83. package/template/src/__tests__/purchase-fulfillment.test.ts +204 -0
  84. package/template/src/__tests__/query-unauthorized-redirect.test.ts +95 -0
  85. package/template/src/__tests__/rls-policy.test.ts +120 -0
  86. package/template/src/__tests__/same-page-locale-href.test.ts +47 -0
  87. package/template/src/__tests__/search-params-oauth.test.ts +57 -0
  88. package/template/src/__tests__/security-headers.test.ts +12 -0
  89. package/template/src/__tests__/server-fn-error-contract.test.ts +173 -0
  90. package/template/src/__tests__/stripe.test.ts +84 -0
  91. package/template/src/__tests__/test-db.ts +32 -0
  92. package/template/src/__tests__/test-rls-db.ts +23 -0
  93. package/template/src/__tests__/user-update-guard.test.ts +109 -0
  94. package/template/src/components/LocaleSwitcher.tsx +79 -0
  95. package/template/src/components/ThemeToggle.tsx +47 -0
  96. package/template/src/components/auth/social.tsx +90 -0
  97. package/template/src/components/auth/turnstile.tsx +67 -0
  98. package/template/src/components/root/navigation-progress.tsx +32 -0
  99. package/template/src/components/root/not-found.tsx +20 -0
  100. package/template/src/components/root/pending.tsx +9 -0
  101. package/template/src/components/root/root-document.tsx +37 -0
  102. package/template/src/components/root/root-error.tsx +34 -0
  103. package/template/src/components/shells.tsx +201 -0
  104. package/template/src/components/ui/styles.ts +9 -0
  105. package/template/src/db/auth.schema.ts +455 -0
  106. package/template/src/db/helper.ts +38 -0
  107. package/template/src/db/index.ts +15 -0
  108. package/template/src/db/platform.schema.ts +122 -0
  109. package/template/src/db/schema.ts +2 -0
  110. package/template/src/env.ts +65 -0
  111. package/template/src/hooks/use-theme.ts +32 -0
  112. package/template/src/integrations/better-auth/header-user.tsx +36 -0
  113. package/template/src/integrations/better-auth/session-query.ts +11 -0
  114. package/template/src/integrations/react-email/PasswordOtpEmail.tsx +62 -0
  115. package/template/src/integrations/react-email/ResetPasswordEmail.tsx +64 -0
  116. package/template/src/integrations/react-email/VerifyEmail.tsx +72 -0
  117. package/template/src/integrations/react-email/theme.ts +75 -0
  118. package/template/src/integrations/tanstack-query/devtools.tsx +6 -0
  119. package/template/src/integrations/tanstack-query/root-provider.tsx +60 -0
  120. package/template/src/lib/auth-client.ts +27 -0
  121. package/template/src/lib/auth-paths.ts +94 -0
  122. package/template/src/lib/auth-plugins/existing-email-signup.ts +86 -0
  123. package/template/src/lib/auth-plugins/field-rules.ts +84 -0
  124. package/template/src/lib/auth-plugins/social-two-factor.ts +194 -0
  125. package/template/src/lib/auth-timing.ts +9 -0
  126. package/template/src/lib/auth.ts +279 -0
  127. package/template/src/lib/captcha.ts +11 -0
  128. package/template/src/lib/email.tsx +162 -0
  129. package/template/src/lib/github.ts +18 -0
  130. package/template/src/lib/locale-cookie.ts +12 -0
  131. package/template/src/lib/localized-pages.ts +25 -0
  132. package/template/src/lib/oauth-metadata.ts +19 -0
  133. package/template/src/lib/permissions.ts +29 -0
  134. package/template/src/lib/security-headers.ts +20 -0
  135. package/template/src/lib/site.ts +59 -0
  136. package/template/src/lib/theme.ts +42 -0
  137. package/template/src/lib/user-update-guard.ts +35 -0
  138. package/template/src/middlewares/admin.ts +46 -0
  139. package/template/src/middlewares/auth-rate-limit.ts +37 -0
  140. package/template/src/middlewares/better-auth.ts +15 -0
  141. package/template/src/middlewares/body-limit.ts +62 -0
  142. package/template/src/middlewares/database.ts +13 -0
  143. package/template/src/middlewares/protected.ts +45 -0
  144. package/template/src/routeTree.gen.ts +536 -0
  145. package/template/src/router.tsx +53 -0
  146. package/template/src/routes/[.]well-known/oauth-authorization-server/api/auth.ts +18 -0
  147. package/template/src/routes/[.]well-known/oauth-protected-resource/index.ts +32 -0
  148. package/template/src/routes/__root.tsx +175 -0
  149. package/template/src/routes/admin/audit.tsx +74 -0
  150. package/template/src/routes/admin/index.tsx +7 -0
  151. package/template/src/routes/admin/oauth-apps.tsx +164 -0
  152. package/template/src/routes/admin/route.tsx +27 -0
  153. package/template/src/routes/admin/users.tsx +215 -0
  154. package/template/src/routes/api/auth/$.ts +14 -0
  155. package/template/src/routes/api/auth/[.]well-known/openid-configuration.ts +15 -0
  156. package/template/src/routes/api/webhooks/creem.ts +69 -0
  157. package/template/src/routes/api/webhooks/nowpayments.ts +62 -0
  158. package/template/src/routes/api/webhooks/stripe.ts +74 -0
  159. package/template/src/routes/dashboard/account.tsx +430 -0
  160. package/template/src/routes/dashboard/index.tsx +128 -0
  161. package/template/src/routes/dashboard/route.tsx +17 -0
  162. package/template/src/routes/forgot-password.tsx +65 -0
  163. package/template/src/routes/index.tsx +85 -0
  164. package/template/src/routes/oauth/consent.tsx +77 -0
  165. package/template/src/routes/reset-password.tsx +78 -0
  166. package/template/src/routes/sign-in.tsx +174 -0
  167. package/template/src/routes/sign-up.tsx +135 -0
  168. package/template/src/routes/two-factor.tsx +74 -0
  169. package/template/src/server/README.md +3 -0
  170. package/template/src/server/account-security.ts +163 -0
  171. package/template/src/server/admin-audit.ts +37 -0
  172. package/template/src/server/admin-users.ts +112 -0
  173. package/template/src/server/auth.ts +44 -0
  174. package/template/src/server/demo-checkout.ts +67 -0
  175. package/template/src/server/oauth-clients.ts +83 -0
  176. package/template/src/server/payment-toggles.ts +39 -0
  177. package/template/src/server.ts +20 -0
  178. package/template/src/services/README.md +3 -0
  179. package/template/src/services/account-security.ts +893 -0
  180. package/template/src/services/admin-security.ts +164 -0
  181. package/template/src/services/admin-users.ts +181 -0
  182. package/template/src/services/auth-rate-limit.ts +32 -0
  183. package/template/src/services/creem.ts +83 -0
  184. package/template/src/services/demo-checkout.ts +100 -0
  185. package/template/src/services/nowpayments.ts +154 -0
  186. package/template/src/services/oauth-client.ts +85 -0
  187. package/template/src/services/oauth-clients.ts +15 -0
  188. package/template/src/services/payment-providers.ts +157 -0
  189. package/template/src/services/payment-toggles.ts +48 -0
  190. package/template/src/services/purchase.ts +233 -0
  191. package/template/src/services/stripe.ts +106 -0
  192. package/template/src/start.ts +35 -0
  193. package/template/src/styles/layout.css +93 -0
  194. package/template/src/styles/theme.css +84 -0
  195. package/template/src/styles/ui.css +233 -0
  196. package/template/src/styles.css +90 -0
  197. package/template/src/utils/api-error.ts +129 -0
  198. package/template/src/utils/auth-errors.ts +157 -0
  199. package/template/src/utils/auth-redirect.ts +61 -0
  200. package/template/src/utils/body-limit.ts +84 -0
  201. package/template/src/utils/cn.ts +6 -0
  202. package/template/src/utils/input-rules.ts +172 -0
  203. package/template/src/utils/locale-paths.ts +18 -0
  204. package/template/src/utils/same-page-locale-href.ts +20 -0
  205. package/template/src/utils/search-params.ts +71 -0
  206. package/template/tsconfig.json +30 -0
  207. package/template/tsr.config.json +6 -0
  208. package/template/vite.config.ts +24 -0
  209. package/template/vitest.config.ts +26 -0
  210. package/template/worker-configuration.d.ts +14660 -0
  211. package/template/wrangler.jsonc +39 -0
@@ -0,0 +1,75 @@
1
+ import { replace, text } from "../template.mjs";
2
+
3
+ export async function configureEmail({ files, selection }) {
4
+ const original = text(files, "src/lib/email.tsx");
5
+ const start = original.indexOf("type EmailBinding =");
6
+ const end = original.indexOf("/**\n * The recipient's own language");
7
+ if (start < 0 || end < 0) throw new Error("Email template anchors changed.");
8
+ files.set(
9
+ "src/lib/email.tsx",
10
+ `import { sendEmail } from "./email-transport";\n${original.slice(0, start)}${send}\n${original.slice(end)}`,
11
+ );
12
+ if (selection.email === "resend") {
13
+ replace(
14
+ files,
15
+ "scripts/validate-prod-env.ts",
16
+ '\t"EMAIL_FROM",',
17
+ '\t"EMAIL_FROM",\n\t"RESEND_API_KEY",',
18
+ );
19
+ files.set("src/lib/email-transport.ts", resend);
20
+ replace(
21
+ files,
22
+ "src/env.ts",
23
+ "\t\t\tEMAIL_FROM: z.string().min(1),",
24
+ "\t\t\tRESEND_API_KEY: z.string().optional(),\n\t\t\tEMAIL_FROM: z.string().min(1),",
25
+ );
26
+ files.set(
27
+ ".env.example",
28
+ `${text(files, ".env.example")}\n# Resend: verify your sender domain before production use.\nRESEND_API_KEY=\n`,
29
+ );
30
+ if (selection.deployment === "cloudflare") {
31
+ replace(files, "worker-configuration.d.ts", "\tEMAIL: SendEmail;\n", "");
32
+ const config = text(files, "wrangler.jsonc");
33
+ files.set(
34
+ "wrangler.jsonc",
35
+ config.replace(/\t"send_email": \[[\s\S]*?\n\t\],\n/, ""),
36
+ );
37
+ }
38
+ } else files.set("src/lib/email-transport.ts", cloudflare);
39
+ }
40
+
41
+ const send = `async function send(opts: {
42
+ to: string;
43
+ subject: string;
44
+ email: ReactElement;
45
+ devLabel: string;
46
+ devDetail: string;
47
+ }): Promise<void> {
48
+ const [html, text] = await Promise.all([render(opts.email), render(opts.email, { plainText: true })]);
49
+ const sent = await sendEmail({ to: opts.to, subject: opts.subject, html, text });
50
+ if (!sent) {
51
+ if (!import.meta.env.DEV) throw new Error("Email transport is not configured.");
52
+ console.warn(\`[email] $\{opts.devLabel} for $\{opts.to}: $\{opts.devDetail}\`);
53
+ }
54
+ }
55
+ `;
56
+ const resend = `import { env } from "@/env";
57
+ export async function sendEmail(message: { to: string; subject: string; html: string; text: string }): Promise<boolean> {
58
+ if (!env.RESEND_API_KEY) return false;
59
+ const response = await fetch("https://api.resend.com/emails", {
60
+ method: "POST",
61
+ headers: { authorization: \`Bearer $\{env.RESEND_API_KEY}\`, "content-type": "application/json" },
62
+ body: JSON.stringify({ ...message, from: \`$\{env.EMAIL_FROM_NAME} <$\{env.EMAIL_FROM}>\` }),
63
+ });
64
+ if (!response.ok) throw new Error(\`Resend email failed ($\{response.status})\`);
65
+ return true;
66
+ }
67
+ `;
68
+ const cloudflare = `import { env } from "@/env";
69
+ export async function sendEmail(message: { to: string; subject: string; html: string; text: string }): Promise<boolean> {
70
+ const { env: bindings } = await import("cloudflare:workers");
71
+ if (!bindings.EMAIL) return false;
72
+ await bindings.EMAIL.send({ ...message, from: { email: env.EMAIL_FROM, name: env.EMAIL_FROM_NAME } });
73
+ return true;
74
+ }
75
+ `;
@@ -0,0 +1,270 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { catalog, isPostgres } from "../catalog.mjs";
3
+ import { replace, text } from "../template.mjs";
4
+
5
+ const definitions = {
6
+ stripe: {
7
+ required: "env.STRIPE_SECRET_KEY",
8
+ functionName: "createStripeSession",
9
+ envKeys: ["STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET"],
10
+ },
11
+ creem: {
12
+ required: "env.CREEM_API_KEY && env.CREEM_PRODUCT_ID",
13
+ functionName: "createCreemCheckout",
14
+ envKeys: [
15
+ "CREEM_API_KEY",
16
+ "CREEM_WEBHOOK_SECRET",
17
+ "CREEM_PRODUCT_ID",
18
+ "CREEM_TEST_MODE",
19
+ ],
20
+ },
21
+ nowpayments: {
22
+ required: "env.NOW_PAYMENTS_API_KEY",
23
+ functionName: "createNowPaymentsInvoice",
24
+ envKeys: [
25
+ "NOW_PAYMENTS_API_KEY",
26
+ "NOW_PAYMENTS_IPN_KEY",
27
+ "NOW_PAYMENTS_TEST_MODE",
28
+ ],
29
+ },
30
+ polar: {
31
+ checkoutAsset: "polar-checkout.ts",
32
+ testAsset: "polar.test.ts",
33
+ webhookAsset: "polar.ts",
34
+ routeAsset: "polar-route.ts",
35
+ dependencies: { "@polar-sh/sdk": "1.0.2" },
36
+ envSchema:
37
+ "\t\t\tPOLAR_ACCESS_TOKEN: z.string().optional(),\n\t\t\tPOLAR_PRODUCT_ID: z.string().optional(),\n\t\t\tPOLAR_WEBHOOK_SECRET: z.string().optional(),\n\t\t\tPOLAR_SANDBOX: z.stringbool().default(false),",
38
+ envExample:
39
+ "POLAR_ACCESS_TOKEN=\nPOLAR_PRODUCT_ID=\nPOLAR_WEBHOOK_SECRET=\nPOLAR_SANDBOX=true\n",
40
+ required: "env.POLAR_ACCESS_TOKEN && env.POLAR_PRODUCT_ID",
41
+ functionName: "createPolarCheckout",
42
+ envKeys: [
43
+ "POLAR_ACCESS_TOKEN",
44
+ "POLAR_PRODUCT_ID",
45
+ "POLAR_WEBHOOK_SECRET",
46
+ "POLAR_SANDBOX",
47
+ ],
48
+ },
49
+ };
50
+
51
+ export async function configurePayments({ files, pkg, selection }) {
52
+ const ids = selection.payments;
53
+ const allIds = catalog.payments.map((option) => option.id);
54
+ const original = text(files, "src/services/payment-providers.ts");
55
+ let functions = "";
56
+ for (const id of ids) {
57
+ const definition = definitions[id];
58
+ if (!definition) throw new Error(`Missing payment adapter: ${id}`);
59
+ if (definition.checkoutAsset) {
60
+ functions += await readFile(
61
+ new URL(`../../assets/${definition.checkoutAsset}`, import.meta.url),
62
+ "utf8",
63
+ );
64
+ files.set(
65
+ `src/services/${id}.ts`,
66
+ await readFile(
67
+ new URL(`../../assets/${definition.webhookAsset}`, import.meta.url),
68
+ ),
69
+ );
70
+ files.set(
71
+ `src/routes/api/webhooks/${id}.ts`,
72
+ (
73
+ await readFile(
74
+ new URL(`../../assets/${definition.routeAsset}`, import.meta.url),
75
+ "utf8",
76
+ )
77
+ ).replaceAll(
78
+ "withRlsService",
79
+ isPostgres(selection.database)
80
+ ? "withRlsService"
81
+ : "withDatabaseService",
82
+ ),
83
+ );
84
+ if (definition.testAsset)
85
+ files.set(
86
+ `src/__tests__/${id}.test.ts`,
87
+ await readFile(
88
+ new URL(`../../assets/${definition.testAsset}`, import.meta.url),
89
+ ),
90
+ );
91
+ Object.assign(pkg.dependencies, definition.dependencies);
92
+ } else {
93
+ const start = original.indexOf(
94
+ `async function ${definition.functionName}(`,
95
+ );
96
+ const end = original.indexOf("\nasync function ", start + 1);
97
+ if (start < 0) throw new Error(`Payment template changed: ${id}`);
98
+ functions += `${original.slice(start, end < 0 ? undefined : end)}\n`;
99
+ }
100
+ }
101
+ files.set(
102
+ "src/lib/payment-options.ts",
103
+ `export const supportedPaymentProviders = ${JSON.stringify(allIds)} as const;
104
+ export const paymentProviders: readonly (typeof supportedPaymentProviders[number])[] = ${JSON.stringify(ids)};
105
+ `,
106
+ );
107
+ files.set(
108
+ "src/services/payment-providers.ts",
109
+ `${ids.length ? 'import { env } from "@/env";\n' : ""}import type { PurchaseProvider } from "./purchase";
110
+ export function isPaymentProviderConfigured(provider: PurchaseProvider): boolean {
111
+ switch (provider) {
112
+ ${ids.map((id) => ` case "${id}": return Boolean(${definitions[id].required});`).join("\n")}
113
+ default: return false;
114
+ }
115
+ }
116
+ export async function createHostedCheckoutSession(input: { provider: PurchaseProvider; userId: string; userEmail: string; amount: number; currency: string }): Promise<{ externalId: string; url: string }> {
117
+ switch (input.provider) {
118
+ ${ids.map((id) => ` case "${id}": return ${definitions[id].functionName}(input);`).join("\n")}
119
+ default: throw new Error("Payment provider is not enabled in this project.");
120
+ }
121
+ }
122
+ ${functions}`,
123
+ );
124
+ replace(
125
+ files,
126
+ "src/services/purchase.ts",
127
+ '"stripe" | "creem" | "nowpayments"',
128
+ allIds.map((id) => JSON.stringify(id)).join(" | "),
129
+ );
130
+ files.set(
131
+ "src/services/payment-toggles.ts",
132
+ `import { eq } from "drizzle-orm";
133
+ import type { Database } from "@/db";
134
+ import { systemConfig } from "@/db/schema";
135
+ import { paymentProviders, supportedPaymentProviders } from "@/lib/payment-options";
136
+ import type { PurchaseProvider } from "./purchase";
137
+ export type PaymentToggles = Record<PurchaseProvider, boolean>;
138
+ const KEY = "payment_toggles";
139
+ export async function getPaymentToggles(db: Database): Promise<PaymentToggles> {
140
+ const [row] = await db.select().from(systemConfig).where(eq(systemConfig.key, KEY)).limit(1);
141
+ const saved = row && typeof row.value === "object" && row.value !== null ? row.value as Partial<PaymentToggles> : {};
142
+ return Object.fromEntries(supportedPaymentProviders.map((id) => [id, paymentProviders.includes(id) && saved[id] !== false])) as PaymentToggles;
143
+ }
144
+ export async function setPaymentToggles(db: Database, toggles: PaymentToggles): Promise<void> {
145
+ await db.insert(systemConfig).values({ key: KEY, value: toggles }).onConflictDoUpdate({ target: systemConfig.key, set: { value: toggles, updatedAt: new Date() } });
146
+ }
147
+ `,
148
+ );
149
+ const extras = allIds.filter(
150
+ (id) => !["stripe", "creem", "nowpayments"].includes(id),
151
+ );
152
+ if (files.has("src/__tests__/demo-checkout.test.ts"))
153
+ files.set(
154
+ "src/__tests__/demo-checkout.test.ts",
155
+ text(files, "src/__tests__/demo-checkout.test.ts").replace(
156
+ /nowpayments: (true|false),?/g,
157
+ `nowpayments: $1, ${extras.map((id) => `${id}: false,`).join(" ")}`,
158
+ ),
159
+ );
160
+ replace(
161
+ files,
162
+ "src/server/payment-toggles.ts",
163
+ "stripe: z.boolean(),\n creem: z.boolean(),\n nowpayments: z.boolean(),",
164
+ allIds.map((id) => `${id}: z.boolean(),`).join("\n"),
165
+ );
166
+ replace(
167
+ files,
168
+ "src/routes/admin/users.tsx",
169
+ "mutationFn: (next: {\n stripe: boolean;\n creem: boolean;\n nowpayments: boolean;\n })",
170
+ "mutationFn: (next: PaymentToggles)",
171
+ );
172
+ files.set(
173
+ "src/routes/admin/users.tsx",
174
+ `import { paymentProviders } from "@/lib/payment-options";\nimport type { PaymentToggles } from "@/services/payment-toggles";\n${text(files, "src/routes/admin/users.tsx")}`,
175
+ );
176
+ replace(
177
+ files,
178
+ "src/routes/admin/users.tsx",
179
+ '(["stripe", "creem", "nowpayments"] as const).map',
180
+ "paymentProviders.map",
181
+ );
182
+ files.set(
183
+ "src/routes/dashboard/index.tsx",
184
+ `import { paymentProviders } from "@/lib/payment-options";\nimport type { PurchaseProvider } from "@/services/purchase";\n${text(files, "src/routes/dashboard/index.tsx")}`,
185
+ );
186
+ replace(
187
+ files,
188
+ "src/routes/dashboard/index.tsx",
189
+ '"stripe" | "creem" | "nowpayments"',
190
+ "PurchaseProvider",
191
+ );
192
+ replace(
193
+ files,
194
+ "src/routes/dashboard/index.tsx",
195
+ '(["stripe", "creem", "nowpayments"] as const).filter',
196
+ "paymentProviders.filter",
197
+ );
198
+ files.set(
199
+ "src/server/demo-checkout.ts",
200
+ `import { paymentProviders, supportedPaymentProviders } from "@/lib/payment-options";\nimport type { PurchaseProvider } from "@/services/purchase";\n${text(files, "src/server/demo-checkout.ts")}`,
201
+ );
202
+ replace(
203
+ files,
204
+ "src/server/demo-checkout.ts",
205
+ `return {
206
+ stripe: toggles.stripe && isPaymentProviderConfigured("stripe"),
207
+ creem: toggles.creem && isPaymentProviderConfigured("creem"),
208
+ nowpayments:
209
+ toggles.nowpayments && isPaymentProviderConfigured("nowpayments"),
210
+ };`,
211
+ `return Object.fromEntries(supportedPaymentProviders.map((id) => [id, toggles[id] && isPaymentProviderConfigured(id)])) as Record<PurchaseProvider, boolean>;`,
212
+ );
213
+ replace(
214
+ files,
215
+ "src/server/demo-checkout.ts",
216
+ 'provider: z.enum(["stripe", "creem", "nowpayments"]),',
217
+ 'provider: z.enum(supportedPaymentProviders).refine((value) => paymentProviders.includes(value), "Payment provider is not enabled."),',
218
+ );
219
+ files.set(
220
+ "src/services/demo-checkout.ts",
221
+ `import { supportedPaymentProviders } from "@/lib/payment-options";\n${text(files, "src/services/demo-checkout.ts")}`,
222
+ );
223
+ replace(
224
+ files,
225
+ "src/services/demo-checkout.ts",
226
+ `configured: {
227
+ stripe: port.isConfigured("stripe"),
228
+ creem: port.isConfigured("creem"),
229
+ nowpayments: port.isConfigured("nowpayments"),
230
+ },`,
231
+ `configured: Object.fromEntries(supportedPaymentProviders.map((id) => [id, port.isConfigured(id)])) as Record<PurchaseProvider, boolean>,`,
232
+ );
233
+ for (const id of allIds) {
234
+ const definition = definitions[id];
235
+ if (!ids.includes(id)) {
236
+ files.delete(`src/services/${id}.ts`);
237
+ files.delete(`src/routes/api/webhooks/${id}.ts`);
238
+ files.delete(`src/__tests__/${id}.test.ts`);
239
+ for (const key of definition.envKeys) {
240
+ files.set(
241
+ "src/env.ts",
242
+ text(files, "src/env.ts").replace(
243
+ new RegExp(`^ ${key}:.*\\n`, "m"),
244
+ "",
245
+ ),
246
+ );
247
+ files.set(
248
+ ".env.example",
249
+ text(files, ".env.example").replace(
250
+ new RegExp(`^${key}=.*\\n?`, "m"),
251
+ "",
252
+ ),
253
+ );
254
+ }
255
+ } else if (definition.envSchema) {
256
+ replace(
257
+ files,
258
+ "src/env.ts",
259
+ " // Optional payment adapters",
260
+ `${definition.envSchema}\n // Optional payment adapters`,
261
+ );
262
+ files.set(
263
+ ".env.example",
264
+ `${text(files, ".env.example")}\n${definition.envExample}`,
265
+ );
266
+ }
267
+ }
268
+ pkg.scripts.typecheck =
269
+ "bun run generate-routes && bun run paraglide:compile && tsc --noEmit";
270
+ }
@@ -0,0 +1,128 @@
1
+ // One catalog drives prompts, flag validation and adapter composition.
2
+ export const catalog = {
3
+ database: [
4
+ {
5
+ id: "pg",
6
+ label: "PostgreSQL",
7
+ hint: "Direct connection",
8
+ requires: { deployment: ["nitro", "bun"] },
9
+ },
10
+ {
11
+ id: "pg-hyperdrive",
12
+ label: "PostgreSQL (Hyperdrive)",
13
+ hint: "Cloudflare connection pooling",
14
+ requires: { deployment: ["cloudflare"] },
15
+ },
16
+ {
17
+ id: "d1",
18
+ label: "Cloudflare D1",
19
+ requires: { deployment: ["cloudflare"] },
20
+ },
21
+ {
22
+ id: "turso-cloud",
23
+ label: "Turso Cloud",
24
+ hint: "Remote database with URL and auth token",
25
+ },
26
+ {
27
+ id: "turso-local",
28
+ label: "Turso Local (libSQL)",
29
+ hint: "Local database file",
30
+ requires: { deployment: ["nitro", "bun"] },
31
+ },
32
+ {
33
+ id: "bun-sqlite",
34
+ label: "Bun SQLite",
35
+ hint: "Native bun:sqlite driver",
36
+ requires: { deployment: ["bun"] },
37
+ },
38
+ ],
39
+ deployment: [
40
+ { id: "cloudflare", label: "Cloudflare Workers" },
41
+ { id: "nitro", label: "Nitro (Node.js)" },
42
+ { id: "bun", label: "Bun native server" },
43
+ ],
44
+ email: [
45
+ {
46
+ id: "cloudflare",
47
+ label: "Cloudflare Email",
48
+ requires: { deployment: ["cloudflare"] },
49
+ },
50
+ { id: "resend", label: "Resend" },
51
+ ],
52
+ payments: [
53
+ { id: "stripe", label: "Stripe" },
54
+ { id: "creem", label: "Creem.io" },
55
+ { id: "nowpayments", label: "NOWPayments" },
56
+ { id: "polar", label: "Polar" },
57
+ ],
58
+ };
59
+
60
+ export const defaults = {
61
+ database: "pg-hyperdrive",
62
+ deployment: "cloudflare",
63
+ email: "cloudflare",
64
+ payments: ["stripe"],
65
+ };
66
+
67
+ export function incompatibilities(selection) {
68
+ const errors = [];
69
+ for (const [category, options] of Object.entries(catalog)) {
70
+ const ids =
71
+ category === "payments"
72
+ ? (selection.payments ?? [])
73
+ : [selection[category]];
74
+ for (const id of ids) {
75
+ if (id === undefined) continue;
76
+ const option = options.find((entry) => entry.id === id);
77
+ if (!option) {
78
+ errors.push(
79
+ `Unknown ${category} option: ${id}. Choose ${options.map((entry) => entry.id).join(", ")}.`,
80
+ );
81
+ continue;
82
+ }
83
+ for (const [dependency, allowed] of Object.entries(
84
+ option.requires ?? {},
85
+ )) {
86
+ if (selection[dependency] && !allowed.includes(selection[dependency])) {
87
+ errors.push(
88
+ `${option.label} requires ${allowed.map((id) => catalog[dependency].find((entry) => entry.id === id).label).join(" or ")}.`,
89
+ );
90
+ }
91
+ }
92
+ }
93
+ }
94
+ return errors;
95
+ }
96
+
97
+ export function choices(category, selection) {
98
+ return catalog[category].map((option) => {
99
+ const reason = incompatibilities({
100
+ ...selection,
101
+ [category]: option.id,
102
+ }).join(" ");
103
+ return {
104
+ value: option.id,
105
+ name: option.label,
106
+ hint: option.hint,
107
+ ...(reason ? { disabled: reason } : {}),
108
+ };
109
+ });
110
+ }
111
+
112
+ export function isPostgres(database) {
113
+ return database === "pg" || database === "pg-hyperdrive";
114
+ }
115
+
116
+ export function validateSelection(selection) {
117
+ for (const category of Object.keys(catalog)) {
118
+ if (selection[category] === undefined)
119
+ throw new Error(`Missing ${category} selection.`);
120
+ }
121
+ if (!Array.isArray(selection.payments))
122
+ throw new Error("payments must be an array.");
123
+ if (new Set(selection.payments).size !== selection.payments.length)
124
+ throw new Error("Duplicate payment providers.");
125
+ const errors = incompatibilities(selection);
126
+ if (errors.length) throw new Error(errors.join("\n"));
127
+ return selection;
128
+ }