create-ailk 0.18.0 → 0.19.1

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 (218) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +11 -3
  4. package/dist/category-profile.d.ts +10 -0
  5. package/dist/category-profile.js +21 -0
  6. package/dist/derive-architecture.d.ts +5 -1
  7. package/dist/derive-architecture.js +8 -3
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/lib/license-key-files.d.ts +6 -3
  11. package/dist/lib/license-key-files.js +8 -2
  12. package/dist/lib/scaffold-render-sweep.d.ts +32 -0
  13. package/dist/lib/scaffold-render-sweep.js +76 -3
  14. package/dist/license-set.d.ts +1 -1
  15. package/dist/license-set.js +13 -8
  16. package/dist/module-architecture.js +128 -3
  17. package/dist/programmatic.js +11 -1
  18. package/dist/site-roster.js +21 -4
  19. package/dist/spoke-checkout.d.ts +11 -3
  20. package/dist/spoke-checkout.js +62 -12
  21. package/dist/starter-grid.d.ts +1 -1
  22. package/dist/surfaces.d.ts +14 -0
  23. package/dist/surfaces.js +170 -1
  24. package/dist/template-manifest.d.ts +4 -3
  25. package/dist/topology.d.ts +7 -2
  26. package/dist/topology.js +88 -3
  27. package/package.json +5 -4
  28. package/templates/.env.example +3 -3
  29. package/templates/.github/workflows/ci.yml +5 -0
  30. package/templates/apps/api/.env.example +17 -1
  31. package/templates/apps/api/CLAUDE.md +31 -7
  32. package/templates/apps/api/LICENSE +6 -5
  33. package/templates/apps/api/NOTICE +2 -0
  34. package/templates/apps/api/jest.config.cjs +4 -29
  35. package/templates/apps/api/package.json +1 -0
  36. package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +19 -0
  37. package/templates/apps/api/src/__tests__/stripe-boot.test.ts +132 -0
  38. package/templates/apps/api/src/lib/__mocks__/prisma.ts +17 -0
  39. package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +237 -0
  40. package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +334 -0
  41. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  42. package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +217 -0
  43. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +7 -0
  44. package/templates/apps/api/src/lib/consent-store.ts +335 -0
  45. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  46. package/templates/apps/api/src/lib/stripe.ts +176 -3
  47. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +272 -39
  48. package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +51 -0
  49. package/templates/apps/api/src/middleware/auth.ts +214 -46
  50. package/templates/apps/api/src/middleware/rate-limit.ts +5 -0
  51. package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +167 -0
  52. package/templates/apps/api/src/routes/checkout/session.ts +3 -3
  53. package/templates/apps/api/src/routes/checkout/sessions.ts +6 -4
  54. package/templates/apps/api/src/routes/consent/cookie.ts +90 -0
  55. package/templates/apps/api/src/routes/consent/index.ts +18 -0
  56. package/templates/apps/api/src/routes/consent/unsubscribe.ts +127 -0
  57. package/templates/apps/api/src/routes/leads/index.ts +19 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +5 -0
  59. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +17 -4
  60. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +2 -0
  61. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +159 -0
  62. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +158 -0
  63. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +2 -0
  64. package/templates/apps/api/src/routes/webhooks/stripe.ts +17 -0
  65. package/templates/apps/api/src/server.ts +218 -16
  66. package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +503 -0
  67. package/templates/apps/api/src/services/__tests__/consent.test.ts +18 -3
  68. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +173 -4
  69. package/templates/apps/api/src/services/__tests__/person.test.ts +392 -0
  70. package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +87 -0
  71. package/templates/apps/api/src/services/__tests__/revalidation.test.ts +57 -17
  72. package/templates/apps/api/src/services/consent.ts +488 -3
  73. package/templates/apps/api/src/services/lead-routing.ts +135 -14
  74. package/templates/apps/api/src/services/person.ts +501 -1
  75. package/templates/apps/api/src/services/project-listings.ts +7 -2
  76. package/templates/apps/api/src/services/request-confirm.ts +52 -0
  77. package/templates/apps/api/src/services/revalidation.ts +11 -1
  78. package/templates/apps/api/vercel.json +2 -2
  79. package/templates/apps/mcp/.env.example +10 -0
  80. package/templates/apps/mcp/CLAUDE.md +9 -4
  81. package/templates/apps/mcp/LICENSE +6 -5
  82. package/templates/apps/mcp/NOTICE +2 -0
  83. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  84. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  85. package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
  86. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  87. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  88. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  89. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  90. package/templates/apps/mcp/jest.config.cjs +3 -44
  91. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  92. package/templates/apps/mcp/src/boot.ts +5 -23
  93. package/templates/apps/mcp/src/http-transport.ts +93 -73
  94. package/templates/apps/mcp/src/index.ts +2 -0
  95. package/templates/apps/mcp/src/request-context.ts +52 -0
  96. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  97. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  98. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  99. package/templates/apps/web/LICENSE +6 -5
  100. package/templates/apps/web/NOTICE +2 -0
  101. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  102. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/account-frame-sign-out.test.tsx +175 -0
  103. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +302 -0
  104. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +724 -0
  105. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +346 -41
  106. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  107. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout-sign-out.test.tsx +146 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +232 -29
  109. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +140 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  113. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +409 -0
  115. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +65 -380
  116. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  117. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  118. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +188 -54
  119. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +24 -45
  120. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  121. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  123. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  124. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  125. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  126. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  127. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +174 -0
  129. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  132. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  133. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  134. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  135. package/templates/apps/web/app/[locale]/(authed)/account/general/contact-actions.ts +81 -0
  136. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  137. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +193 -0
  138. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  139. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  140. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  141. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  142. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +30 -4
  143. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  144. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  145. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  146. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  147. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  148. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  149. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +132 -0
  150. package/templates/apps/web/app/[locale]/(authed)/account/security/security-panel.tsx +102 -0
  151. package/templates/apps/web/app/[locale]/(authed)/account/security/security-source.ts +62 -0
  152. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  153. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +116 -0
  154. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  155. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  156. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  157. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  158. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  159. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  160. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  161. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  162. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  163. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  164. package/templates/apps/web/auth.ts +25 -2
  165. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  166. package/templates/apps/web/lib/__tests__/auth-email.test.ts +84 -0
  167. package/templates/apps/web/lib/account-config.ts +188 -52
  168. package/templates/apps/web/lib/auth-email.ts +45 -10
  169. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  170. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  171. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  172. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  173. package/templates/apps/web/lib/topology.generated.ts +33 -0
  174. package/templates/apps/web/messages/en.json +194 -33
  175. package/templates/database/CHANGELOG.md +37 -0
  176. package/templates/database/LICENSE +6 -5
  177. package/templates/database/NOTICE +2 -0
  178. package/templates/database/README.md +1 -1
  179. package/templates/database/__tests__/consent-ledger.db.test.ts +345 -0
  180. package/templates/database/__tests__/fence-fan-out.db.test.ts +6 -2
  181. package/templates/database/__tests__/fixtures/person-contact-backfill.json +182 -0
  182. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  183. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  184. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  185. package/templates/database/__tests__/person-contact.db.test.ts +563 -0
  186. package/templates/database/__tests__/person.db.test.ts +3 -1
  187. package/templates/database/__tests__/seed.test.ts +54 -2
  188. package/templates/database/inbox/schema.prisma +166 -25
  189. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  190. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  191. package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +122 -0
  192. package/templates/database/migrations/20261014120000_person_contact/migration.sql +231 -0
  193. package/templates/database/package.json +3 -2
  194. package/templates/database/seed.ts +34 -2
  195. package/versions.json +36 -36
  196. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  197. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  198. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  199. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  200. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  201. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  202. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  203. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  204. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  205. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  206. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  207. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  208. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  209. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  210. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  211. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  212. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  213. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  214. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  215. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  216. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  217. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  218. package/templates/database/__tests__/build.test.ts +0 -131
@@ -1,367 +1,26 @@
1
1
  /**
2
- * /[locale]/account/settings — User settings (#7430, spec 7423 U4 · D10,
3
- * D10a; DESIGN.md §5.12a's section content).
2
+ * /[locale]/account/settings — redirects to User settings › General (#8232,
3
+ * spec docs/specs/8232-member-settings-account-general-and-spec.md D8).
4
4
  *
5
- * RSC, inside the account layout's SignedInShell with the rail visible
6
- * (D10a — a member's rail has one row, so a separate no-rail frame would add
7
- * a step for nothing). One page of anchored sections — the For-you items
8
- * link to `#general` and `#billing`:
5
+ * Settings is four tabs since #8339 — General · Security · Notifications ·
6
+ * Billing — and this one-page, anchored route (#7430) only forwards. A URL
7
+ * fragment never reaches the server, so `#billing` cannot be redirected
8
+ * here: the browser carries it across this redirect, and General's
9
+ * `BillingAnchor` sends it on to Billing; `#general` lands on General as it
10
+ * is. The hub's authorize step still sends an unverified person here
11
+ * (`VERIFY_EMAIL_PATH`), and this redirect carries them on.
9
12
  *
10
- * - General — color mode (only when the site's theme defines more than
11
- * one mode), the email read-only, and the name the member sets and edits
12
- * (an accepted applicant arrives with none, #7418).
13
- * - Security — the sign-in methods this site has configured; a Password
14
- * row only when the password method is on.
15
- * - Notifications — a note until a preference store exists.
16
- * - Privacy — the cookie settings (`/privacy#cookie-settings`, since the
17
- * account area carries no consent Banner, D4) and "Request account
18
- * deletion", which files a Support request of kind `deletion`.
19
- * - Billing — only when `subscription.me()` answers a status or an
20
- * available portal; absent on `status: null` + no portal, and on any
21
- * 401/403/404 (the commerce-disabled build answers 404).
22
- * - Then any section a module contributes (`accountSettingsSections`).
23
- *
24
- * On a spoke that signs in through its hub (`SIGN_IN_PROVIDER === "hub"`,
25
- * #7894 D10) the account belongs to the hub: the name is a read-only row,
26
- * Security says sign-in happens on the hub, and both the name edit and the
27
- * deletion request are a link to the hub's own account settings — so a
28
- * request about a hub account never lands in this spoke's inbox
29
- * (SEC-7894-11). Billing is absent there because its read answers 404.
30
- *
31
- * No Credentials, Scope, Organizations or Credits section: those are an
32
- * agent's or an operator's (§5.12a), never a member's.
13
+ * The directory keeps the one file General reuses unchanged: `theme-modes.ts`.
33
14
  */
34
15
 
35
- import { subscription, type SubscriptionMeResponse } from "@working-theory/api-client";
36
16
  import { localeHref } from "@working-theory/ui";
37
- import { DescriptionList } from "@working-theory/ui/blocks";
38
- import { Button, Heading, Link, Text } from "@working-theory/ui/primitives";
39
- import type { Metadata } from "next";
40
- import { getTranslations } from "next-intl/server";
41
- import type { ReactElement, ReactNode } from "react";
42
-
43
- import { accountSettingsSections } from "../../../../../lib/account-config";
44
- import { getActiveProviderIds } from "../../../../../lib/auth-env";
45
- import { hubSessionConfig } from "../../../../../lib/hub-session/config";
46
- import { SIGN_IN_PROVIDER } from "../../../../../lib/topology.generated";
47
- import { accountSession, logReadFailure } from "../account-data";
48
- import { openBillingPortal } from "../actions";
49
-
50
- import { ColorModeSetting } from "./color-mode";
51
- import { DeletionRequest } from "./deletion-request";
52
- import { NameForm } from "./name-form";
53
- import { siteThemeModeCount } from "./theme-modes";
17
+ import { redirect } from "next/navigation";
54
18
 
55
19
  interface Props {
56
20
  params: Promise<{ locale: string }>;
57
21
  }
58
22
 
59
- export async function generateMetadata({ params }: Props): Promise<Metadata> {
60
- const { locale } = await params;
61
- const t = await getTranslations({ locale, namespace: "account" });
62
- return { title: t("settings.title"), description: t("settings.lead") };
63
- }
64
-
65
- /** The section role DESIGN.md §5.12 names: a title, a muted subtitle, then the fields. */
66
- function Section({
67
- id,
68
- title,
69
- lead,
70
- children,
71
- }: {
72
- id: string;
73
- title: string;
74
- lead?: string;
75
- children: ReactNode;
76
- }): ReactElement {
77
- return (
78
- <section
79
- id={id}
80
- aria-labelledby={`${id}-title`}
81
- className="flex scroll-mt-8 flex-col gap-4 border-t border-(--color-border) pt-8"
82
- data-testid={`account-settings-${id}`}
83
- >
84
- <div>
85
- <Heading level={2} visual={4} id={`${id}-title`}>
86
- {title}
87
- </Heading>
88
- {lead ? (
89
- <Text as="p" variant="body-sm" tone="muted" className="mt-1">
90
- {lead}
91
- </Text>
92
- ) : null}
93
- </div>
94
- {children}
95
- </section>
96
- );
97
- }
98
-
99
- /** The sign-in methods this deployment has configured, as message keys. */
100
- const METHOD_KEYS: Record<string, string> = {
101
- google: "settings.security.google",
102
- github: "settings.security.github",
103
- "magic-link": "settings.security.magicLink",
104
- email: "settings.security.password",
105
- };
106
-
107
- /** The hub's own account settings page, on a spoke that signs in through it. */
108
- function hubSettingsHref(): string | null {
109
- const config = hubSessionConfig();
110
- return config === null ? null : new URL("/account/settings", config.hubOrigin).toString();
111
- }
112
-
113
- /** A link to the hub's account settings, in place of a control that would act on the spoke. */
114
- function HubSettingsLink({
115
- href,
116
- label,
117
- testId,
118
- }: {
119
- href: string;
120
- label: string;
121
- testId: string;
122
- }): ReactElement {
123
- return (
124
- <div>
125
- <Link href={href} className="inline-flex min-h-6 items-center" data-testid={testId}>
126
- {label}
127
- </Link>
128
- </div>
129
- );
130
- }
131
-
132
- /** The statuses with a message key; any other renders as the API sent it. */
133
- const KNOWN_STATUSES: ReadonlySet<string> = new Set(["trialing", "active", "past_due", "canceled"]);
134
-
135
- /**
136
- * Billing's read: `null` on any non-2xx — 401/403/404 by D10 — and a failed
137
- * read leaves the section out rather than failing the whole page. A failure
138
- * other than those three is logged, so it is not silent to the operator.
139
- */
140
- async function readSubscription(
141
- init: { headers: Record<string, string> },
142
- ): Promise<SubscriptionMeResponse | null> {
143
- try {
144
- const res = await subscription.me(init);
145
- if (res.ok) return res.body;
146
- if (![401, 403, 404].includes(res.status)) {
147
- console.error(`[account] read failed source=billing status=${res.status}`);
148
- }
149
- return null;
150
- } catch (err) {
151
- logReadFailure("billing", err);
152
- return null;
153
- }
154
- }
155
-
156
- export default async function AccountSettingsPage({ params }: Props): Promise<ReactElement> {
23
+ export default async function AccountSettingsPage({ params }: Props): Promise<never> {
157
24
  const { locale } = await params;
158
- const t = await getTranslations({ locale, namespace: "account" });
159
- const session = await accountSession(locale);
160
-
161
- // Unreachable behind the (authed) gate; a session that expired between
162
- // the layout and the page renders the heading alone.
163
- if (!session) {
164
- return (
165
- <div className="max-w-3xl py-8" data-testid="account-settings">
166
- <Heading level={1} visual={2}>
167
- {t("settings.title")}
168
- </Heading>
169
- </div>
170
- );
171
- }
172
-
173
- const { ctx } = session;
174
- const sub = await readSubscription(ctx.init);
175
- const showBilling = sub !== null && (sub.status !== null || sub.portalAvailable);
176
- const methods = getActiveProviderIds().filter((id) => id in METHOD_KEYS);
177
- const viaHub = SIGN_IN_PROVIDER === "hub";
178
- const hubHref = viaHub ? hubSettingsHref() : null;
179
-
180
- return (
181
- <div className="flex max-w-3xl flex-col gap-8 py-8" data-testid="account-settings">
182
- <div>
183
- <Heading level={1} visual={2}>
184
- {t("settings.title")}
185
- </Heading>
186
- <Text as="p" variant="body-sm" tone="muted" className="mt-block-tight">
187
- {t("settings.lead")}
188
- </Text>
189
- </div>
190
-
191
- <Section id="general" title={t("settings.general.title")} lead={t("settings.general.lead")}>
192
- {siteThemeModeCount() > 1 ? (
193
- <div className="flex flex-col items-start gap-2">
194
- <Text as="p" variant="body-sm" className="font-medium">
195
- {t("settings.general.colorMode")}
196
- </Text>
197
- <ColorModeSetting
198
- labels={{
199
- title: t("settings.general.colorMode"),
200
- light: t("settings.general.light"),
201
- auto: t("settings.general.auto"),
202
- dark: t("settings.general.dark"),
203
- }}
204
- />
205
- </div>
206
- ) : null}
207
- <DescriptionList
208
- items={[
209
- {
210
- label: t("settings.general.email"),
211
- value: ctx.user.email,
212
- "data-testid": "account-settings-email",
213
- },
214
- ...(viaHub && ctx.user.name
215
- ? [
216
- {
217
- label: t("settings.general.name"),
218
- value: ctx.user.name,
219
- "data-testid": "account-settings-name",
220
- },
221
- ]
222
- : []),
223
- ]}
224
- />
225
- {viaHub ? (
226
- hubHref !== null ? (
227
- <HubSettingsLink
228
- href={hubHref}
229
- label={t("settings.general.manageOnHub")}
230
- testId="account-settings-manage-on-hub"
231
- />
232
- ) : null
233
- ) : (
234
- <NameForm
235
- initialName={ctx.user.name ?? ""}
236
- labels={{
237
- name: t("settings.general.name"),
238
- placeholder: t("settings.general.namePlaceholder"),
239
- save: t("settings.general.save"),
240
- saving: t("settings.general.saving"),
241
- saved: t("settings.general.saved"),
242
- unsaved: t("settings.general.unsaved"),
243
- clean: t("settings.general.clean"),
244
- required: t("settings.general.nameRequired"),
245
- error: t("settings.general.saveError"),
246
- }}
247
- />
248
- )}
249
- </Section>
250
-
251
- <Section id="security" title={t("settings.security.title")} lead={t("settings.security.lead")}>
252
- {viaHub ? (
253
- <Text as="p" variant="body-sm" tone="muted">
254
- {t("settings.security.viaHub")}
255
- </Text>
256
- ) : methods.length > 0 ? (
257
- <div className="flex flex-col gap-2">
258
- <Text as="p" variant="body-sm" className="font-medium">
259
- {t("settings.security.methods")}
260
- </Text>
261
- <DescriptionList
262
- items={methods.map((id) => ({
263
- label: ctx.t(METHOD_KEYS[id]!),
264
- value: t("settings.security.available"),
265
- "data-testid": `account-signin-${id}`,
266
- }))}
267
- />
268
- </div>
269
- ) : (
270
- <Text as="p" variant="body-sm" tone="muted">
271
- {t("settings.security.none")}
272
- </Text>
273
- )}
274
- </Section>
275
-
276
- <Section
277
- id="notifications"
278
- title={t("settings.notifications.title")}
279
- lead={t("settings.notifications.lead")}
280
- >
281
- <Text as="p" variant="body-sm" tone="muted">
282
- {t("settings.notifications.placeholder")}
283
- </Text>
284
- </Section>
285
-
286
- <Section id="privacy" title={t("settings.privacy.title")} lead={t("settings.privacy.lead")}>
287
- <div>
288
- <Link
289
- href={localeHref(locale, "/privacy#cookie-settings")}
290
- className="inline-flex min-h-6 items-center"
291
- data-testid="account-cookie-settings"
292
- >
293
- {t("settings.privacy.cookies")}
294
- </Link>
295
- </div>
296
- {viaHub ? (
297
- hubHref !== null ? (
298
- <HubSettingsLink
299
- href={hubHref}
300
- label={t("settings.general.manageOnHub")}
301
- testId="account-privacy-manage-on-hub"
302
- />
303
- ) : null
304
- ) : (
305
- <div className="flex flex-col gap-2">
306
- <Text as="p" variant="body-sm" tone="muted">
307
- {t("settings.privacy.deletionLead")}
308
- </Text>
309
- <DeletionRequest
310
- labels={{
311
- action: t("settings.privacy.deletion"),
312
- subject: t("settings.privacy.deletionSubject"),
313
- message: t("settings.privacy.deletionMessage"),
314
- sent: t("settings.privacy.deletionSent"),
315
- error: t("settings.privacy.deletionError"),
316
- }}
317
- />
318
- </div>
319
- )}
320
- </Section>
321
-
322
- {showBilling ? (
323
- <Section id="billing" title={t("settings.billing.title")} lead={t("settings.billing.lead")}>
324
- {sub.status !== null || sub.accessRevokesAt ? (
325
- <DescriptionList
326
- items={[
327
- ...(sub.status !== null
328
- ? [
329
- {
330
- label: t("settings.billing.status"),
331
- value: KNOWN_STATUSES.has(sub.status)
332
- ? ctx.t(`settings.billing.statuses.${sub.status}`)
333
- : sub.status,
334
- },
335
- ]
336
- : []),
337
- ...(sub.accessRevokesAt
338
- ? [
339
- {
340
- label: t("settings.billing.accessRevokesAt"),
341
- value: new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(
342
- new Date(sub.accessRevokesAt),
343
- ),
344
- },
345
- ]
346
- : []),
347
- ]}
348
- />
349
- ) : null}
350
- {sub.portalAvailable ? (
351
- <form action={openBillingPortal}>
352
- <Button type="submit" variant="secondary" data-testid="account-billing-manage">
353
- {t("settings.billing.manage")}
354
- </Button>
355
- </form>
356
- ) : null}
357
- </Section>
358
- ) : null}
359
-
360
- {accountSettingsSections.map((section) => (
361
- <Section key={section.id} id={section.id} title={ctx.t(section.title)}>
362
- {section.render(ctx)}
363
- </Section>
364
- ))}
365
- </div>
366
- );
25
+ redirect(localeHref(locale, "/account/general"));
367
26
  }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * settings-frame.tsx — User settings' four tabs, General · Security ·
3
+ * Notifications · Billing (#8339 D7; builds on #8232 D1).
4
+ *
5
+ * `memberSettingsFrame` supplies the member's half of the shared
6
+ * `AccountSettingsFrame` (@working-theory/templates/account, spec 8339 D2):
7
+ * `standalone: true` — the Admin's shape, full height and flush, because the
8
+ * account layout renders these routes outside its AppShell (#8430) — Back to
9
+ * the Inbox, and the tabs of `SETTINGS_ROUTES`. Every member settings page
10
+ * calls it and renders the result through `AccountSettingsFrame` (General,
11
+ * via the shared `AccountSettings`) or directly (Security, Notifications,
12
+ * Billing).
13
+ *
14
+ * `SettingsSection` stays as it is: contributed sections
15
+ * (`accountSettingsSections`, `accountBillingSections`) still use it, as do
16
+ * General's and Billing's own trailing content.
17
+ *
18
+ * #8441 — `SETTINGS_ROUTES` is the ONE place the settings routes are named:
19
+ * the tabs here and `isSettingsPath` (which `account-frame.tsx` takes these
20
+ * routes out of its shell with) both read it, so a tab added here is a tab
21
+ * that renders outside the shell — never a second `<main>` nested in the
22
+ * shell's. Adding a tab is one line here (plus its page and message).
23
+ */
24
+
25
+ import type { AccountSettingsFrameProps, AccountSettingsTab } from "@working-theory/templates/account";
26
+ import { localeHref } from "@working-theory/ui";
27
+ import { Heading, Icon, Text } from "@working-theory/ui/primitives";
28
+ import { Bell, CreditCard, ShieldCheck, UserRound } from "lucide-react";
29
+ import type { ReactElement, ReactNode } from "react";
30
+
31
+ /** The `next-intl` translator General/Security/Notifications/Billing each
32
+ * hold. Widened from `getTranslations`'s own literal-key-checked type (the
33
+ * `AccountTranslate` idiom, `account-data.ts`) — a settings page's own
34
+ * string keys are static, but the function is shared across four route
35
+ * files with four different generic instantiations. Call sites cast their
36
+ * own `t` with `as unknown as Translate`. */
37
+ export type Translate = (key: string, values?: Record<string, unknown>) => string;
38
+
39
+ /** User settings' tabs, in the reference's order (#8339 D7) — each one a
40
+ * route that renders outside the account shell (#8430, #8441). */
41
+ export const SETTINGS_ROUTES = [
42
+ { key: "general", path: "/account/general", titleKey: "settings.general.title", icon: UserRound },
43
+ { key: "security", path: "/account/security", titleKey: "settings.security.title", icon: ShieldCheck },
44
+ { key: "notifications", path: "/account/notifications", titleKey: "settings.notifications.title", icon: Bell },
45
+ { key: "billing", path: "/account/billing", titleKey: "settings.billing.title", icon: CreditCard },
46
+ ] as const;
47
+
48
+ export type SettingsTabKey = (typeof SETTINGS_ROUTES)[number]["key"];
49
+
50
+ /** Whether `pathname` is one of `SETTINGS_ROUTES`: the request locale's
51
+ * `/<locale>` prefix removed, then the rest compared whole — so a sub-route
52
+ * (`/account/billing/history`) is not a settings route unless it is listed. */
53
+ export function isSettingsPath(pathname: string, locale: string): boolean {
54
+ const prefix = `/${locale}`;
55
+ const bare =
56
+ pathname === prefix || pathname.startsWith(`${prefix}/`) ? pathname.slice(prefix.length) : pathname;
57
+ return SETTINGS_ROUTES.some(({ path }) => path === bare);
58
+ }
59
+
60
+ /** The member's half of `AccountSettingsFrameProps` — every settings page's
61
+ * `frame` prop, less `name` (the caller's own session) and `children`. */
62
+ export function memberSettingsFrame(
63
+ locale: string,
64
+ t: Translate,
65
+ active: SettingsTabKey,
66
+ ): Omit<AccountSettingsFrameProps, "name" | "children"> {
67
+ const tabs: readonly AccountSettingsTab[] = SETTINGS_ROUTES.map(({ key, path, titleKey, icon }) => ({
68
+ key,
69
+ label: t(titleKey),
70
+ href: localeHref(locale, path),
71
+ icon: <Icon icon={icon} size="sm" decorative className="shrink-0" />,
72
+ }));
73
+
74
+ return {
75
+ standalone: true,
76
+ back: { label: t("settings.back"), href: localeHref(locale, "/account") },
77
+ railLabel: t("settings.title"),
78
+ accountLabel: t("settings.account"),
79
+ tabs,
80
+ active,
81
+ };
82
+ }
83
+
84
+ /** The section role DESIGN.md §5.12 names: a title, a muted subtitle, then the fields. */
85
+ export function SettingsSection({
86
+ id,
87
+ title,
88
+ lead,
89
+ children,
90
+ }: {
91
+ id: string;
92
+ title: string;
93
+ lead?: string;
94
+ children: ReactNode;
95
+ }): ReactElement {
96
+ return (
97
+ <section
98
+ id={id}
99
+ aria-labelledby={`${id}-title`}
100
+ className="flex scroll-mt-8 flex-col gap-4 border-t border-(--color-border) pt-8"
101
+ data-testid={`account-settings-${id}`}
102
+ >
103
+ <div>
104
+ <Heading level={2} visual={4} id={`${id}-title`}>
105
+ {title}
106
+ </Heading>
107
+ {lead ? (
108
+ <Text as="p" variant="body-sm" tone="muted" className="mt-1">
109
+ {lead}
110
+ </Text>
111
+ ) : null}
112
+ </div>
113
+ {children}
114
+ </section>
115
+ );
116
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * account/sign-in-method.ts (server code) — how a member signs in, for User settings ›
3
+ * Security's sign-in block (#8232, spec
4
+ * docs/specs/8232-member-settings-account-general-and-spec.md D7).
5
+ *
6
+ * One of the account tree's TWO topology seams (roadmap D1; the other is
7
+ * `billing-source.ts`): no other file under the account tree tests
8
+ * `SIGN_IN_PROVIDER`, `TOPOLOGY` or `SIGN_IN_ENABLED`.
9
+ *
10
+ * - A spoke that signs in through its hub (`SIGN_IN_PROVIDER === "hub"`,
11
+ * #7894) answers `hub`, with the hub's host when it is configured.
12
+ * - Any other site answers `site`, with the sign-in methods this deployment
13
+ * has configured. Better Auth's session does not say which one this
14
+ * member used, so the list is the deployment's (C7).
15
+ */
16
+
17
+ import { getActiveProviderIds } from "../../../../lib/auth-env";
18
+ import { hubSessionConfig } from "../../../../lib/hub-session/config";
19
+ import { SIGN_IN_PROVIDER } from "../../../../lib/topology.generated";
20
+
21
+ /** The sign-in methods General names, in the order it lists them. */
22
+ export const SIGN_IN_METHODS = ["google", "github", "magic-link", "email"] as const;
23
+
24
+ export type SignInMethodId = (typeof SIGN_IN_METHODS)[number];
25
+
26
+ export type SignInMethod =
27
+ | { kind: "hub"; hubHost: string | null }
28
+ | { kind: "site"; methods: SignInMethodId[] };
29
+
30
+ export function signInMethod(): SignInMethod {
31
+ if (SIGN_IN_PROVIDER === "hub") {
32
+ const config = hubSessionConfig();
33
+ return { kind: "hub", hubHost: config === null ? null : new URL(config.hubOrigin).host };
34
+ }
35
+ const active = new Set(getActiveProviderIds());
36
+ return { kind: "site", methods: SIGN_IN_METHODS.filter((id) => active.has(id)) };
37
+ }
@@ -4,9 +4,12 @@
4
4
  * account/support-actions.ts — the account area's Support request action.
5
5
  *
6
6
  * `submitSupportRequest` (#7430, spec 7423 D11) — files a Support request
7
- * (and, from User settings › Privacy, an account-deletion request) as a
8
- * contact lead: `POST /v1/leads/submit` with `source: "web"`, the session's
9
- * email and the payload `{ kind, subject, message }`. No `siteId` — the
7
+ * as a contact lead: `POST /v1/leads/submit` with `source: "web"`, the
8
+ * session's email and the payload `{ kind: "support", subject, message }`.
9
+ * Every request it files is `support`, whatever is posted (#8232 D12): an
10
+ * account-deletion request goes through `member.requestDeletion`
11
+ * (`profile-actions.ts`), which files it with the deployment that holds the
12
+ * account, so this action needs no topology test (SEC-7894-11). No `siteId` — the
10
13
  * route resolves the tenant server-side and ignores a posted one (#6203).
11
14
  * The request lands in the admin Inbox as a message. Not a `*Action`-suffixed
12
15
  * export in a `*.actions.ts` file: lead capture already has its MCP twin
@@ -21,7 +24,7 @@
21
24
  import { leads, visitorForwardHeaders } from "@working-theory/api-client";
22
25
  import { headers } from "next/headers";
23
26
 
24
- import { SIGN_IN_PROVIDER, readSession } from "../../../../lib/topology.generated";
27
+ import { readSession } from "../../../../lib/topology.generated";
25
28
 
26
29
  /** PII-safe error identity for logs (the `checkout.actions.ts` idiom: `err.name` only). */
27
30
  function errorName(err: unknown): string {
@@ -53,7 +56,7 @@ function isNotConfigured(res: { status: number; body: unknown }): boolean {
53
56
  }
54
57
 
55
58
  /**
56
- * Files the signed-in member's Support (or deletion) request. The email is
59
+ * Files the signed-in member's Support request. The email is
57
60
  * the session's own — never a form field — so a request always names the
58
61
  * person who sent it.
59
62
  */
@@ -61,10 +64,6 @@ export async function submitSupportRequest(
61
64
  _prev: SupportRequestState,
62
65
  formData: FormData,
63
66
  ): Promise<SupportRequestState> {
64
- // A hub sign-in spoke's account belongs to the hub, whose own settings take
65
- // a deletion request (#7894 D10, SEC-7894-11): here it can only be support.
66
- const kind =
67
- field(formData, "kind") === "deletion" && SIGN_IN_PROVIDER !== "hub" ? "deletion" : "support";
68
67
  const subject = field(formData, "subject");
69
68
  const message = field(formData, "message");
70
69
  if (!subject || !message || subject.length > MAX_SUBJECT || message.length > MAX_MESSAGE) {
@@ -79,7 +78,7 @@ export async function submitSupportRequest(
79
78
  const body = {
80
79
  source: "web",
81
80
  email: session.user.email,
82
- payload: { kind, subject, message },
81
+ payload: { kind: "support", subject, message },
83
82
  };
84
83
  // #7394 — a spoke counts the request against the visitor's own budget.
85
84
  const forwarded = visitorForwardHeaders(
@@ -27,6 +27,7 @@ import "../globals.css";
27
27
 
28
28
  import { Telemetry, VerificationMeta } from "@working-theory/analytics";
29
29
  import { MarketingShell } from "@working-theory/templates/shells";
30
+ import { themeToShellCss } from "@working-theory/theme";
30
31
  import { Link, isRtl, localeHref } from "@working-theory/ui";
31
32
  import { BRAND_PATHS, LocaleSwitcherNav } from "@working-theory/ui/blocks";
32
33
  import {
@@ -58,7 +59,11 @@ import { assertEnabledLocale } from "../../lib/locale-guard";
58
59
  import { footerColumns, navItems } from "../../lib/nav-config";
59
60
  import { getOgImageUrl } from "../../lib/og";
60
61
  import { backLinkHref } from "../../lib/profile-back-link";
61
- import { COPYRIGHT_HOLDER, SITE_NAME } from "../../lib/site-brand";
62
+ import {
63
+ COPYRIGHT_HOLDER,
64
+ SITE_DESCRIPTION,
65
+ SITE_NAME,
66
+ } from "../../lib/site-brand";
62
67
  import { siteChrome, type SiteChrome } from "../../lib/site-chrome";
63
68
  import { siteLogo } from "../../lib/site-logo";
64
69
  // Registers the `_site.mdx` identity with page-renderer at module load, so
@@ -126,6 +131,11 @@ const NAV_CLIENT_ACTION_FLOW: NavActionFlow["flow"] = {
126
131
  action: "#",
127
132
  };
128
133
 
134
+ // The member account shell's token reset (#8337 D3) — a package constant
135
+ // (no site, request or user input), computed once at module scope and
136
+ // emitted only on account routes, after `theme-vars`, below.
137
+ const SHELL_CSS = themeToShellCss();
138
+
129
139
  // ── Theme helpers ────────────────────────────────────────────────────────────
130
140
 
131
141
  /**
@@ -214,10 +224,10 @@ export async function generateMetadata(): Promise<Metadata> {
214
224
 
215
225
  return {
216
226
  title: SITE_NAME,
217
- description: "AX-first OSS template — API + MCP as canonical surfaces.",
227
+ description: SITE_DESCRIPTION,
218
228
  openGraph: {
219
229
  title: SITE_NAME,
220
- description: "AX-first OSS template — API + MCP as canonical surfaces.",
230
+ description: SITE_DESCRIPTION,
221
231
  type: "website",
222
232
  url: baseUrl,
223
233
  images: [ogImageUrl],
@@ -225,7 +235,7 @@ export async function generateMetadata(): Promise<Metadata> {
225
235
  twitter: {
226
236
  card: "summary_large_image",
227
237
  title: SITE_NAME,
228
- description: "AX-first OSS template — API + MCP as canonical surfaces.",
238
+ description: SITE_DESCRIPTION,
229
239
  images: [ogImageUrl],
230
240
  },
231
241
  ...getStealthRobots(),
@@ -617,6 +627,15 @@ export default async function LocaleLayout({
617
627
  data-ailk="theme-vars"
618
628
  dangerouslySetInnerHTML={{ __html: themeHead.css }}
619
629
  />
630
+ {/* The member account shell's token reset (#8337 D3) — account
631
+ routes only, after theme-vars, so :has([data-ailk-shell])'s
632
+ higher specificity wins whatever a browser's :has() support. */}
633
+ {isAccountRoute ? (
634
+ <style
635
+ data-ailk="shell-vars"
636
+ dangerouslySetInnerHTML={{ __html: SHELL_CSS }}
637
+ />
638
+ ) : null}
620
639
  {/* Favicons — single brand mark (theme-independent), PNG fallbacks, ICO legacy */}
621
640
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
622
641
  <link