@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,35 @@
1
+ /**
2
+ * The `user.update.before` guard for server-controlled columns.
3
+ *
4
+ * Better Auth **merges** a before-hook's result over the original data
5
+ * (`actualData = { ...actualData, ...result.data }`), so omitting a key leaves
6
+ * the caller's value in place. Strip-based defences silently no-op.
7
+ *
8
+ * So this refuses instead. No legitimate client sends these on an update.
9
+ * Admin/plugin writes that must set role or ban go through `auth.api.*` admin
10
+ * endpoints or drizzle, which do not run this hook the same way (or use
11
+ * dedicated admin APIs).
12
+ */
13
+ import { APIError } from "better-auth";
14
+
15
+ /** Server-controlled: never settable through the member update-user API. */
16
+ const PROTECTED_FIELDS = [
17
+ "tokensRevokedAt",
18
+ "role",
19
+ "banned",
20
+ "banReason",
21
+ "banExpires",
22
+ ] as const;
23
+
24
+ export function userUpdateGuard<T>(data: T): { data: T } {
25
+ if (!data || typeof data !== "object") return { data };
26
+ const present = PROTECTED_FIELDS.filter(
27
+ (field) => (data as Record<string, unknown>)[field] !== undefined,
28
+ );
29
+ if (present.length > 0) {
30
+ throw new APIError("BAD_REQUEST", {
31
+ message: `These fields cannot be changed here: ${present.join(", ")}.`,
32
+ });
33
+ }
34
+ return { data };
35
+ }
@@ -0,0 +1,46 @@
1
+ import { createMiddleware } from "@tanstack/react-start";
2
+ import { eq } from "drizzle-orm";
3
+ import { user } from "@/db/schema";
4
+ import { HttpError } from "@/utils/api-error";
5
+ import { protectedMiddleware } from "./protected";
6
+
7
+ function forbidden() {
8
+ return new HttpError({
9
+ status: 403,
10
+ error: "forbidden",
11
+ message: "Forbidden",
12
+ });
13
+ }
14
+
15
+ async function can(
16
+ // biome-ignore lint/suspicious/noExplicitAny: middleware context is framework-typed
17
+ context: any,
18
+ permissions: Record<string, string[]>,
19
+ ): Promise<boolean> {
20
+ const [currentUser] = await context.db
21
+ .select({ role: user.role })
22
+ .from(user)
23
+ .where(eq(user.id, context.session.user.id))
24
+ .limit(1);
25
+ const res = await context.auth.api.userHasPermission({
26
+ body: { role: currentUser?.role ?? "user", permissions },
27
+ });
28
+ return Boolean(res?.success);
29
+ }
30
+
31
+ /** Admin console gate: roles holding `config:manage` (admin / superadmin). */
32
+ export const adminMiddleware = createMiddleware()
33
+ .middleware([protectedMiddleware])
34
+ .server(async ({ next, context }) => {
35
+ if (!(await can(context, { config: ["manage"] }))) {
36
+ throw forbidden();
37
+ }
38
+ // Preserve db + auth from parent middleware (do not drop them).
39
+ return next({
40
+ context: {
41
+ session: context.session,
42
+ db: context.db,
43
+ auth: context.auth,
44
+ },
45
+ });
46
+ });
@@ -0,0 +1,37 @@
1
+ import { env } from "cloudflare:workers";
2
+ import { createMiddleware } from "@tanstack/react-start";
3
+ import { authRateLimitKey } from "@/services/auth-rate-limit";
4
+
5
+ // Brute-force / abuse throttle for the sensitive Better Auth endpoints. Better
6
+ // Auth's own limiter is an in-memory Map — per Worker isolate on Cloudflare, so
7
+ // it never aggregates into a global limit. This uses the Workers Rate Limiting
8
+ // binding (AUTH_RATE_LIMITER), which is shared across isolates in a location and
9
+ // strongly consistent. Only enforced when deployed; a no-op locally / if the
10
+ // binding is absent (env.AUTH_RATE_LIMITER undefined). `env` is referenced only
11
+ // inside .server() so it's stripped from the client bundle.
12
+ export const authRateLimitMiddleware = createMiddleware().server(
13
+ async ({ next, request }) => {
14
+ const limiter = (
15
+ env as {
16
+ AUTH_RATE_LIMITER?: {
17
+ limit: (o: { key: string }) => Promise<{ success: boolean }>;
18
+ };
19
+ }
20
+ ).AUTH_RATE_LIMITER;
21
+ const key = limiter ? authRateLimitKey(request) : null;
22
+ if (limiter && key) {
23
+ const { success } = await limiter.limit({ key });
24
+ if (!success) {
25
+ return Response.json(
26
+ {
27
+ status: 429,
28
+ error: "rate_limited",
29
+ message: "Too many requests. Please try again shortly.",
30
+ },
31
+ { status: 429, headers: { "Retry-After": "60" } },
32
+ );
33
+ }
34
+ }
35
+ return next();
36
+ },
37
+ );
@@ -0,0 +1,15 @@
1
+ import { createMiddleware } from "@tanstack/react-start";
2
+ import { createAuth } from "@/lib/auth";
3
+ import { databaseMiddleware } from "./database";
4
+
5
+ export const betterAuthMiddleware = createMiddleware()
6
+ .middleware([databaseMiddleware])
7
+ .server(({ next, context }) => {
8
+ const auth = createAuth(context.db);
9
+
10
+ return next({
11
+ context: {
12
+ auth,
13
+ },
14
+ });
15
+ });
@@ -0,0 +1,62 @@
1
+ import { createMiddleware } from "@tanstack/react-start";
2
+ import {
3
+ BodyTooLargeError,
4
+ readTextCapped,
5
+ UnsupportedMediaTypeError,
6
+ } from "@/utils/body-limit";
7
+
8
+ /**
9
+ * Generous for a JSON callback, small enough that an unauthenticated caller
10
+ * cannot spend the Worker's memory at will.
11
+ */
12
+ export const MAX_WEBHOOK_BODY_BYTES = 64 * 1024;
13
+
14
+ /**
15
+ * Read the request body once, behind a hard cap, and expose the capped raw
16
+ * string as `context.rawBody`.
17
+ *
18
+ * Raw text rather than parsed JSON on purpose: webhooks verify signatures over
19
+ * the exact characters received, so re-serialising would break verification.
20
+ * Handlers that want an object parse `context.rawBody` themselves.
21
+ *
22
+ * This is a *route* middleware, so a refusal is a returned `Response` — the
23
+ * `HttpError` rule in `utils/api-error` covers server-function middleware,
24
+ * where a returned Response would be stamped as data. Same reason
25
+ * `auth-rate-limit` answers 429 the same way.
26
+ */
27
+ export function bodyLimitMiddleware(maxBytes: number) {
28
+ return createMiddleware().server(async ({ request, next }) => {
29
+ let rawBody: string;
30
+ try {
31
+ rawBody = await readTextCapped(request, { maxBytes });
32
+ } catch (error) {
33
+ if (error instanceof BodyTooLargeError) {
34
+ return Response.json(
35
+ {
36
+ status: 413,
37
+ error: "payload_too_large",
38
+ message: error.message,
39
+ },
40
+ { status: 413 },
41
+ );
42
+ }
43
+ if (error instanceof UnsupportedMediaTypeError) {
44
+ return Response.json(
45
+ {
46
+ status: 415,
47
+ error: "unsupported_media_type",
48
+ message: error.message,
49
+ },
50
+ { status: 415 },
51
+ );
52
+ }
53
+ throw error;
54
+ }
55
+ return next({ context: { rawBody } });
56
+ });
57
+ }
58
+
59
+ /** The cap on a public, unauthenticated payment webhook. */
60
+ export const webhookBodyLimitMiddleware = bodyLimitMiddleware(
61
+ MAX_WEBHOOK_BODY_BYTES,
62
+ );
@@ -0,0 +1,13 @@
1
+ import { env } from "cloudflare:workers";
2
+ import { createMiddleware } from "@tanstack/react-start";
3
+ import { createDB } from "@/db";
4
+
5
+ export const databaseMiddleware = createMiddleware().server(({ next }) => {
6
+ const db = createDB(env.HYPERDRIVE);
7
+
8
+ return next({
9
+ context: {
10
+ db,
11
+ },
12
+ });
13
+ });
@@ -0,0 +1,45 @@
1
+ import { createMiddleware } from "@tanstack/react-start";
2
+ import { withRlsUser } from "@/db/helper";
3
+ import { HttpError } from "@/utils/api-error";
4
+ import { betterAuthMiddleware } from "./better-auth";
5
+
6
+ function unauthorized() {
7
+ return new HttpError({
8
+ status: 401,
9
+ error: "unauthorized",
10
+ message: "Authentication required.",
11
+ });
12
+ }
13
+
14
+ /** Require login; leave db as the raw connection (no user RLS transaction). */
15
+ export const sessionMiddleware = createMiddleware()
16
+ .middleware([betterAuthMiddleware])
17
+ .server(async ({ next, context, request }) => {
18
+ const session = await context.auth.api.getSession({
19
+ headers: request.headers,
20
+ });
21
+
22
+ if (!session?.user) throw unauthorized();
23
+
24
+ return next({ context: { session } });
25
+ });
26
+
27
+ /** Require login and run the handler inside a user-scoped RLS transaction. */
28
+ export const protectedMiddleware = createMiddleware()
29
+ .middleware([betterAuthMiddleware])
30
+ .server(async ({ next, context, request }) => {
31
+ const session = await context.auth.api.getSession({
32
+ headers: request.headers,
33
+ });
34
+
35
+ if (!session?.user) throw unauthorized();
36
+
37
+ return withRlsUser(context.db, session.user.id, (tx) =>
38
+ next({
39
+ context: {
40
+ db: tx,
41
+ session,
42
+ },
43
+ }),
44
+ );
45
+ });