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,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth's outbound-mail callbacks (#5962 § D3).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS AT ALL. `architecture.yaml`'s `apps/web` node denies
|
|
5
|
+
* `packages/email` in as many words ("email send is server-side only via
|
|
6
|
+
* apps/api"), and `tools/lint-architecture.ts` evaluates deny BEFORE allow, so
|
|
7
|
+
* an added allow entry would not clear it. These callbacks therefore serialize
|
|
8
|
+
* one body, sign it, and POST it to `apps/api`'s
|
|
9
|
+
* `/v1/auth/transactional-email` through the canonical client. `apps/web`
|
|
10
|
+
* sends no mail and imports no mail package.
|
|
11
|
+
*
|
|
12
|
+
* WHY IT IS NOT INLINE IN `auth.ts`. Three callbacks doing a credential-account
|
|
13
|
+
* lookup, a one-shot serialization, an HMAC and a bounded log line turn a
|
|
14
|
+
* declarative config file into a service module, and make themselves
|
|
15
|
+
* untestable without constructing a Better Auth instance. Each factory below
|
|
16
|
+
* takes its deps, so the suite next door exercises them with an injected
|
|
17
|
+
* transport and a prisma double.
|
|
18
|
+
*
|
|
19
|
+
* ONE SERIALIZATION. `buildAuthEmailRequest` builds the body string ONCE,
|
|
20
|
+
* signs THAT string, and hands the same string to the transport — the
|
|
21
|
+
* invariant `apps/api/src/services/revalidation.ts` states for itself ("a
|
|
22
|
+
* key-order or whitespace drift between signer and sender is impossible by
|
|
23
|
+
* construction"). This is why the call cannot go through the client's
|
|
24
|
+
* `post()`, which stringifies its own payload argument.
|
|
25
|
+
*
|
|
26
|
+
* PII BOUNDARY (§ D12). Every line carries `userId`, `kind`, `reason` and — on
|
|
27
|
+
* a failure — an error KIND. Never the recipient address, never the token,
|
|
28
|
+
* never the assembled link. The link IS the credential: one log line quoting
|
|
29
|
+
* it hands anyone with log access a live reset for an arbitrary account.
|
|
30
|
+
*
|
|
31
|
+
* NOTHING HERE THROWS. A reset send that fails must not fail the endpoint that
|
|
32
|
+
* already answered 200, and a failed § D17 notice must not fail the password
|
|
33
|
+
* change it reports.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { createHmac } from "node:crypto";
|
|
37
|
+
|
|
38
|
+
import { authEmail } from "@working-theory/api-client";
|
|
39
|
+
|
|
40
|
+
/** The three fixed kinds the relay will send. A caller picks no copy. */
|
|
41
|
+
export type AuthEmailKind =
|
|
42
|
+
| "password-reset"
|
|
43
|
+
| "email-verification"
|
|
44
|
+
| "password-changed";
|
|
45
|
+
|
|
46
|
+
export interface AuthEmailPayload {
|
|
47
|
+
kind: AuthEmailKind;
|
|
48
|
+
userId: string;
|
|
49
|
+
to: string;
|
|
50
|
+
url?: string;
|
|
51
|
+
expiresInMinutes?: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface AuthEmailRequest {
|
|
55
|
+
rawBody: string;
|
|
56
|
+
headers: Record<string, string>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The transport seam — `authEmail.send`'s shape, injectable for tests. */
|
|
60
|
+
export type AuthEmailTransport = (
|
|
61
|
+
rawBody: string,
|
|
62
|
+
headers: Record<string, string>,
|
|
63
|
+
) => Promise<unknown>;
|
|
64
|
+
|
|
65
|
+
/** The log seam — the two levels § D12's table uses. */
|
|
66
|
+
export interface AuthEmailLog {
|
|
67
|
+
warn: (fields: Record<string, unknown>) => void;
|
|
68
|
+
info: (fields: Record<string, unknown>) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The narrow slice of the Prisma client these callbacks touch.
|
|
73
|
+
*
|
|
74
|
+
* Declared with METHOD SHORTHAND rather than arrow properties, deliberately
|
|
75
|
+
* (code review). Under `strictFunctionTypes` a property-typed
|
|
76
|
+
* `(args: unknown) => …` is checked contravariantly, so the generated
|
|
77
|
+
* `PrismaClient` — whose delegates take their own narrow arg types — is not
|
|
78
|
+
* assignable to it, and every caller needs an `as unknown as` double
|
|
79
|
+
* assertion to get past that. Method shorthand is checked bivariantly, which
|
|
80
|
+
* is the correct reading here: this interface exists to NARROW the surface
|
|
81
|
+
* these callbacks may touch, not to accept a wider argument than Prisma does.
|
|
82
|
+
* The real client now satisfies it structurally and `auth.ts` passes it with
|
|
83
|
+
* no cast at all.
|
|
84
|
+
*/
|
|
85
|
+
export interface AuthEmailPrisma {
|
|
86
|
+
account: {
|
|
87
|
+
findFirst(args: unknown): Promise<{ id: string } | null>;
|
|
88
|
+
};
|
|
89
|
+
verification?: {
|
|
90
|
+
deleteMany(args: unknown): Promise<unknown>;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const defaultLog: AuthEmailLog = {
|
|
95
|
+
warn: (fields) => console.warn(`[auth-email] ${JSON.stringify(fields)}`),
|
|
96
|
+
info: (fields) => console.info(`[auth-email] ${JSON.stringify(fields)}`),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Builds the signed request for one message. The timestamp is inside the
|
|
101
|
+
* signed material, so it cannot be swapped independently of the body it
|
|
102
|
+
* authenticates — the replay window #5851's revalidate pair deliberately does
|
|
103
|
+
* without, closed here because a replayed EMAIL is not a no-op.
|
|
104
|
+
*/
|
|
105
|
+
export function buildAuthEmailRequest(
|
|
106
|
+
payload: AuthEmailPayload,
|
|
107
|
+
secret: string,
|
|
108
|
+
): AuthEmailRequest {
|
|
109
|
+
// ONE serialization. This exact string is both signed and sent.
|
|
110
|
+
const rawBody = JSON.stringify({
|
|
111
|
+
kind: payload.kind,
|
|
112
|
+
userId: payload.userId,
|
|
113
|
+
to: payload.to,
|
|
114
|
+
...(payload.url !== undefined ? { url: payload.url } : {}),
|
|
115
|
+
...(payload.expiresInMinutes !== undefined
|
|
116
|
+
? { expiresInMinutes: payload.expiresInMinutes }
|
|
117
|
+
: {}),
|
|
118
|
+
});
|
|
119
|
+
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
120
|
+
const signature = `sha256=${createHmac("sha256", secret)
|
|
121
|
+
.update(`${timestamp}.${rawBody}`, "utf8")
|
|
122
|
+
.digest("hex")}`;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
rawBody,
|
|
126
|
+
headers: {
|
|
127
|
+
"x-ailk-auth-email-signature": signature,
|
|
128
|
+
"x-ailk-auth-email-timestamp": timestamp,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
interface DispatchDeps {
|
|
134
|
+
send: AuthEmailTransport;
|
|
135
|
+
/** Defaults to a console logger bound to the § D12 field set. */
|
|
136
|
+
log?: AuthEmailLog;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Signs and posts one message, and swallows every failure into one log line.
|
|
141
|
+
* The relay answers 202 for an accepted send AND for one its per-recipient
|
|
142
|
+
* budget suppressed, so a caller here cannot tell the two apart — which is the
|
|
143
|
+
* point: an observable suppression is an account-enumeration oracle.
|
|
144
|
+
*/
|
|
145
|
+
async function dispatch(
|
|
146
|
+
payload: AuthEmailPayload,
|
|
147
|
+
{ send, log }: { send: AuthEmailTransport; log: AuthEmailLog },
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
const secret = process.env.AUTH_EMAIL_WEBHOOK_SECRET;
|
|
150
|
+
const base = { userId: payload.userId, kind: payload.kind };
|
|
151
|
+
|
|
152
|
+
if (!secret) {
|
|
153
|
+
// Never sign with an empty key: the relay would reject every delivery and
|
|
154
|
+
// the deployment would have no local signal at all.
|
|
155
|
+
log.warn({ ...base, reason: "not_configured" });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* SIGNING IS GUARDED TOO (#6068). This file's header promises "NOTHING HERE
|
|
161
|
+
* THROWS", and `auth.ts`'s `onPasswordReset` relies on that promise in as
|
|
162
|
+
* many words — but the build-and-sign step used to sit OUTSIDE the try
|
|
163
|
+
* below, so anything `buildAuthEmailRequest` threw propagated straight out
|
|
164
|
+
* of a callback that runs AFTER the password has already changed and the
|
|
165
|
+
* presented token has already been consumed. #6061 made that callback
|
|
166
|
+
* unconditional, so it now runs in degraded configurations, which is exactly
|
|
167
|
+
* where a signer is least likely to be healthy.
|
|
168
|
+
*
|
|
169
|
+
* Its own `try` rather than a widened one, so the log line still names the
|
|
170
|
+
* STAGE that failed: a signer that cannot run is an operator's problem with
|
|
171
|
+
* this deployment, not the relay's, and `transport_error` would send whoever
|
|
172
|
+
* reads the line looking in the wrong place.
|
|
173
|
+
*/
|
|
174
|
+
let request: AuthEmailRequest;
|
|
175
|
+
try {
|
|
176
|
+
request = buildAuthEmailRequest(payload, secret);
|
|
177
|
+
} catch {
|
|
178
|
+
// The KIND only — see this file's PII boundary note.
|
|
179
|
+
log.warn({ ...base, reason: "send_failed", errorKind: "signing_failed" });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const result = (await send(request.rawBody, request.headers)) as
|
|
185
|
+
| { ok: boolean; status?: number }
|
|
186
|
+
| undefined;
|
|
187
|
+
if (result && result.ok === false) {
|
|
188
|
+
// The STATUS, never the response body — the relay's own error text is
|
|
189
|
+
// not guaranteed free of the address it was given.
|
|
190
|
+
log.warn({ ...base, reason: "send_failed", errorKind: "relay_rejected" });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
log.info({ ...base, reason: "sent" });
|
|
194
|
+
} catch {
|
|
195
|
+
// The error's NAME would be safe; its message is not guaranteed to be.
|
|
196
|
+
log.warn({ ...base, reason: "send_failed", errorKind: "transport_error" });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface ResetPasswordDeps extends DispatchDeps {
|
|
201
|
+
prisma: AuthEmailPrisma;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Better Auth's `sendResetPassword` (§ D14).
|
|
206
|
+
*
|
|
207
|
+
* The credential-account check is the load-bearing part. `password.mjs`
|
|
208
|
+
* CREATES a `credential` account on reset if the user has none — so combined
|
|
209
|
+
* with § D2, which enables the password method on `AUTH_EMAIL_SERVER` +
|
|
210
|
+
* `AUTH_EMAIL_FROM`, an existing Google-or-GitHub-only user who completed a
|
|
211
|
+
* reset would silently gain a password path around their identity provider
|
|
212
|
+
* and whatever MFA it enforces. No credential account means no mail.
|
|
213
|
+
*
|
|
214
|
+
* The skip is SILENT — same 200, same body, log only. The visitor-facing
|
|
215
|
+
* answer must not distinguish "this address has no password" from "this
|
|
216
|
+
* address does not exist": the second is the enumeration AC-1 forbids and the
|
|
217
|
+
* first is nearly as useful to an attacker, since it maps a site's users to
|
|
218
|
+
* their identity provider.
|
|
219
|
+
*/
|
|
220
|
+
export function makeSendResetPassword(deps: ResetPasswordDeps) {
|
|
221
|
+
const { prisma, send } = deps;
|
|
222
|
+
const log = deps.log ?? defaultLog;
|
|
223
|
+
return async function sendResetPassword({
|
|
224
|
+
user,
|
|
225
|
+
url,
|
|
226
|
+
}: {
|
|
227
|
+
user: { id: string; email: string };
|
|
228
|
+
url: string;
|
|
229
|
+
}): Promise<void> {
|
|
230
|
+
let hasPassword: { id: string } | null;
|
|
231
|
+
try {
|
|
232
|
+
hasPassword = await prisma.account.findFirst({
|
|
233
|
+
where: { userId: user.id, providerId: "credential" },
|
|
234
|
+
select: { id: true },
|
|
235
|
+
});
|
|
236
|
+
} catch {
|
|
237
|
+
log.warn({
|
|
238
|
+
userId: user.id,
|
|
239
|
+
kind: "password-reset",
|
|
240
|
+
reason: "send_failed",
|
|
241
|
+
errorKind: "lookup_failed",
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!hasPassword) {
|
|
247
|
+
log.warn({
|
|
248
|
+
userId: user.id,
|
|
249
|
+
kind: "password-reset",
|
|
250
|
+
reason: "no_credential_account",
|
|
251
|
+
});
|
|
252
|
+
return; // no send; the endpoint's 200 body is unchanged
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
await dispatch(
|
|
256
|
+
{
|
|
257
|
+
kind: "password-reset",
|
|
258
|
+
userId: user.id,
|
|
259
|
+
to: user.email,
|
|
260
|
+
url,
|
|
261
|
+
expiresInMinutes: 60,
|
|
262
|
+
},
|
|
263
|
+
{ send, log },
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Better Auth's `sendVerificationEmail` (§ D9). */
|
|
269
|
+
export function makeSendVerificationEmail(deps: DispatchDeps) {
|
|
270
|
+
const { send } = deps;
|
|
271
|
+
const log = deps.log ?? defaultLog;
|
|
272
|
+
return async function sendVerificationEmail({
|
|
273
|
+
user,
|
|
274
|
+
url,
|
|
275
|
+
}: {
|
|
276
|
+
user: { id: string; email: string };
|
|
277
|
+
url: string;
|
|
278
|
+
}): Promise<void> {
|
|
279
|
+
await dispatch(
|
|
280
|
+
{
|
|
281
|
+
kind: "email-verification",
|
|
282
|
+
userId: user.id,
|
|
283
|
+
to: user.email,
|
|
284
|
+
url,
|
|
285
|
+
expiresInMinutes: 24 * 60,
|
|
286
|
+
},
|
|
287
|
+
{ send, log },
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The completed-reset notice (§ D17).
|
|
294
|
+
*
|
|
295
|
+
* § D2 leaves `requireEmailVerification: false` and § D7 revokes every session
|
|
296
|
+
* on reset, so a takeover through a compromised mailbox is otherwise silent
|
|
297
|
+
* and total: the owner's sessions vanish and no message ever reaches them.
|
|
298
|
+
* This notice is the only signal the owner gets. It carries no link and no
|
|
299
|
+
* token — the payload has no `url` — so the message reporting a takeover
|
|
300
|
+
* cannot hand the attacker a second credential.
|
|
301
|
+
*/
|
|
302
|
+
export function makeSendPasswordChangedNotice(deps: DispatchDeps) {
|
|
303
|
+
const { send } = deps;
|
|
304
|
+
const log = deps.log ?? defaultLog;
|
|
305
|
+
return async function sendPasswordChangedNotice(user: {
|
|
306
|
+
id: string;
|
|
307
|
+
email: string;
|
|
308
|
+
}): Promise<void> {
|
|
309
|
+
await dispatch(
|
|
310
|
+
{ kind: "password-changed", userId: user.id, to: user.email },
|
|
311
|
+
{ send, log },
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The production transport — the canonical client, never a bare fetch. */
|
|
317
|
+
export const authEmailTransport: AuthEmailTransport = (rawBody, headers) =>
|
|
318
|
+
authEmail.send(rawBody, headers);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for:
|
|
5
5
|
* - AILK_AUTH_REQUIRED flag (whether protected routes redirect unauthenticated users)
|
|
6
|
+
* - AILK_AUTH_SIGNUP_OPEN / AILK_AUTH_PASSWORD_RESET flags (#5963) — whether
|
|
7
|
+
* the /sign-up and /forgot-password|/reset-password surfaces offer a flow
|
|
6
8
|
* - Active provider list (which Better Auth providers are activated by env vars)
|
|
7
9
|
*
|
|
8
10
|
* No better-auth import — this module is safe to require in tests without the
|
|
@@ -24,6 +26,32 @@ export function isAuthRequired(): boolean {
|
|
|
24
26
|
return val === "1" || val === "true";
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Returns true when AILK_AUTH_SIGNUP_OPEN is set to "1" or "true" (#5963).
|
|
31
|
+
* Default (unset or "0"/"false") = sign-up is CLOSED, which is what a
|
|
32
|
+
* scaffold actually is: `apps/web/auth.ts` does not enable
|
|
33
|
+
* `emailAndPassword`, so there is no public sign-up flow to open. The
|
|
34
|
+
* `/sign-up` page renders the `invitation` intent while this is false.
|
|
35
|
+
*
|
|
36
|
+
* This is the seam #5962 flips once the server-side flow is real.
|
|
37
|
+
*/
|
|
38
|
+
export function isSignUpOpen(): boolean {
|
|
39
|
+
const val = process.env.AILK_AUTH_SIGNUP_OPEN;
|
|
40
|
+
return val === "1" || val === "true";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns true when AILK_AUTH_PASSWORD_RESET is set to "1" or "true" (#5963).
|
|
45
|
+
* Default (unset or "0"/"false") = password reset is NOT available — again
|
|
46
|
+
* the honest reading of a scaffold, where `emailAndPassword` is unconfigured.
|
|
47
|
+
* `/forgot-password` and `/reset-password` render their not-available state
|
|
48
|
+
* while this is false; #5962 lands the flow behind it.
|
|
49
|
+
*/
|
|
50
|
+
export function isResetAvailable(): boolean {
|
|
51
|
+
const val = process.env.AILK_AUTH_PASSWORD_RESET;
|
|
52
|
+
return val === "1" || val === "true";
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
/**
|
|
28
56
|
* Returns the list of provider IDs whose required env vars are all present.
|
|
29
57
|
* A provider is only included when every one of its required vars is non-empty.
|
|
@@ -64,3 +92,60 @@ export function getActiveProviderIds(): string[] {
|
|
|
64
92
|
export function hasAnyProvider(): boolean {
|
|
65
93
|
return getActiveProviderIds().length > 0;
|
|
66
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* True when this deployment can actually complete a password reset end to end,
|
|
98
|
+
* SAFELY (#5962 § D10). Five conjuncts, each of which independently makes the
|
|
99
|
+
* flow impossible or unsafe:
|
|
100
|
+
*
|
|
101
|
+
* AILK_AUTH_PASSWORD_RESET the operator's own opt-in (#5963 D5, read here
|
|
102
|
+
* through isResetAvailable()). Default off.
|
|
103
|
+
* the password METHOD `emailAndPassword` is enabled on exactly this
|
|
104
|
+
* predicate (#5962 D2); with only OAuth
|
|
105
|
+
* configured there is no password to reset.
|
|
106
|
+
* a send SEAM AUTH_EMAIL_WEBHOOK_SECRET + AUTH_EMAIL_FROM —
|
|
107
|
+
* without the secret the apps/api relay answers
|
|
108
|
+
* 503 and a visitor would be told "check your
|
|
109
|
+
* email" for mail that will never be sent.
|
|
110
|
+
* BETTER_AUTH_URL unset means Better Auth derives the emitted
|
|
111
|
+
* link's host AND the redirectTo allow-list from
|
|
112
|
+
* the incoming REQUEST (§ D13). A reset token
|
|
113
|
+
* minted under a request-derived origin is a
|
|
114
|
+
* token an attacker can aim at itself.
|
|
115
|
+
* BETTER_AUTH_SECRET unset means auth.ts's dev fallback — a string
|
|
116
|
+
* published in this repository — signs the
|
|
117
|
+
* verification JWTs and the session cookies.
|
|
118
|
+
*
|
|
119
|
+
* WHY THIS IS A SECOND NAME RATHER THAN A WIDENED `isResetAvailable()`.
|
|
120
|
+
* #5962's spec § D10 widens `isResetAvailable()` itself and makes this an
|
|
121
|
+
* alias of it. That is not takeable as written: #5963 landed
|
|
122
|
+
* `apps/web/__tests__/auth/env-helpers.test.ts`, which asserts
|
|
123
|
+
* `isResetAvailable()` is true on the flag ALONE, and #5962's AC-3 requires
|
|
124
|
+
* every existing auth test to pass UNEDITED. So the conjunction lives here,
|
|
125
|
+
* under its own name, and `isResetAvailable()` keeps the contract #5963
|
|
126
|
+
* shipped — one conjunct of this one.
|
|
127
|
+
*
|
|
128
|
+
* The consequence, stated rather than hidden: on a deployment that sets the
|
|
129
|
+
* flag but not the transport, `/forgot-password` renders the form rather than
|
|
130
|
+
* the not-available card, and a submit lands on the error state instead —
|
|
131
|
+
* because the server refuses to send. That is a degraded message, not a false
|
|
132
|
+
* promise: the card only swaps to "check your email" on a successful
|
|
133
|
+
* response, so no visitor is ever told mail is coming when none can be sent.
|
|
134
|
+
*
|
|
135
|
+
* `RESEND_API_KEY` is deliberately NOT a conjunct: it is `apps/api`'s
|
|
136
|
+
* variable, not `apps/web`'s, and `apps/web` cannot read it. The relay answers
|
|
137
|
+
* 503 when it is absent, which lands as a `send_failed` log line rather than a
|
|
138
|
+
* page state.
|
|
139
|
+
*
|
|
140
|
+
* No better-auth import: this module stays require-safe in tests, exactly as
|
|
141
|
+
* its own header promises.
|
|
142
|
+
*/
|
|
143
|
+
export function resetSendConfigured(): boolean {
|
|
144
|
+
if (!isResetAvailable()) return false;
|
|
145
|
+
if (!getActiveProviderIds().includes("email")) return false;
|
|
146
|
+
if (!process.env.AUTH_EMAIL_WEBHOOK_SECRET || !process.env.AUTH_EMAIL_FROM) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (!process.env.BETTER_AUTH_URL) return false;
|
|
150
|
+
return Boolean(process.env.BETTER_AUTH_SECRET);
|
|
151
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The auth route group's segment list and its path predicate (#5963).
|
|
3
|
+
*
|
|
4
|
+
* ONE module, so the list `app/[locale]/layout.tsx` branches on to drop the
|
|
5
|
+
* marketing chrome and the list the `(auth)` group's page segments exist at
|
|
6
|
+
* cannot drift. A route group contributes no URL segment, so these are the
|
|
7
|
+
* real paths: `/sign-in`, `/sign-up`, `/forgot-password`, `/reset-password`
|
|
8
|
+
* and `/verify-email`, each under next-intl's `as-needed` prefixing (bare for
|
|
9
|
+
* the default locale, `/<locale>/…` otherwise).
|
|
10
|
+
*
|
|
11
|
+
* No Next.js import — a pure function, asserted directly by its own suite.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The five segments the `(auth)` route group serves. */
|
|
15
|
+
export const AUTH_SEGMENTS = [
|
|
16
|
+
"sign-in",
|
|
17
|
+
"sign-up",
|
|
18
|
+
"forgot-password",
|
|
19
|
+
"reset-password",
|
|
20
|
+
"verify-email",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type AuthSegment = (typeof AUTH_SEGMENTS)[number];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether `pathname` addresses one of the five auth routes.
|
|
27
|
+
*
|
|
28
|
+
* Locale-prefix-aware the same way `resolveLocalePath()` is: strip a leading
|
|
29
|
+
* `/<locale>` when it matches an ENABLED locale, then compare the first
|
|
30
|
+
* remaining segment against the list. A prefix that is not an enabled locale
|
|
31
|
+
* is a first segment, not a prefix.
|
|
32
|
+
*
|
|
33
|
+
* Fail-safe direction: an absent or empty pathname answers `false`. The
|
|
34
|
+
* layout's branch reads a header that can legitimately be missing (a direct
|
|
35
|
+
* render in a unit test, a path the middleware matcher excludes), and a
|
|
36
|
+
* missing header must never strip a marketing page's chrome. The worst case
|
|
37
|
+
* is an auth page that renders WITH chrome, which the group's own tests catch.
|
|
38
|
+
*/
|
|
39
|
+
export function isAuthPath(
|
|
40
|
+
pathname: string | undefined | null,
|
|
41
|
+
locales: readonly string[],
|
|
42
|
+
): boolean {
|
|
43
|
+
if (!pathname) return false;
|
|
44
|
+
|
|
45
|
+
// Drop a query string / fragment — `x-pathname` carries the path, but a
|
|
46
|
+
// caller passing a full request path should get the same verdict.
|
|
47
|
+
const path = pathname.split(/[?#]/)[0] ?? "";
|
|
48
|
+
const bare =
|
|
49
|
+
locales.length > 0
|
|
50
|
+
? path.replace(new RegExp(`^/(${locales.join("|")})(?=/|$)`), "")
|
|
51
|
+
: path;
|
|
52
|
+
|
|
53
|
+
const first = bare.split("/")[1];
|
|
54
|
+
return AUTH_SEGMENTS.some((segment) => segment === first);
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* enabledMeetingSlugs — the enabled-meeting-slugs read both `/meet` pages
|
|
4
|
+
* share (#6183 S9b D5).
|
|
5
|
+
*
|
|
6
|
+
* Resolves `GET /v1/schedule/meeting-types`'s three D4 outcomes into ONE
|
|
7
|
+
* array both pages consume identically:
|
|
8
|
+
*
|
|
9
|
+
* - an array is returned as-is (a real, operator-written enabled list —
|
|
10
|
+
* D4 rows 2 and 3, INCLUDING an intentionally empty `[]`);
|
|
11
|
+
* - `null` becomes the D4 row-1 singleton derived from
|
|
12
|
+
* `SCHEDULING_CAL_LINK` — the ONE slug an unconfigured deployment has
|
|
13
|
+
* ever advertised — or `[]` when that env var is unset;
|
|
14
|
+
* - any non-`ok` API result becomes `[]`, fail closed: an API outage
|
|
15
|
+
* must not open the gate.
|
|
16
|
+
*
|
|
17
|
+
* A duplicated inline call in both pages is how the two would drift, and
|
|
18
|
+
* it is also how one of them would forget the fail-closed arm — this is
|
|
19
|
+
* the ONE place that reasoning lives (D5).
|
|
20
|
+
*/
|
|
21
|
+
import { schedule } from "@working-theory/api-client";
|
|
22
|
+
import { MeetingSlugSchema } from "@working-theory/site-config/schema";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The D4 row-1 fallback: the slug half of the env-configured booker's own
|
|
26
|
+
* calLink (`"<owner>/<slug>"`), or `[]` when the env var is unset. This is
|
|
27
|
+
* deliberately the ONLY reader of `SCHEDULING_CAL_LINK`'s SECOND segment —
|
|
28
|
+
* `meet/[slug]/page.tsx`'s own `composedCalLink` reads the FIRST segment
|
|
29
|
+
* (the owner) for an unrelated purpose and is not touched by this file.
|
|
30
|
+
*
|
|
31
|
+
* Re-validated against `MeetingSlugSchema` (D14 parity with the operator-
|
|
32
|
+
* written path — `meeting-types.ts`'s own per-entry `safeParse` filter):
|
|
33
|
+
* `SCHEDULING_CAL_LINK` is operator-set env config, not a schema-validated
|
|
34
|
+
* write, so a malformed value (a stray comma, an empty second segment) could
|
|
35
|
+
* otherwise yield a slug that corrupts the batched `event-meta` `calLinks`
|
|
36
|
+
* join downstream — which splits on comma and would misattribute the rest
|
|
37
|
+
* of the list to a foreign owner (security review, 2026-09-19).
|
|
38
|
+
*/
|
|
39
|
+
function derivedFallbackSlugs(): string[] {
|
|
40
|
+
const configured = process.env["SCHEDULING_CAL_LINK"];
|
|
41
|
+
if (!configured) return [];
|
|
42
|
+
const slug = configured.split("/")[1];
|
|
43
|
+
if (!slug) return [];
|
|
44
|
+
const parsed = MeetingSlugSchema.safeParse(slug);
|
|
45
|
+
return parsed.success ? [parsed.data] : [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The enabled meeting-type slug set for this deployment, D4-resolved. */
|
|
49
|
+
export async function enabledMeetingSlugs(): Promise<string[]> {
|
|
50
|
+
const res = await schedule.meetingTypes();
|
|
51
|
+
if (!res.ok) return [];
|
|
52
|
+
return res.body.slugs ?? derivedFallbackSlugs();
|
|
53
|
+
}
|
|
@@ -108,14 +108,10 @@ export const SITE_BASE_URL =
|
|
|
108
108
|
process.env.NEXT_PUBLIC_BASE_URL ?? siteFrontmatter?.baseUrl ?? FALLBACK_BASE_URL;
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
|
-
* Shared brand mark — the
|
|
112
|
-
* `
|
|
111
|
+
* Shared brand mark — the square logomark (the central brand library's
|
|
112
|
+
* `logomark/loop/icon.svg`). The Nav pairs it with `/wordmark.svg`.
|
|
113
|
+
* RSC-safe (no hooks, no client state).
|
|
113
114
|
*/
|
|
114
115
|
export function siteLogo(): ReactElement {
|
|
115
|
-
return
|
|
116
|
-
<>
|
|
117
|
-
<img src="/logo-light.svg" alt={SITE_NAME} height={28} className="block dark:hidden" />
|
|
118
|
-
<img src="/logo-dark.svg" alt={SITE_NAME} height={28} className="hidden dark:block" />
|
|
119
|
-
</>
|
|
120
|
-
);
|
|
116
|
+
return <img src="/logomark.svg" alt={SITE_NAME} width={28} height={28} className="block" />;
|
|
121
117
|
}
|
|
@@ -5,10 +5,14 @@
|
|
|
5
5
|
// `page_location` default and into Sentry events, transactions, and session
|
|
6
6
|
// replays. This is the single source of the stripped-parameter list.
|
|
7
7
|
//
|
|
8
|
+
// `session_id` joined by #6084 (D15): the Checkout Session id riding
|
|
9
|
+
// `/purchase/success?session_id=cs_…` is the SAME class of bearer capability
|
|
10
|
+
// — the exact defect #5562 fixed for `?g=`, recurring on a second surface.
|
|
11
|
+
//
|
|
8
12
|
// Mirrored (duplicated, not shared) in packages/analytics/src/scrub.ts,
|
|
9
13
|
// since apps/* must not import @working-theory/* packages — keep both lists
|
|
10
14
|
// in sync (#5613).
|
|
11
|
-
export const TOKEN_QUERY_PARAMS = ["g"];
|
|
15
|
+
export const TOKEN_QUERY_PARAMS = ["g", "session_id"];
|
|
12
16
|
|
|
13
17
|
// Sentry navigation breadcrumbs (`data.to`/`data.from`) are relative
|
|
14
18
|
// (`path?search`, no origin) rather than the absolute URLs GA and
|