create-ailk 0.5.0 → 0.6.0
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/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- 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/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/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -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/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/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/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__/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/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- 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 +61 -0
- 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 +142 -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 +1 -1
- 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/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,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file auth-email.test.ts — Better Auth's three mail callbacks (#5962 § D3).
|
|
3
|
+
*
|
|
4
|
+
* Written before the implementation. The transport is INJECTED, so nothing
|
|
5
|
+
* here mocks an internal module and nothing here sends mail.
|
|
6
|
+
*
|
|
7
|
+
* What the suite pins, in the order the design depends on it:
|
|
8
|
+
* - ONE serialization: the bytes that are signed are the bytes that are
|
|
9
|
+
* sent. A key-order or whitespace drift between signer and sender is the
|
|
10
|
+
* whole failure mode the raw-string seam exists to make impossible.
|
|
11
|
+
* - the signature is HMAC-SHA256 over `<timestamp>.<body>`, so the
|
|
12
|
+
* timestamp cannot be swapped independently of the body it authenticates.
|
|
13
|
+
* - the credential-account check (§ D14): a user with no `credential`
|
|
14
|
+
* account gets NO reset mail, and the suppression is silent — same return,
|
|
15
|
+
* no throw, log-only. An OAuth-only user who completes a reset would
|
|
16
|
+
* otherwise silently gain a password path around their IdP.
|
|
17
|
+
* - the § D12 PII boundary on every log line: a userId, a kind, a reason and
|
|
18
|
+
* an error KIND. Never the address, never the token, never the link.
|
|
19
|
+
* - nothing throws. A failed notification must never fail the password
|
|
20
|
+
* change it reports.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { AuthEmailLog } from "../auth-email";
|
|
24
|
+
import {
|
|
25
|
+
buildAuthEmailRequest,
|
|
26
|
+
makeSendResetPassword,
|
|
27
|
+
makeSendVerificationEmail,
|
|
28
|
+
makeSendPasswordChangedNotice,
|
|
29
|
+
} from "../auth-email";
|
|
30
|
+
|
|
31
|
+
const ORIGINAL_ENV = process.env;
|
|
32
|
+
|
|
33
|
+
type LogLine = { level: string; fields: Record<string, unknown> };
|
|
34
|
+
|
|
35
|
+
function makeLog(): { lines: LogLine[]; log: AuthEmailLog } {
|
|
36
|
+
const lines: LogLine[] = [];
|
|
37
|
+
const record =
|
|
38
|
+
(level: string) =>
|
|
39
|
+
(fields: Record<string, unknown>): void => {
|
|
40
|
+
lines.push({ level, fields });
|
|
41
|
+
};
|
|
42
|
+
return { lines, log: { warn: record("warn"), info: record("info") } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A prisma double exposing only what these callbacks touch. */
|
|
46
|
+
function makePrisma(hasCredential: boolean) {
|
|
47
|
+
return {
|
|
48
|
+
account: {
|
|
49
|
+
findFirst: jest.fn().mockResolvedValue(
|
|
50
|
+
hasCredential ? { id: "acct_1" } : null,
|
|
51
|
+
),
|
|
52
|
+
},
|
|
53
|
+
verification: { deleteMany: jest.fn().mockResolvedValue({ count: 2 }) },
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
process.env = { ...ORIGINAL_ENV };
|
|
59
|
+
process.env.AUTH_EMAIL_WEBHOOK_SECRET = "webhook-secret";
|
|
60
|
+
process.env.AUTH_EMAIL_FROM = "noreply@site.example";
|
|
61
|
+
process.env.BETTER_AUTH_URL = "https://site.example";
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(() => {
|
|
65
|
+
process.env = ORIGINAL_ENV;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("buildAuthEmailRequest()", () => {
|
|
69
|
+
it("signs the EXACT string it returns as the body", () => {
|
|
70
|
+
const req = buildAuthEmailRequest(
|
|
71
|
+
{ kind: "password-reset", userId: "user_1", to: "a@b.test", url: "https://site.example/r" },
|
|
72
|
+
"webhook-secret",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const { createHmac } = require("node:crypto") as typeof import("node:crypto");
|
|
76
|
+
const timestamp = req.headers["x-ailk-auth-email-timestamp"];
|
|
77
|
+
const expected = `sha256=${createHmac("sha256", "webhook-secret")
|
|
78
|
+
.update(`${timestamp}.${req.rawBody}`, "utf8")
|
|
79
|
+
.digest("hex")}`;
|
|
80
|
+
|
|
81
|
+
expect(req.headers["x-ailk-auth-email-signature"]).toBe(expected);
|
|
82
|
+
// And the body really is a single string, not an object re-serialized later.
|
|
83
|
+
expect(typeof req.rawBody).toBe("string");
|
|
84
|
+
expect(JSON.parse(req.rawBody)).toMatchObject({
|
|
85
|
+
kind: "password-reset",
|
|
86
|
+
userId: "user_1",
|
|
87
|
+
to: "a@b.test",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("puts a current unix-seconds timestamp inside the signed material", () => {
|
|
92
|
+
const req = buildAuthEmailRequest(
|
|
93
|
+
{ kind: "password-changed", userId: "user_1", to: "a@b.test" },
|
|
94
|
+
"webhook-secret",
|
|
95
|
+
);
|
|
96
|
+
const ts = Number(req.headers["x-ailk-auth-email-timestamp"]);
|
|
97
|
+
expect(Math.abs(Math.floor(Date.now() / 1000) - ts)).toBeLessThan(5);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe("sendResetPassword (§ D14 — the credential-account check)", () => {
|
|
102
|
+
it("sends for a user who HAS a credential account", async () => {
|
|
103
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
104
|
+
const { lines, log } = makeLog();
|
|
105
|
+
const prisma = makePrisma(true);
|
|
106
|
+
|
|
107
|
+
const cb = makeSendResetPassword({ prisma, send, log });
|
|
108
|
+
await cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/api/auth/reset-password/tok" });
|
|
109
|
+
|
|
110
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(lines.some((l) => l.fields.reason === "sent")).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("sends NOTHING for a user with no credential account, and does not throw", async () => {
|
|
115
|
+
const send = jest.fn();
|
|
116
|
+
const { lines, log } = makeLog();
|
|
117
|
+
const prisma = makePrisma(false);
|
|
118
|
+
|
|
119
|
+
const cb = makeSendResetPassword({ prisma, send, log });
|
|
120
|
+
await expect(
|
|
121
|
+
cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/x" }),
|
|
122
|
+
).resolves.toBeUndefined();
|
|
123
|
+
|
|
124
|
+
expect(send).not.toHaveBeenCalled();
|
|
125
|
+
expect(lines.some((l) => l.fields.reason === "no_credential_account")).toBe(
|
|
126
|
+
true,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("never throws when the transport fails", async () => {
|
|
131
|
+
const send = jest.fn().mockRejectedValue(new Error("network"));
|
|
132
|
+
const { lines, log } = makeLog();
|
|
133
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
|
|
134
|
+
|
|
135
|
+
await expect(
|
|
136
|
+
cb({ user: { id: "user_1", email: "p@e.test" }, url: "https://site.example/x" }),
|
|
137
|
+
).resolves.toBeUndefined();
|
|
138
|
+
expect(lines.some((l) => l.fields.reason === "send_failed")).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("logs nothing when the relay secret is absent — it does not send blind", async () => {
|
|
142
|
+
delete process.env.AUTH_EMAIL_WEBHOOK_SECRET;
|
|
143
|
+
const send = jest.fn();
|
|
144
|
+
const { lines, log } = makeLog();
|
|
145
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
|
|
146
|
+
|
|
147
|
+
await cb({ user: { id: "user_1", email: "p@e.test" }, url: "https://site.example/x" });
|
|
148
|
+
expect(send).not.toHaveBeenCalled();
|
|
149
|
+
expect(lines.some((l) => l.fields.reason === "not_configured")).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("the § D12 PII boundary", () => {
|
|
154
|
+
it("never logs the address, the token or the assembled link", async () => {
|
|
155
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
156
|
+
const { lines, log } = makeLog();
|
|
157
|
+
const url =
|
|
158
|
+
"https://site.example/api/auth/reset-password/SECRETTOKEN?callbackURL=%2Freset-password";
|
|
159
|
+
|
|
160
|
+
const reset = makeSendResetPassword({ prisma: makePrisma(true), send, log });
|
|
161
|
+
await reset({ user: { id: "user_1", email: "person@example.com" }, url });
|
|
162
|
+
|
|
163
|
+
const verify = makeSendVerificationEmail({ send, log });
|
|
164
|
+
await verify({ user: { id: "user_1", email: "person@example.com" }, url });
|
|
165
|
+
|
|
166
|
+
const notice = makeSendPasswordChangedNotice({ send, log });
|
|
167
|
+
await notice({ id: "user_1", email: "person@example.com" });
|
|
168
|
+
|
|
169
|
+
const serialized = JSON.stringify(lines);
|
|
170
|
+
expect(serialized).not.toContain("person@example.com");
|
|
171
|
+
expect(serialized).not.toContain("SECRETTOKEN");
|
|
172
|
+
expect(serialized).not.toContain("site.example");
|
|
173
|
+
// What it DOES carry.
|
|
174
|
+
expect(serialized).toContain("user_1");
|
|
175
|
+
expect(serialized).toContain("password-reset");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("carries only the tagged error KIND, never a message that can quote an address", async () => {
|
|
179
|
+
const send = jest.fn().mockResolvedValue({
|
|
180
|
+
ok: false,
|
|
181
|
+
status: 400,
|
|
182
|
+
body: { error: "validation_failed for person@example.com" },
|
|
183
|
+
});
|
|
184
|
+
const { lines, log } = makeLog();
|
|
185
|
+
const cb = makeSendResetPassword({ prisma: makePrisma(true), send, log });
|
|
186
|
+
|
|
187
|
+
await cb({ user: { id: "user_1", email: "person@example.com" }, url: "https://site.example/x" });
|
|
188
|
+
expect(JSON.stringify(lines)).not.toContain("person@example.com");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("makeSendPasswordChangedNotice (§ D17)", () => {
|
|
193
|
+
it("sends a password-changed body carrying no url", async () => {
|
|
194
|
+
const send = jest.fn().mockResolvedValue({ ok: true, body: { ok: true } });
|
|
195
|
+
const { log } = makeLog();
|
|
196
|
+
const notice = makeSendPasswordChangedNotice({ send, log });
|
|
197
|
+
|
|
198
|
+
await notice({ id: "user_1", email: "person@example.com" });
|
|
199
|
+
|
|
200
|
+
const [rawBody] = send.mock.calls[0];
|
|
201
|
+
const parsed = JSON.parse(rawBody);
|
|
202
|
+
expect(parsed.kind).toBe("password-changed");
|
|
203
|
+
expect(parsed).not.toHaveProperty("url");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("never throws — a failed notice must not fail the password change", async () => {
|
|
207
|
+
const send = jest.fn().mockRejectedValue(new Error("down"));
|
|
208
|
+
const { log } = makeLog();
|
|
209
|
+
const notice = makeSendPasswordChangedNotice({ send, log });
|
|
210
|
+
await expect(
|
|
211
|
+
notice({ id: "user_1", email: "person@example.com" }),
|
|
212
|
+
).resolves.toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the auth-route predicate (#5963).
|
|
3
|
+
*
|
|
4
|
+
* `isAuthPath()` is the ONE list the locale layout branches on and the five
|
|
5
|
+
* `(auth)` page segments are named from, so the layout and the routes cannot
|
|
6
|
+
* drift. Covers bare paths, locale-prefixed paths, near misses, nested paths,
|
|
7
|
+
* and the absent-header fail-safe.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AUTH_SEGMENTS, isAuthPath } from "../auth-routes";
|
|
11
|
+
|
|
12
|
+
const LOCALES = ["en", "fr", "de"];
|
|
13
|
+
|
|
14
|
+
describe("AUTH_SEGMENTS", () => {
|
|
15
|
+
it("names exactly the five auth routes", () => {
|
|
16
|
+
expect([...AUTH_SEGMENTS]).toEqual([
|
|
17
|
+
"sign-in",
|
|
18
|
+
"sign-up",
|
|
19
|
+
"forgot-password",
|
|
20
|
+
"reset-password",
|
|
21
|
+
"verify-email",
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("isAuthPath", () => {
|
|
27
|
+
it.each(AUTH_SEGMENTS)("matches the bare /%s path", (segment) => {
|
|
28
|
+
expect(isAuthPath(`/${segment}`, LOCALES)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it.each(AUTH_SEGMENTS)("matches the locale-prefixed /fr/%s path", (segment) => {
|
|
32
|
+
expect(isAuthPath(`/fr/${segment}`, LOCALES)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("matches a trailing-slash auth path", () => {
|
|
36
|
+
expect(isAuthPath("/sign-in/", LOCALES)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("matches an auth path carrying a query string", () => {
|
|
40
|
+
expect(isAuthPath("/sign-in?next=/account", LOCALES)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("matches a nested path under an auth segment", () => {
|
|
44
|
+
expect(isAuthPath("/reset-password/confirm", LOCALES)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does not match a near miss", () => {
|
|
48
|
+
expect(isAuthPath("/sign-in-x", LOCALES)).toBe(false);
|
|
49
|
+
expect(isAuthPath("/sign", LOCALES)).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("does not match a marketing path", () => {
|
|
53
|
+
expect(isAuthPath("/pricing", LOCALES)).toBe(false);
|
|
54
|
+
expect(isAuthPath("/fr/pricing", LOCALES)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("does not match the locale root or the site root", () => {
|
|
58
|
+
expect(isAuthPath("/", LOCALES)).toBe(false);
|
|
59
|
+
expect(isAuthPath("/fr", LOCALES)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not strip a prefix that is not an enabled locale", () => {
|
|
63
|
+
// `/es` is not enabled here, so `es` is the first segment, not a prefix.
|
|
64
|
+
expect(isAuthPath("/es/sign-in", LOCALES)).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("fails to NOT-an-auth-path for an empty or absent pathname", () => {
|
|
68
|
+
// The fail-safe direction (§ D1): an absent `x-pathname` must never strip
|
|
69
|
+
// a marketing page's chrome. The worst case is an auth page WITH chrome,
|
|
70
|
+
// which the (auth) group's own tests catch.
|
|
71
|
+
expect(isAuthPath("", LOCALES)).toBe(false);
|
|
72
|
+
expect(isAuthPath(undefined, LOCALES)).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* Tests for enabledMeetingSlugs (#6183 S9b D5) — the D4-resolving read both
|
|
4
|
+
* /meet pages share.
|
|
5
|
+
*/
|
|
6
|
+
const mockMeetingTypes = jest.fn();
|
|
7
|
+
jest.mock("@working-theory/api-client", () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
schedule: { meetingTypes: (...args: unknown[]) => mockMeetingTypes(...args) },
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { enabledMeetingSlugs } from "../meeting-slugs";
|
|
13
|
+
|
|
14
|
+
describe("enabledMeetingSlugs", () => {
|
|
15
|
+
const originalEnv = process.env["SCHEDULING_CAL_LINK"];
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockMeetingTypes.mockReset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (originalEnv === undefined) delete process.env["SCHEDULING_CAL_LINK"];
|
|
23
|
+
else process.env["SCHEDULING_CAL_LINK"] = originalEnv;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("passes a real, populated array through unchanged", async () => {
|
|
27
|
+
mockMeetingTypes.mockResolvedValue({
|
|
28
|
+
ok: true,
|
|
29
|
+
body: { slugs: ["intro", "demo"] },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual(["intro", "demo"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("passes a real, intentionally empty array through unchanged (D4 row 2 — never the fallback)", async () => {
|
|
36
|
+
process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
|
|
37
|
+
mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: [] } });
|
|
38
|
+
|
|
39
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("null + SCHEDULING_CAL_LINK set → the derived singleton (D4 row 1)", async () => {
|
|
43
|
+
process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
|
|
44
|
+
mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
|
|
45
|
+
|
|
46
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual(["intro-call"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("null + SCHEDULING_CAL_LINK unset → [] (no shipped page to protect)", async () => {
|
|
50
|
+
delete process.env["SCHEDULING_CAL_LINK"];
|
|
51
|
+
mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
|
|
52
|
+
|
|
53
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("a non-ok API result → [], fail closed — an outage must not open the gate", async () => {
|
|
57
|
+
process.env["SCHEDULING_CAL_LINK"] = "acme/intro-call";
|
|
58
|
+
mockMeetingTypes.mockResolvedValue({ ok: false, status: 500, body: null });
|
|
59
|
+
|
|
60
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("null + a MALFORMED SCHEDULING_CAL_LINK slug → [], never a corrupt fallback (D14 parity, security review 2026-09-19)", async () => {
|
|
64
|
+
// A comma in the derived slug would corrupt the batched event-meta
|
|
65
|
+
// `calLinks` join downstream (it splits on comma), misattributing the
|
|
66
|
+
// rest of the list to a foreign owner — MeetingSlugSchema rejects it.
|
|
67
|
+
process.env["SCHEDULING_CAL_LINK"] = "acme/intro,evil";
|
|
68
|
+
mockMeetingTypes.mockResolvedValue({ ok: true, body: { slugs: null } });
|
|
69
|
+
|
|
70
|
+
await expect(enabledMeetingSlugs()).resolves.toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -311,7 +311,24 @@ const NO_STRUCTURED_DATA_ROUTES: ReadonlyArray<{
|
|
|
311
311
|
subtree: true,
|
|
312
312
|
},
|
|
313
313
|
{ route: "meet", reason: "booking surface, API-backed", subtree: true },
|
|
314
|
-
|
|
314
|
+
// #6084 — both purchase return pages read a Checkout Session (or read
|
|
315
|
+
// nothing, for cancel) and render via SectionRenderer directly; neither
|
|
316
|
+
// is a content-adapter page, and the paid render's own schema.org node
|
|
317
|
+
// (`PaymentComplete`, D6) is asserted directly, not through this walk.
|
|
318
|
+
{
|
|
319
|
+
route: "purchase",
|
|
320
|
+
reason: "checkout return pages, API-backed / read-nothing",
|
|
321
|
+
subtree: true,
|
|
322
|
+
},
|
|
323
|
+
// The whole (auth) route group (#5963). The walk keys on the DIRECTORY
|
|
324
|
+
// path, and a route group's parens are part of it, so this one subtree
|
|
325
|
+
// entry covers all five auth pages — including /sign-in, whose own entry it
|
|
326
|
+
// replaces now that the page sits inside the group.
|
|
327
|
+
{
|
|
328
|
+
route: "(auth)",
|
|
329
|
+
reason: "auth surfaces, not content pages",
|
|
330
|
+
subtree: true,
|
|
331
|
+
},
|
|
315
332
|
];
|
|
316
333
|
|
|
317
334
|
interface RouteEntry {
|