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.
- package/dist/content-payloads.js +2 -2
- package/dist/module-architecture.js +11 -0
- package/package.json +1 -1
- package/templates/.env.example +13 -3
- package/templates/apps/api/.env.example +5 -0
- package/templates/apps/api/src/__tests__/cors.test.ts +91 -10
- package/templates/apps/api/src/__tests__/server.test.ts +5 -1
- package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +135 -0
- package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +238 -0
- package/templates/apps/api/src/config/index.ts +136 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +2 -2
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +142 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +1 -1
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +1 -1
- package/templates/apps/api/src/middleware/auth.ts +19 -21
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +1 -1
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +1 -1
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +168 -39
- package/templates/apps/api/src/routes/checkout/sessions.ts +20 -34
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +1 -1
- package/templates/apps/api/src/server.ts +21 -16
- package/templates/apps/web/app/[locale]/layout.tsx +57 -56
- package/templates/apps/web/auth.ts +73 -13
- package/templates/apps/web/components/sign-out-trigger.tsx +3 -2
- 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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 ??
|
|
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
|
|
535
|
-
//
|
|
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
|
|
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
|
|
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
|
+
}
|