create-ailk 0.11.1 → 0.11.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 (34) hide show
  1. package/dist/content-payloads.js +2 -2
  2. package/dist/module-architecture.js +11 -0
  3. package/package.json +1 -1
  4. package/templates/.env.example +13 -3
  5. package/templates/apps/api/.env.example +5 -0
  6. package/templates/apps/api/src/__tests__/cors.test.ts +91 -10
  7. package/templates/apps/api/src/__tests__/server.test.ts +5 -1
  8. package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +135 -0
  9. package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +238 -0
  10. package/templates/apps/api/src/config/index.ts +136 -0
  11. package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +1 -1
  12. package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +1 -1
  13. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +1 -1
  14. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +2 -2
  15. package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +142 -0
  16. package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +1 -1
  17. package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +1 -1
  18. package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +1 -1
  19. package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +1 -1
  20. package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +1 -1
  21. package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +1 -1
  22. package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +1 -1
  23. package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +1 -1
  24. package/templates/apps/api/src/middleware/auth.ts +19 -21
  25. package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +1 -1
  26. package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +1 -1
  27. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +168 -39
  28. package/templates/apps/api/src/routes/checkout/sessions.ts +20 -34
  29. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +1 -1
  30. package/templates/apps/api/src/server.ts +21 -16
  31. package/templates/apps/web/app/[locale]/layout.tsx +57 -56
  32. package/templates/apps/web/auth.ts +73 -13
  33. package/templates/apps/web/components/sign-out-trigger.tsx +3 -2
  34. package/templates/apps/web/components/signed-in-nav.tsx +58 -0
@@ -59,16 +59,76 @@ const RESET_TOKEN_TTL_SECONDS = 3600;
59
59
  */
60
60
  const VERIFY_TOKEN_TTL_SECONDS = 86_400;
61
61
 
62
- // Lazy-load prisma-for-auth: import is deferred until createAuth() is called
63
- // without an explicit `prisma` dep. This prevents the PrismaClient singleton
64
- // from initialising (and calling @working-theory/database) at module-load time, which
65
- // lets tests import `createAuth` without triggering a DB connection.
66
- function getDefaultPrisma(): PrismaClient {
67
- // eslint-disable-next-line @typescript-eslint/no-require-imports
68
- const { prismaForAuth } = require("./lib/prisma-for-auth") as {
69
- prismaForAuth: PrismaClient;
70
- };
71
- return prismaForAuth;
62
+ let prismaForAuthLoad: Promise<PrismaClient> | undefined;
63
+
64
+ /**
65
+ * The `prismaForAuth` singleton, loaded once through `import()`. A failed load
66
+ * is not cached, so the next call retries — as the old in-`createAuth()`
67
+ * require did.
68
+ */
69
+ function loadPrismaForAuth(): Promise<PrismaClient> {
70
+ prismaForAuthLoad ??= import("./lib/prisma-for-auth").then(
71
+ (mod) => mod.prismaForAuth,
72
+ (err: unknown) => {
73
+ prismaForAuthLoad = undefined;
74
+ throw err;
75
+ },
76
+ );
77
+ return prismaForAuthLoad;
78
+ }
79
+
80
+ /**
81
+ * The default client: every member resolves the singleton before it runs.
82
+ *
83
+ * NEVER a synchronous `require()` of `./lib/prisma-for-auth` (#7401).
84
+ * `next build --webpack` compiles that module as an ASYNC module — it imports
85
+ * ESM (`@prisma/adapter-pg`, the generated client) — and a synchronous
86
+ * `require()` of an async webpack module returns a pending namespace, not its
87
+ * exports. `prismaAdapter()` received `undefined`, and every social sign-in
88
+ * 500'd on `Cannot read properties of undefined (reading 'verification')`.
89
+ * `import()` is the ESM-correct load: webpack awaits the module.
90
+ *
91
+ * Deferred rather than imported statically so the module still loads WITHOUT
92
+ * the generated client — `createAuth({ prisma })` tests import this file under
93
+ * CommonJS Jest, which cannot parse that client. Nothing is lost by the
94
+ * deferral: Better Auth, `onPasswordReset` and the reset mailer only touch the
95
+ * client inside request-time async calls. Each member is a function, so the
96
+ * adapter's `typeof db.$transaction === "function"` check holds and
97
+ * `$transaction(cb)` runs on the real client, handing `cb` a real transaction
98
+ * client. (The array form of `$transaction` needs real PrismaPromises; nothing
99
+ * here uses it.)
100
+ */
101
+ function deferredPrismaForAuth(): PrismaClient {
102
+ return new Proxy({} as PrismaClient, {
103
+ get(_, member) {
104
+ // Not a thenable, and no symbol-keyed members (inspection, tags).
105
+ if (typeof member !== "string" || member === "then") return undefined;
106
+ return new Proxy(() => undefined, {
107
+ // `$transaction(cb)`, `$connect()` and the other client methods.
108
+ apply: (_target, _this, args: unknown[]) =>
109
+ loadPrismaForAuth().then((client) =>
110
+ Reflect.apply(Reflect.get(client, member), client, args),
111
+ ),
112
+ // Model delegates: `verification.create(args)`, `user.findFirst(args)`.
113
+ // A delegate is not a thenable either.
114
+ get: (_target, method) =>
115
+ typeof method === "string" && method !== "then"
116
+ ? (...args: unknown[]) =>
117
+ loadPrismaForAuth().then((client) => {
118
+ const delegate: unknown = Reflect.get(client, member);
119
+ // Every member passes the adapter's `if (!db[model])` check,
120
+ // so name a missing model here instead of a bare TypeError.
121
+ if (typeof delegate !== "object" || delegate === null) {
122
+ throw new Error(
123
+ `prismaForAuth has no model delegate "${member}" — regenerate the Prisma client`,
124
+ );
125
+ }
126
+ return Reflect.apply(Reflect.get(delegate, method), delegate, args);
127
+ })
128
+ : undefined,
129
+ });
130
+ },
131
+ });
72
132
  }
73
133
 
74
134
  /**
@@ -215,7 +275,7 @@ export interface CreateAuthDeps {
215
275
  * existing callers (`import { auth }`) are unchanged.
216
276
  */
217
277
  export function createAuth(deps: CreateAuthDeps = {}) {
218
- const prisma = deps.prisma ?? getDefaultPrisma();
278
+ const prisma = deps.prisma ?? deferredPrismaForAuth();
219
279
 
220
280
  /**
221
281
  * ONE predicate, read in ONE place (#5962 § D2).
@@ -531,8 +591,8 @@ export function createAuth(deps: CreateAuthDeps = {}) {
531
591
  }
532
592
 
533
593
  // Lazy singleton: resolved on first access, not at module-load time.
534
- // Defers the `require("./lib/prisma-for-auth")` call until auth is actually
535
- // used, so tests can `import { createAuth }` without triggering a DB connection.
594
+ // Defers `createAuth()` — and `resolveSecret()`'s production check — until
595
+ // auth is actually used.
536
596
  let _auth: ReturnType<typeof createAuth> | undefined;
537
597
 
538
598
  export const auth: ReturnType<typeof createAuth> = new Proxy(
@@ -1,11 +1,12 @@
1
1
  "use client"; // Better Auth's client signOut + router navigation need the browser
2
2
 
3
3
  /**
4
- * SignOutTrigger — the one client child of the server-rendered Nav (#4984).
4
+ * SignOutTrigger — wraps the signed-in Nav's "Sign out" trigger (#4984).
5
5
  *
6
6
  * The locale layout (RSC) reads the session and authors the "Sign out" nav
7
7
  * action as `kind: "flow"`; Nav hands the rendered trigger `<Button>` back
8
- * through `renderFlowTrigger`, and this component clones it with the real
8
+ * through `renderFlowTrigger` — composed in `SignedInNav`, on the client side
9
+ * of the RSC boundary (#7402) — and this component clones it with the real
9
10
  * sign-out handler. Same `createAuthClient()` idiom as `SignInGate`.
10
11
  */
11
12
 
@@ -0,0 +1,58 @@
1
+ "use client"; // owns renderFlowTrigger + navItems — functions that cannot cross the RSC boundary (#7402)
2
+
3
+ /**
4
+ * SignedInNav — the site Nav for a signed-in visitor (#7402).
5
+ *
6
+ * Signed in, the locale layout (RSC) authors two `kind: "flow"` Nav actions —
7
+ * the visitor's name and "Sign out" — and Nav hands their rendered triggers
8
+ * back through `renderFlowTrigger`. That composer is a function, and Nav
9
+ * forwards it into NavMobileMenu ("use client"), so a Server Component that
10
+ * passes it crashes every signed-in page with "Functions cannot be passed
11
+ * directly to Client Components". Composing Nav HERE keeps the function on the
12
+ * client side of the boundary.
13
+ *
14
+ * `navItems` is owned here for the same reason: a generated nav inventory
15
+ * carries Lucide icon components on its flyout entries, and a component is a
16
+ * function too. The layout passes only serializable props (labels, actions,
17
+ * the brand elements); everything callable is imported or defined below.
18
+ *
19
+ * Signed out, the layout keeps rendering Nav as a Server Component — it has
20
+ * no flow action, so nothing callable needs to cross.
21
+ */
22
+
23
+ import { Nav, type NavProps } from "@working-theory/ui/chrome";
24
+ import type { ReactElement } from "react";
25
+
26
+ import { navItems } from "../lib/nav-config";
27
+ import { siteChrome } from "../lib/site-chrome";
28
+
29
+ import { SignOutTrigger } from "./sign-out-trigger";
30
+
31
+ /** Nav's props minus every function-typed one — this type IS the boundary. */
32
+ export type SignedInNavProps = Omit<
33
+ NavProps,
34
+ "navItems" | "renderFlowTrigger" | "translate" | "onSearchOpen"
35
+ >;
36
+
37
+ export function SignedInNav(props: SignedInNavProps): ReactElement {
38
+ return (
39
+ <Nav
40
+ {...props}
41
+ navItems={siteChrome.nav === "mark-only" ? [] : navItems}
42
+ // Both signed-in actions are flows with no lightbox: Sign out becomes a
43
+ // SignOutTrigger, the account action a non-navigating name label.
44
+ renderFlowTrigger={(action, trigger) =>
45
+ action.labelKey === "signOut" ? (
46
+ <SignOutTrigger>{trigger}</SignOutTrigger>
47
+ ) : (
48
+ <span
49
+ className="text-body-sm text-(--color-text-muted)"
50
+ data-testid="nav-account-name"
51
+ >
52
+ {props.labels?.items.account}
53
+ </span>
54
+ )
55
+ }
56
+ />
57
+ );
58
+ }