create-ailk 0.5.0 → 0.6.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.
- package/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/cli.js +54 -5
- package/dist/copy.d.ts +14 -0
- package/dist/copy.js +38 -1
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/extract-module-bundle.d.ts +20 -1
- package/dist/lib/extract-module-bundle.js +64 -5
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/lib/module-license-gate.d.ts +6 -1
- package/dist/lib/module-license-gate.js +6 -1
- package/dist/lib/pro-callout.js +1 -1
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/programmatic.js +14 -10
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/DESIGN.md +148 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/DESIGN.md +148 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/DESIGN.md +148 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/saas-product/DESIGN.md +148 -0
- package/site-templates/simple-service/DESIGN.md +148 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/CLAUDE.md +2 -3
- package/templates/DESIGN.md +155 -0
- package/templates/README.md +42 -8
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/PRODUCT.md +86 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -8
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/middleware.ts +1 -1
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
- package/templates/content/_site.mdx +18 -3
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +3 -2
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- package/templates/apps/web/public/logo-dark.svg +0 -21
- package/templates/apps/web/public/logo-light.svg +0 -21
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client"; // Better Auth's client calls (signIn.social / signIn.email) + error state
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SignInCard — the /sign-in page's client island (#4984).
|
|
5
|
+
*
|
|
6
|
+
* Composes the shipped `AuthCard` block (`intent="sign-in"`, `footer="none"`)
|
|
7
|
+
* and wires its provider buttons / password form to the Better Auth client
|
|
8
|
+
* via the shared `useAuthCardFlow` hook (#6018 AC-3) — the same hook
|
|
9
|
+
* `SignUpCard` next door uses, using the SAME `createAuthClient()` idiom the
|
|
10
|
+
* `(authed)` group's `SignInGate` uses. The page (RSC) decides providers +
|
|
11
|
+
* method from auth-env and hands them in; this component never reads env
|
|
12
|
+
* itself.
|
|
13
|
+
*
|
|
14
|
+
* The brand mark is the SHELL's since #5963 — this island renders no `logo`,
|
|
15
|
+
* or it would appear twice on the page.
|
|
16
|
+
*
|
|
17
|
+
* No sign-up affordance anywhere: `footer="none"` drops the "Not a member?
|
|
18
|
+
* Create an account" cross-link (Better Auth `emailAndPassword.disableSignUp`
|
|
19
|
+
* posture).
|
|
20
|
+
*
|
|
21
|
+
* Password path: `apps/web/auth.ts` does not enable `emailAndPassword` today,
|
|
22
|
+
* so `signIn.email` is wired but inert (Better Auth answers with an error)
|
|
23
|
+
* until an operator enables it server-side — the page only selects
|
|
24
|
+
* `method="password"` when auth-env reports the "email" provider.
|
|
25
|
+
*
|
|
26
|
+
* Zero providers (method sso, providers []): the card renders in its inert
|
|
27
|
+
* state — heading + `notConfiguredSubhead`, no buttons, no form. Never a
|
|
28
|
+
* fabricated session.
|
|
29
|
+
*
|
|
30
|
+
* Error surface: AuthCard's `errors` prop is per-field. A failed provider
|
|
31
|
+
* launch lands on `errors.email` — the social row sits inside the password
|
|
32
|
+
* form beside the email field, so it renders there. The SSO body has no
|
|
33
|
+
* fields at all, so for `method="sso"` the same message is rendered as an
|
|
34
|
+
* `Alert` above the card instead.
|
|
35
|
+
*
|
|
36
|
+
* `forgot` (#6018 AC-1): the page passes `forgot="none"` when
|
|
37
|
+
* `isResetAvailable()` is false, so the "Forgot password?" link never renders
|
|
38
|
+
* a dead end — AuthCard's own default (`"link"`) assumes reset is wired.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
42
|
+
import { Alert, AlertDescription } from "@working-theory/ui/primitives";
|
|
43
|
+
import type { ReactElement } from "react";
|
|
44
|
+
|
|
45
|
+
import { authClient, useAuthCardFlow } from "../use-auth-card-flow";
|
|
46
|
+
|
|
47
|
+
/** The OAuth providers this page can launch — a subset of AuthCard's. */
|
|
48
|
+
export type SignInProvider = "google" | "github";
|
|
49
|
+
|
|
50
|
+
export interface SignInCardProps {
|
|
51
|
+
/** Active OAuth providers, exactly as auth-env reports them. */
|
|
52
|
+
providers: SignInProvider[];
|
|
53
|
+
/** "password" when the email provider is active; otherwise "sso". */
|
|
54
|
+
method: "sso" | "password";
|
|
55
|
+
/** Sanitised post-sign-in path (see `next-path.ts`). */
|
|
56
|
+
next: string;
|
|
57
|
+
/** Href for the card's "Forgot password?" link (#5963). */
|
|
58
|
+
forgotHref?: string;
|
|
59
|
+
/** "link" (AuthCard's default) unless password reset is unavailable (#6018). */
|
|
60
|
+
forgot?: "link" | "none";
|
|
61
|
+
/** Translated AuthCard labels (resolved server-side by the page). */
|
|
62
|
+
labels: AuthCardLabels;
|
|
63
|
+
/** Subhead shown when no provider is configured. */
|
|
64
|
+
notConfiguredSubhead: string;
|
|
65
|
+
/** Translated error copy. */
|
|
66
|
+
errorLabels: { signInFailed: string; providerFailed: string };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function SignInCard({
|
|
70
|
+
providers,
|
|
71
|
+
method,
|
|
72
|
+
next,
|
|
73
|
+
forgotHref,
|
|
74
|
+
forgot,
|
|
75
|
+
labels,
|
|
76
|
+
notConfiguredSubhead,
|
|
77
|
+
errorLabels,
|
|
78
|
+
}: SignInCardProps): ReactElement {
|
|
79
|
+
const { errors, providerError, launchProvider, submitPassword } =
|
|
80
|
+
useAuthCardFlow(next, {
|
|
81
|
+
providerFailed: errorLabels.providerFailed,
|
|
82
|
+
passwordFailed: errorLabels.signInFailed,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const notConfigured = method === "sso" && providers.length === 0;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
/*
|
|
89
|
+
* No page framing of its own (#5963): AuthShell owns the brand row, the
|
|
90
|
+
* 400px column and the vertical rhythm. The `min-h-screen items-center
|
|
91
|
+
* justify-center` wrapper this used to carry is exactly the "card centered
|
|
92
|
+
* in empty space, first input at 470px" defect the auth route group fixes.
|
|
93
|
+
*/
|
|
94
|
+
<div className="flex flex-col gap-4" data-testid="sign-in-page">
|
|
95
|
+
{method === "sso" && providerError ? (
|
|
96
|
+
<Alert variant="error" data-testid="sign-in-provider-error">
|
|
97
|
+
<AlertDescription>{providerError}</AlertDescription>
|
|
98
|
+
</Alert>
|
|
99
|
+
) : null}
|
|
100
|
+
<AuthCard
|
|
101
|
+
intent="sign-in"
|
|
102
|
+
method={method}
|
|
103
|
+
footer="none"
|
|
104
|
+
{...(forgotHref !== undefined ? { forgotHref } : {})}
|
|
105
|
+
{...(forgot !== undefined ? { forgot } : {})}
|
|
106
|
+
{...(notConfigured ? { subhead: notConfiguredSubhead } : {})}
|
|
107
|
+
providers={providers.map((provider) => ({
|
|
108
|
+
provider,
|
|
109
|
+
onClick: () => void launchProvider(provider),
|
|
110
|
+
}))}
|
|
111
|
+
onSubmit={(data) =>
|
|
112
|
+
void submitPassword(
|
|
113
|
+
authClient.signIn.email({
|
|
114
|
+
email: data.email,
|
|
115
|
+
password: data.password ?? "",
|
|
116
|
+
callbackURL: next,
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
errors={errors}
|
|
121
|
+
labels={labels}
|
|
122
|
+
/>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/[locale]/sign-up` — the site's sign-up page (#5963).
|
|
3
|
+
*
|
|
4
|
+
* RSC. Branches on `isSignUpOpen()`:
|
|
5
|
+
* - open → the sign-up form island, wired to Better Auth exactly as
|
|
6
|
+
* `/sign-in`'s island is (providers + method from auth-env).
|
|
7
|
+
* - closed → AuthCard's `invitation` intent: copy only, with the footer's
|
|
8
|
+
* "Back to sign in" link. This is the DEFAULT — `apps/web/auth.ts`
|
|
9
|
+
* does not enable `emailAndPassword`, so a scaffold offers no
|
|
10
|
+
* public sign-up and the page must not advertise one.
|
|
11
|
+
*
|
|
12
|
+
* `AuthShell` (the (auth) group's layout) owns the brand mark and the 400px
|
|
13
|
+
* column, so this page passes no `logo` and no framing of its own.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { localeHref } from "@working-theory/ui";
|
|
17
|
+
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
18
|
+
import type { Metadata } from "next";
|
|
19
|
+
import { getTranslations } from "next-intl/server";
|
|
20
|
+
import type { ReactElement } from "react";
|
|
21
|
+
|
|
22
|
+
import { getActiveProviderIds, isSignUpOpen } from "../../../../lib/auth-env";
|
|
23
|
+
|
|
24
|
+
import { SignUpCard, type SignUpProvider } from "./sign-up-card";
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
params: Promise<{ locale: string }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
31
|
+
const { locale } = await params;
|
|
32
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
33
|
+
return { title: t("signUpTitle"), description: t("signUpDescription") };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default async function SignUpPage({
|
|
37
|
+
params,
|
|
38
|
+
}: Props): Promise<ReactElement> {
|
|
39
|
+
const { locale } = await params;
|
|
40
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
41
|
+
const signInHref = localeHref(locale, "/sign-in");
|
|
42
|
+
|
|
43
|
+
if (!isSignUpOpen()) {
|
|
44
|
+
const closedLabels: AuthCardLabels = {
|
|
45
|
+
invitationHeading: t("invitationHeading"),
|
|
46
|
+
invitationSubhead: t("invitationSubhead"),
|
|
47
|
+
invitationBody: t("invitationBody"),
|
|
48
|
+
backToSignIn: t("backToSignIn"),
|
|
49
|
+
};
|
|
50
|
+
return (
|
|
51
|
+
<div className="flex flex-col gap-4" data-testid="sign-up-page">
|
|
52
|
+
<AuthCard
|
|
53
|
+
intent="invitation"
|
|
54
|
+
signInHref={signInHref}
|
|
55
|
+
labels={closedLabels}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const active = getActiveProviderIds();
|
|
62
|
+
// Exactly the OAuth providers auth-env reports — never a default set.
|
|
63
|
+
const providers = active.filter(
|
|
64
|
+
(id): id is SignUpProvider => id === "google" || id === "github",
|
|
65
|
+
);
|
|
66
|
+
const method = active.includes("email") ? "password" : "sso";
|
|
67
|
+
|
|
68
|
+
const labels: AuthCardLabels = {
|
|
69
|
+
signUpHeading: t("signUpHeading"),
|
|
70
|
+
signUpSubhead: t("signUpSubhead"),
|
|
71
|
+
signUpSubmit: t("signUpSubmit"),
|
|
72
|
+
signUpFooterPrompt: t("signUpFooterPrompt"),
|
|
73
|
+
signUpFooterLink: t("signUpFooterLink"),
|
|
74
|
+
ssoPrefix: t("ssoPrefix"),
|
|
75
|
+
google: t("google"),
|
|
76
|
+
github: t("github"),
|
|
77
|
+
email: t("email"),
|
|
78
|
+
password: t("password"),
|
|
79
|
+
orContinue: t("orContinue"),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<SignUpCard
|
|
84
|
+
providers={providers}
|
|
85
|
+
method={method}
|
|
86
|
+
next={localeHref(locale, "/account")}
|
|
87
|
+
signInHref={signInHref}
|
|
88
|
+
labels={labels}
|
|
89
|
+
notConfiguredSubhead={t("signUpNotConfiguredSubhead")}
|
|
90
|
+
errorLabels={{
|
|
91
|
+
signUpFailed: t("signUpFailed"),
|
|
92
|
+
providerFailed: t("providerFailed"),
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client"; // Better Auth's client calls (signUp.email / signIn.social) + error state
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SignUpCard — the /sign-up page's client island (#5963).
|
|
5
|
+
*
|
|
6
|
+
* The sign-in island's twin: composes `AuthCard` (`intent="sign-up"`) and
|
|
7
|
+
* wires its provider buttons / password form to the Better Auth client via
|
|
8
|
+
* the shared `useAuthCardFlow` hook (#6018 AC-3) — the same hook
|
|
9
|
+
* `SignInCard` next door uses. The page (RSC) decides providers + method from
|
|
10
|
+
* auth-env and hands them in; this component never reads env.
|
|
11
|
+
*
|
|
12
|
+
* Inert until an operator enables `emailAndPassword` server-side — the same
|
|
13
|
+
* documented posture the sign-in island already carries for its password
|
|
14
|
+
* path. The page only renders this island at all when `isSignUpOpen()`.
|
|
15
|
+
*
|
|
16
|
+
* The brand mark is the shell's: no `logo` is passed, or it would render twice.
|
|
17
|
+
*
|
|
18
|
+
* Error surface: `AuthCard`'s `errors` prop is per-field, so a failed provider
|
|
19
|
+
* launch lands on `errors.email` (the social row sits inside the password form
|
|
20
|
+
* beside the email field). The SSO body has no fields, so the same message is
|
|
21
|
+
* rendered as an `Alert` above the card for `method="sso"`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
25
|
+
import { Alert, AlertDescription } from "@working-theory/ui/primitives";
|
|
26
|
+
import type { ReactElement } from "react";
|
|
27
|
+
|
|
28
|
+
import { authClient, useAuthCardFlow } from "../use-auth-card-flow";
|
|
29
|
+
|
|
30
|
+
/** The OAuth providers this page can launch — a subset of AuthCard's. */
|
|
31
|
+
export type SignUpProvider = "google" | "github";
|
|
32
|
+
|
|
33
|
+
export interface SignUpCardProps {
|
|
34
|
+
/** Active OAuth providers, exactly as auth-env reports them. */
|
|
35
|
+
providers: SignUpProvider[];
|
|
36
|
+
/** "password" when the email provider is active; otherwise "sso". */
|
|
37
|
+
method: "sso" | "password";
|
|
38
|
+
/** Post-sign-up path. */
|
|
39
|
+
next: string;
|
|
40
|
+
/** Href for the footer's "Sign in" cross-link. */
|
|
41
|
+
signInHref: string;
|
|
42
|
+
/** Translated AuthCard labels (resolved server-side by the page). */
|
|
43
|
+
labels: AuthCardLabels;
|
|
44
|
+
/** Subhead shown when no provider is configured. */
|
|
45
|
+
notConfiguredSubhead: string;
|
|
46
|
+
/** Translated error copy. */
|
|
47
|
+
errorLabels: { signUpFailed: string; providerFailed: string };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function SignUpCard({
|
|
51
|
+
providers,
|
|
52
|
+
method,
|
|
53
|
+
next,
|
|
54
|
+
signInHref,
|
|
55
|
+
labels,
|
|
56
|
+
notConfiguredSubhead,
|
|
57
|
+
errorLabels,
|
|
58
|
+
}: SignUpCardProps): ReactElement {
|
|
59
|
+
const { errors, providerError, launchProvider, submitPassword } =
|
|
60
|
+
useAuthCardFlow(next, {
|
|
61
|
+
providerFailed: errorLabels.providerFailed,
|
|
62
|
+
passwordFailed: errorLabels.signUpFailed,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const notConfigured = method === "sso" && providers.length === 0;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex flex-col gap-4" data-testid="sign-up-page">
|
|
69
|
+
{method === "sso" && providerError ? (
|
|
70
|
+
<Alert variant="error" data-testid="sign-up-provider-error">
|
|
71
|
+
<AlertDescription>{providerError}</AlertDescription>
|
|
72
|
+
</Alert>
|
|
73
|
+
) : null}
|
|
74
|
+
<AuthCard
|
|
75
|
+
intent="sign-up"
|
|
76
|
+
method={method}
|
|
77
|
+
signInHref={signInHref}
|
|
78
|
+
{...(notConfigured ? { subhead: notConfiguredSubhead } : {})}
|
|
79
|
+
providers={providers.map((provider) => ({
|
|
80
|
+
provider,
|
|
81
|
+
onClick: () => void launchProvider(provider),
|
|
82
|
+
}))}
|
|
83
|
+
onSubmit={(data) =>
|
|
84
|
+
// Better Auth requires a `name` on email sign-up; this card collects
|
|
85
|
+
// none, so the address doubles as the display name until the person
|
|
86
|
+
// sets one.
|
|
87
|
+
void submitPassword(
|
|
88
|
+
authClient.signUp.email({
|
|
89
|
+
email: data.email,
|
|
90
|
+
password: data.password ?? "",
|
|
91
|
+
name: data.email,
|
|
92
|
+
callbackURL: next,
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
errors={errors}
|
|
97
|
+
labels={labels}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client"; // useState + the Better Auth client's browser-side calls
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useAuthCardFlow — the provider-launch + password-submit boilerplate shared
|
|
5
|
+
* by SignInCard and SignUpCard (#6018 AC-3).
|
|
6
|
+
*
|
|
7
|
+
* Both cards wire an `AuthCard`'s provider buttons and password form to the
|
|
8
|
+
* same two moves: launch a Better Auth OAuth provider (a failed launch lands
|
|
9
|
+
* the shared `providerFailed` message on the email field, and — for an
|
|
10
|
+
* SSO-only card with no fields — as a banner above the card) and submit the
|
|
11
|
+
* password form (a failed submit lands a caller-named message on the
|
|
12
|
+
* password field).
|
|
13
|
+
*
|
|
14
|
+
* What differs between sign-in and sign-up is the underlying Better Auth CALL
|
|
15
|
+
* itself — `signIn.email(...)` vs `signUp.email(...)`, the latter needing a
|
|
16
|
+
* `name` — so `submitPassword` takes the call's own promise as an argument
|
|
17
|
+
* rather than owning the choice of which Better Auth method to invoke. The
|
|
18
|
+
* single shared `authClient` this module exports is what each card's
|
|
19
|
+
* `onSubmit` calls that method on.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { createAuthClient } from "better-auth/react";
|
|
23
|
+
import { useState } from "react";
|
|
24
|
+
|
|
25
|
+
export const authClient = createAuthClient();
|
|
26
|
+
|
|
27
|
+
/** The OAuth providers either card can launch. */
|
|
28
|
+
export type AuthProvider = "google" | "github";
|
|
29
|
+
|
|
30
|
+
export interface AuthCardFlowErrorLabels {
|
|
31
|
+
/** Shown on a failed OAuth provider launch (shared by both cards). */
|
|
32
|
+
providerFailed: string;
|
|
33
|
+
/** Shown on a failed password submit — signInFailed or signUpFailed, caller-named. */
|
|
34
|
+
passwordFailed: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface FieldErrors {
|
|
38
|
+
email?: string;
|
|
39
|
+
password?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The shape every Better Auth client call this hook awaits resolves to. */
|
|
43
|
+
type AuthResult = { error?: unknown } | null | undefined;
|
|
44
|
+
|
|
45
|
+
export interface AuthCardFlow {
|
|
46
|
+
/** Per-field form errors, in `AuthCard`'s `errors` prop shape. */
|
|
47
|
+
errors: FieldErrors;
|
|
48
|
+
/** The SSO-body error banner text, or null when none is pending. */
|
|
49
|
+
providerError: string | null;
|
|
50
|
+
/** Launches an OAuth provider via `signIn.social`. */
|
|
51
|
+
launchProvider: (provider: AuthProvider) => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Awaits the caller's own Better Auth call (`signIn.email(...)` or
|
|
54
|
+
* `signUp.email(...)`) and surfaces a `{ error }` result — or a rejection —
|
|
55
|
+
* on the password field.
|
|
56
|
+
*/
|
|
57
|
+
submitPassword: (action: Promise<AuthResult>) => Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param next - the post-auth `callbackURL` every provider launch uses.
|
|
62
|
+
* @param errorLabels - translated copy for the two failure cases.
|
|
63
|
+
*/
|
|
64
|
+
export function useAuthCardFlow(
|
|
65
|
+
next: string,
|
|
66
|
+
errorLabels: AuthCardFlowErrorLabels,
|
|
67
|
+
): AuthCardFlow {
|
|
68
|
+
const [errors, setErrors] = useState<FieldErrors>({});
|
|
69
|
+
const [providerError, setProviderError] = useState<string | null>(null);
|
|
70
|
+
|
|
71
|
+
const launchProvider = async (provider: AuthProvider): Promise<void> => {
|
|
72
|
+
setProviderError(null);
|
|
73
|
+
// Better Auth reports a failed launch as `{ error }`; a network/runtime
|
|
74
|
+
// failure rejects. Both land on the same providerFailed message.
|
|
75
|
+
const failed = await authClient.signIn
|
|
76
|
+
.social({ provider, callbackURL: next })
|
|
77
|
+
.then(
|
|
78
|
+
(res: AuthResult) => Boolean(res?.error),
|
|
79
|
+
() => true,
|
|
80
|
+
);
|
|
81
|
+
if (failed) {
|
|
82
|
+
setProviderError(errorLabels.providerFailed);
|
|
83
|
+
setErrors((prev) => ({ ...prev, email: errorLabels.providerFailed }));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const submitPassword = async (action: Promise<AuthResult>): Promise<void> => {
|
|
88
|
+
setErrors({});
|
|
89
|
+
// Mirrors launchProvider's own resilience: a `{ error }` result AND a
|
|
90
|
+
// rejected promise both land on the same passwordFailed message, so a
|
|
91
|
+
// network/runtime failure from the caller's signIn.email/signUp.email
|
|
92
|
+
// call surfaces in the form rather than becoming an unhandled rejection
|
|
93
|
+
// (code review, #6018).
|
|
94
|
+
const failed = await action.then(
|
|
95
|
+
(res) => Boolean(res?.error),
|
|
96
|
+
() => true,
|
|
97
|
+
);
|
|
98
|
+
if (failed) setErrors({ password: errorLabels.passwordFailed });
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return { errors, providerError, launchProvider, submitPassword };
|
|
102
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/[locale]/verify-email` — the check-your-email terminus (#5963, wired by #5962).
|
|
3
|
+
*
|
|
4
|
+
* Two states, both copy-only:
|
|
5
|
+
* - no `?error` → the `sent` intent. What #5963 shipped: this page states
|
|
6
|
+
* what just happened.
|
|
7
|
+
* - `?error=…` → the same expired/invalid-token state `/reset-password`
|
|
8
|
+
* renders. Better Auth's `/verify-email` redirects to its
|
|
9
|
+
* callback with `?error=<code>` when the JWT is expired,
|
|
10
|
+
* malformed or signed with a rotated secret.
|
|
11
|
+
*
|
|
12
|
+
* The verification token's properties are NOT the reset token's, and the copy
|
|
13
|
+
* is written accordingly: it is a signed JWT rather than a row, so it is
|
|
14
|
+
* neither single-use nor revocable — the same link keeps working until it
|
|
15
|
+
* expires. That asymmetry is acceptable precisely because its blast radius is
|
|
16
|
+
* `emailVerified: true` on one row rather than a credential.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { localeHref } from "@working-theory/ui";
|
|
20
|
+
import { AuthCard, type AuthCardLabels } from "@working-theory/ui/blocks";
|
|
21
|
+
import type { Metadata } from "next";
|
|
22
|
+
import { getTranslations } from "next-intl/server";
|
|
23
|
+
import type { ReactElement } from "react";
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
params: Promise<{ locale: string }>;
|
|
27
|
+
searchParams?: Promise<{ error?: string | string[] }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function firstValue(raw: string | string[] | undefined): string {
|
|
31
|
+
if (Array.isArray(raw)) return raw[0] ?? "";
|
|
32
|
+
return raw ?? "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
36
|
+
const { locale } = await params;
|
|
37
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
38
|
+
return {
|
|
39
|
+
title: t("verifyEmailTitle"),
|
|
40
|
+
description: t("verifyEmailDescription"),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default async function VerifyEmailPage({
|
|
45
|
+
params,
|
|
46
|
+
searchParams,
|
|
47
|
+
}: Props): Promise<ReactElement> {
|
|
48
|
+
const { locale } = await params;
|
|
49
|
+
const t = await getTranslations({ locale, namespace: "auth" });
|
|
50
|
+
const signInHref = localeHref(locale, "/sign-in");
|
|
51
|
+
|
|
52
|
+
const query = searchParams ? await searchParams : {};
|
|
53
|
+
const error = firstValue(query.error);
|
|
54
|
+
|
|
55
|
+
if (error) {
|
|
56
|
+
const labels: AuthCardLabels = {
|
|
57
|
+
invitationHeading: t("verifyExpiredHeading"),
|
|
58
|
+
invitationSubhead: t("verifyExpiredSubhead"),
|
|
59
|
+
invitationBody: t("verifyExpiredBody"),
|
|
60
|
+
backToSignIn: t("backToSignIn"),
|
|
61
|
+
};
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex flex-col gap-4" data-testid="verify-email-page">
|
|
64
|
+
<div data-testid="verify-token-error">
|
|
65
|
+
<AuthCard
|
|
66
|
+
intent="invitation"
|
|
67
|
+
signInHref={signInHref}
|
|
68
|
+
labels={labels}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const labels: AuthCardLabels = {
|
|
76
|
+
sentHeading: t("sentHeading"),
|
|
77
|
+
sentSubhead: t("sentSubhead"),
|
|
78
|
+
sentBody: t("sentBody"),
|
|
79
|
+
backToSignIn: t("backToSignIn"),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="flex flex-col gap-4" data-testid="verify-email-page">
|
|
84
|
+
<AuthCard intent="sent" signInHref={signInHref} labels={labels} />
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -26,7 +26,9 @@ const mockGetSession = jest.fn();
|
|
|
26
26
|
// eslint-disable-next-line no-restricted-syntax -- #507: mock-internal rewrite pending; auth.ts's lazy Better-Auth singleton has no HTTP/DB boundary a unit test of the session-branch can mock without standing up Prisma.
|
|
27
27
|
jest.mock("../../../../auth", () => ({
|
|
28
28
|
__esModule: true,
|
|
29
|
-
auth: {
|
|
29
|
+
auth: {
|
|
30
|
+
api: { getSession: (...args: unknown[]) => mockGetSession(...args) },
|
|
31
|
+
},
|
|
30
32
|
}));
|
|
31
33
|
|
|
32
34
|
const mockGetActiveProviderIds = jest.fn();
|
|
@@ -86,6 +88,20 @@ describe("AuthedLayout — the (authed) session guard", () => {
|
|
|
86
88
|
expect(screen.queryByTestId("sign-in-gate")).not.toBeInTheDocument();
|
|
87
89
|
});
|
|
88
90
|
|
|
91
|
+
// #5953 AC-1, web half — the app profile applies to the signed-in editor
|
|
92
|
+
// surfaces only; SignInGate stays on the marketing profile.
|
|
93
|
+
it('wraps the signed-in children in a data-profile="app" root', async () => {
|
|
94
|
+
mockGetSession.mockResolvedValue({ user: { id: "user_1" } });
|
|
95
|
+
const element = await AuthedLayout({
|
|
96
|
+
params: Promise.resolve({ locale: "en" }),
|
|
97
|
+
children: <div data-testid="dashboard-content" />,
|
|
98
|
+
});
|
|
99
|
+
render(element);
|
|
100
|
+
const root = screen.getByTestId("authed-profile-root");
|
|
101
|
+
expect(root).toHaveAttribute("data-profile", "app");
|
|
102
|
+
expect(root).toContainElement(screen.getByTestId("dashboard-content"));
|
|
103
|
+
});
|
|
104
|
+
|
|
89
105
|
it("ignores AILK_AUTH_REQUIRED entirely — never read by the guard", async () => {
|
|
90
106
|
const original = process.env.AILK_AUTH_REQUIRED;
|
|
91
107
|
process.env.AILK_AUTH_REQUIRED = "0";
|
|
@@ -114,18 +130,14 @@ describe("<SignInGate/> — the dashboard's own front door", () => {
|
|
|
114
130
|
});
|
|
115
131
|
|
|
116
132
|
it("renders AuthPanel with the active providers", () => {
|
|
117
|
-
render(
|
|
118
|
-
<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />,
|
|
119
|
-
);
|
|
133
|
+
render(<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />);
|
|
120
134
|
expect(screen.getByTestId("sign-in-gate")).toBeInTheDocument();
|
|
121
135
|
expect(screen.getByTestId("auth-panel-card")).toBeInTheDocument();
|
|
122
136
|
});
|
|
123
137
|
|
|
124
138
|
it("renders an EmptyState when no provider is active", () => {
|
|
125
139
|
mockGetActiveProviderIds.mockReturnValue([]);
|
|
126
|
-
render(
|
|
127
|
-
<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />,
|
|
128
|
-
);
|
|
140
|
+
render(<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />);
|
|
129
141
|
expect(screen.getByTestId("sign-in-gate-empty")).toBeInTheDocument();
|
|
130
142
|
expect(screen.getByText("No providers")).toBeInTheDocument();
|
|
131
143
|
expect(screen.queryByTestId("auth-panel-card")).not.toBeInTheDocument();
|
|
@@ -133,9 +145,7 @@ describe("<SignInGate/> — the dashboard's own front door", () => {
|
|
|
133
145
|
|
|
134
146
|
it("filters an inactive/unsupported provider id out of the panel", () => {
|
|
135
147
|
mockGetActiveProviderIds.mockReturnValue(["email"]);
|
|
136
|
-
render(
|
|
137
|
-
<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />,
|
|
138
|
-
);
|
|
148
|
+
render(<SignInGate labels={GATE_LABELS} signInPath="/sign-in" />);
|
|
139
149
|
// "email" is reported by getActiveProviderIds but is not a supported
|
|
140
150
|
// AuthPanel provider (google | github) — treated as zero active providers.
|
|
141
151
|
expect(screen.getByTestId("sign-in-gate-empty")).toBeInTheDocument();
|
|
@@ -50,10 +50,16 @@ jest.mock("../../../../../auth", () => ({
|
|
|
50
50
|
|
|
51
51
|
const mockSignupMe = jest.fn();
|
|
52
52
|
const mockCheckoutsMe = jest.fn();
|
|
53
|
+
const mockSubscriptionMe = jest.fn();
|
|
54
|
+
const mockSubscriptionPortalSession = jest.fn();
|
|
53
55
|
jest.mock("@working-theory/api-client", () => ({
|
|
54
56
|
__esModule: true,
|
|
55
57
|
waitlistSignups: { me: (...args: unknown[]) => mockSignupMe(...args) },
|
|
56
58
|
flowCheckouts: { me: (...args: unknown[]) => mockCheckoutsMe(...args) },
|
|
59
|
+
subscription: {
|
|
60
|
+
me: (...args: unknown[]) => mockSubscriptionMe(...args),
|
|
61
|
+
portalSession: (...args: unknown[]) => mockSubscriptionPortalSession(...args),
|
|
62
|
+
},
|
|
57
63
|
}));
|
|
58
64
|
|
|
59
65
|
let mockExperimentKeys: string[] = [];
|
|
@@ -139,6 +145,12 @@ beforeEach(() => {
|
|
|
139
145
|
key === SIGNUP.experimentKey ? { ok: true, body: SIGNUP } : NOT_FOUND,
|
|
140
146
|
);
|
|
141
147
|
mockCheckoutsMe.mockReset().mockResolvedValue({ ok: true, body: CHECKOUTS });
|
|
148
|
+
// Default: a healthy subscriber (no banner) — commerce-disabled builds
|
|
149
|
+
// read identically (a 404), covered explicitly below (D8).
|
|
150
|
+
mockSubscriptionMe.mockReset().mockResolvedValue({
|
|
151
|
+
ok: true,
|
|
152
|
+
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: false },
|
|
153
|
+
});
|
|
142
154
|
mockSignOut.mockReset().mockResolvedValue(undefined);
|
|
143
155
|
mockPush.mockReset();
|
|
144
156
|
mockRefresh.mockReset();
|
|
@@ -258,3 +270,75 @@ describe("AccountPage — sign out", () => {
|
|
|
258
270
|
expect(mockSignOut.mock.invocationCallOrder[0]).toBeLessThan(mockPush.mock.invocationCallOrder[0]!);
|
|
259
271
|
});
|
|
260
272
|
});
|
|
273
|
+
|
|
274
|
+
describe("AccountPage — subscription status banner (#6038)", () => {
|
|
275
|
+
it("renders the banner for a subscriber with actionRequired: true", async () => {
|
|
276
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
277
|
+
ok: true,
|
|
278
|
+
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
|
|
279
|
+
});
|
|
280
|
+
await renderPage();
|
|
281
|
+
expect(screen.getByTestId("subscription-status-banner")).toBeInTheDocument();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("renders no banner for a healthy subscriber — AC-1", async () => {
|
|
285
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
286
|
+
ok: true,
|
|
287
|
+
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: true },
|
|
288
|
+
});
|
|
289
|
+
await renderPage();
|
|
290
|
+
expect(screen.queryByTestId("subscription-status-banner")).not.toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("clears once the recovery lands — AC-3, no dismissal consulted", async () => {
|
|
294
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
295
|
+
ok: true,
|
|
296
|
+
body: { actionRequired: false, status: null, accessRevokesAt: null, portalAvailable: true },
|
|
297
|
+
});
|
|
298
|
+
await renderPage();
|
|
299
|
+
expect(screen.queryByTestId("subscription-status-banner")).not.toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it.each([401, 403, 404])(
|
|
303
|
+
"renders no banner and no throw on a %d read (AC-4, the existing SKIPPED_STATUSES constant)",
|
|
304
|
+
async (status) => {
|
|
305
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
306
|
+
ok: false,
|
|
307
|
+
status,
|
|
308
|
+
body: { error: { code: "n/a", message: "n/a" } },
|
|
309
|
+
});
|
|
310
|
+
await renderPage();
|
|
311
|
+
expect(screen.getByTestId("account-page")).toBeInTheDocument();
|
|
312
|
+
expect(screen.queryByTestId("subscription-status-banner")).not.toBeInTheDocument();
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
it("rejects to the route error boundary on a 500 subscription read", async () => {
|
|
317
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
318
|
+
ok: false,
|
|
319
|
+
status: 500,
|
|
320
|
+
body: { error: { code: "internal_error", message: "Boom" } },
|
|
321
|
+
});
|
|
322
|
+
await expect(Page({ params: Promise.resolve({ locale: "en" }) })).rejects.toThrow("Boom");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("renders the action slot when portalAvailable is true", async () => {
|
|
326
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
327
|
+
ok: true,
|
|
328
|
+
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: true },
|
|
329
|
+
});
|
|
330
|
+
await renderPage();
|
|
331
|
+
const banner = screen.getByTestId("subscription-status-banner");
|
|
332
|
+
expect(within(banner).getByRole("button")).toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("renders no action slot when portalAvailable is false — D3.0a", async () => {
|
|
336
|
+
mockSubscriptionMe.mockResolvedValue({
|
|
337
|
+
ok: true,
|
|
338
|
+
body: { actionRequired: true, status: "past_due", accessRevokesAt: null, portalAvailable: false },
|
|
339
|
+
});
|
|
340
|
+
await renderPage();
|
|
341
|
+
const banner = screen.getByTestId("subscription-status-banner");
|
|
342
|
+
expect(within(banner).queryByRole("button")).not.toBeInTheDocument();
|
|
343
|
+
});
|
|
344
|
+
});
|