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,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
|
+
});
|
|
@@ -4,11 +4,28 @@
|
|
|
4
4
|
* This is the end-to-end claim the wiring makes — a site declares a theme and
|
|
5
5
|
* the built page renders in those colours — asserted on the actual repo file
|
|
6
6
|
* rather than a fixture, so deleting the `theme:` block would fail here.
|
|
7
|
+
*
|
|
8
|
+
* Every expectation is DERIVED from whichever `_site.mdx` the test reads, never
|
|
9
|
+
* hard-coded (#6460). This file is snapshotted into `create-ailk`'s
|
|
10
|
+
* `templates/`, so it runs against TWO different `_site.mdx` files: this
|
|
11
|
+
* monorepo's, and the scaffold's own. Hard-coded values made it a snapshot of
|
|
12
|
+
* one tree's palette masquerading as a round-trip test — green here, red in a
|
|
13
|
+
* scaffold whose theme differs, which is exactly what happened when the base
|
|
14
|
+
* scaffold moved to the black-on-white foundation. It also meant a CUSTOMER who
|
|
15
|
+
* took the file's own invitation to "declare only what you change" got a red
|
|
16
|
+
* suite for editing their own colours.
|
|
17
|
+
*
|
|
18
|
+
* Deriving keeps every claim the hard-coded form made — the mapping is applied,
|
|
19
|
+
* the values reach the CSS, and nothing else moves — while making all three
|
|
20
|
+
* true of any declared palette. Deleting the `theme:` block still fails, on the
|
|
21
|
+
* `toBeDefined()` below.
|
|
7
22
|
*/
|
|
8
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
24
|
import { tmpdir } from "node:os";
|
|
10
25
|
import { join } from "node:path";
|
|
11
26
|
|
|
27
|
+
import { parse as parseYaml } from "yaml";
|
|
28
|
+
|
|
12
29
|
import { PartialColorPaletteSchema } from "@working-theory/site-config/schema";
|
|
13
30
|
import { siteColorTokenMap, themeToCssVarsBothModes } from "@working-theory/theme";
|
|
14
31
|
|
|
@@ -26,6 +43,46 @@ function darkBlock(css: string): string {
|
|
|
26
43
|
return css.slice(css.indexOf(':root[data-theme="dark"]'));
|
|
27
44
|
}
|
|
28
45
|
|
|
46
|
+
/** The two modes, as the palette and the emitted CSS both name them. */
|
|
47
|
+
const MODES = ["light", "dark"] as const;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The `theme.colors` block exactly as the `_site.mdx` under test declares it —
|
|
51
|
+
* raw SLOT names (`text`, `primary`), not token keys. Read with the same
|
|
52
|
+
* frontmatter parse `site-theme.ts` uses, so the test cannot pass against a
|
|
53
|
+
* file the implementation reads differently.
|
|
54
|
+
*/
|
|
55
|
+
function declaredColors(): Partial<Record<(typeof MODES)[number], Record<string, string>>> {
|
|
56
|
+
const raw = readFileSync(join(REPO_CONTENT_ROOT, "_site.mdx"), "utf8");
|
|
57
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
58
|
+
const parsed = parseYaml(frontmatter?.[1] ?? "") as
|
|
59
|
+
| { theme?: { colors?: Record<string, Record<string, string>> } }
|
|
60
|
+
| null;
|
|
61
|
+
return parsed?.theme?.colors ?? {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The CSS custom property a token key is emitted as — `fgOnBrand` →
|
|
66
|
+
* `--color-fg-on-brand`. The colon in every call site anchors the match, so
|
|
67
|
+
* `--color-surface` never collides with `--color-surface-secondary`.
|
|
68
|
+
*/
|
|
69
|
+
function cssVar(tokenKey: string): string {
|
|
70
|
+
return `--color-${tokenKey.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** `{ [tokenKey]: value }` for one mode's declared slots, mapping applied. */
|
|
74
|
+
function declaredTokens(mode: (typeof MODES)[number]): Record<string, string> {
|
|
75
|
+
const out: Record<string, string> = {};
|
|
76
|
+
for (const [slot, value] of Object.entries(declaredColors()[mode] ?? {})) {
|
|
77
|
+
const tokenKey = siteColorTokenMap[slot as keyof typeof siteColorTokenMap];
|
|
78
|
+
// A slot with no token counterpart cannot reach the CSS at all —
|
|
79
|
+
// `siteThemeToBrandTokens` throws on it, which the schema's own tests
|
|
80
|
+
// cover. Nothing to assert about it here.
|
|
81
|
+
if (typeof tokenKey === "string") out[tokenKey] = value;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
29
86
|
describe("loadSiteBrandTokens — the real content/_site.mdx", () => {
|
|
30
87
|
const originalRoot = process.env.AILK_CONTENT_ROOT;
|
|
31
88
|
|
|
@@ -38,35 +95,93 @@ describe("loadSiteBrandTokens — the real content/_site.mdx", () => {
|
|
|
38
95
|
else process.env.AILK_CONTENT_ROOT = originalRoot;
|
|
39
96
|
});
|
|
40
97
|
|
|
41
|
-
it("translates the
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
98
|
+
it("translates every colour the file declares into its mapped token", () => {
|
|
99
|
+
const overlay = loadSiteBrandTokens();
|
|
100
|
+
// The `theme:` block exists and produced an overlay — the claim the
|
|
101
|
+
// hard-coded form made by naming values, kept without naming them.
|
|
102
|
+
expect(overlay).toBeDefined();
|
|
103
|
+
|
|
104
|
+
// `BrandTokenOverrides` is deliberately `Readonly<Record<string, unknown>>`
|
|
105
|
+
// — `@working-theory/theme` cannot name the overlay's shape without
|
|
106
|
+
// pinning it to one token set — so the test states the shape it asserts
|
|
107
|
+
// about. The hard-coded form sidestepped this by comparing whole objects.
|
|
108
|
+
const colors = (overlay?.["colors"] ?? {}) as Partial<
|
|
109
|
+
Record<(typeof MODES)[number], Record<string, string>>
|
|
110
|
+
>;
|
|
111
|
+
|
|
112
|
+
let asserted = 0;
|
|
113
|
+
for (const mode of MODES) {
|
|
114
|
+
const expectedTokens = declaredTokens(mode);
|
|
115
|
+
// Exactly the declared slots, nothing invented and nothing dropped.
|
|
116
|
+
expect(colors[mode] ?? {}).toEqual(expectedTokens);
|
|
117
|
+
asserted += Object.keys(expectedTokens).length;
|
|
118
|
+
}
|
|
119
|
+
// Non-vacuity: a file that declared nothing would satisfy the loop above.
|
|
120
|
+
expect(asserted).toBeGreaterThan(0);
|
|
48
121
|
});
|
|
49
122
|
|
|
50
123
|
it("reaches the emitted CSS custom properties", () => {
|
|
51
124
|
const brand = loadSiteBrandTokens();
|
|
52
125
|
const css = themeToCssVarsBothModes(brand ? { brand } : {});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
126
|
+
const blockFor = { light: lightBlock(css), dark: darkBlock(css) };
|
|
127
|
+
|
|
128
|
+
let asserted = 0;
|
|
129
|
+
for (const mode of MODES) {
|
|
130
|
+
for (const [tokenKey, value] of Object.entries(declaredTokens(mode))) {
|
|
131
|
+
expect(blockFor[mode]).toContain(`${cssVar(tokenKey)}: ${value};`);
|
|
132
|
+
asserted += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
expect(asserted).toBeGreaterThan(0);
|
|
57
136
|
});
|
|
58
137
|
|
|
59
138
|
it("changes exactly the declared variables and nothing else", () => {
|
|
60
139
|
const brand = loadSiteBrandTokens();
|
|
61
140
|
const themed = themeToCssVarsBothModes(brand ? { brand } : {});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
141
|
+
|
|
142
|
+
// Rebuild the expectation from the DEFAULT CSS by overwriting, per mode
|
|
143
|
+
// block, only the variables this site declares. Anything the overlay
|
|
144
|
+
// touched beyond them makes the final equality fail — which is the whole
|
|
145
|
+
// point of this case.
|
|
146
|
+
const base = themeToCssVarsBothModes();
|
|
147
|
+
const rewritten = { light: lightBlock(base), dark: darkBlock(base) };
|
|
148
|
+
for (const mode of MODES) {
|
|
149
|
+
for (const [tokenKey, value] of Object.entries(declaredTokens(mode))) {
|
|
150
|
+
const pattern = new RegExp(`(${cssVar(tokenKey)}: )[^;]+;`);
|
|
151
|
+
// The variable must exist in the default block for the overwrite to
|
|
152
|
+
// mean anything — a replace with no target would weaken the equality
|
|
153
|
+
// below into a tautology. Presence is the right check rather than
|
|
154
|
+
// "the text changed": a site may legitimately declare a value that
|
|
155
|
+
// already MATCHES the default (the black-on-white base declares
|
|
156
|
+
// `bg: #ffffff`, which light mode already is), and that no-op is
|
|
157
|
+
// correct, not vacuous.
|
|
158
|
+
expect(rewritten[mode]).toMatch(pattern);
|
|
159
|
+
rewritten[mode] = rewritten[mode].replace(pattern, `$1${value};`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// `--color-surface-raised` is DERIVED, not independent: the emitter
|
|
163
|
+
// resolves it from `surfaceSecondary` in light mode and from `surface`
|
|
164
|
+
// in dark (`packages/theme/src/theme-to-css.ts`, and the rule is
|
|
165
|
+
// documented at `site-theme.ts`'s header). So a site that declares
|
|
166
|
+
// either of those moves it too, and the equality below has to account
|
|
167
|
+
// for it — re-derived here from whatever the block now carries rather
|
|
168
|
+
// than restated as a value, so the two cannot drift.
|
|
169
|
+
//
|
|
170
|
+
// Kept inside the total equality instead of excluded from it: this
|
|
171
|
+
// variable is SUPPOSED to move, and excluding it would stop the test
|
|
172
|
+
// noticing if it ever stopped moving.
|
|
173
|
+
const derivedFrom = mode === "light" ? "surfaceSecondary" : "surface";
|
|
174
|
+
const sourceValue = new RegExp(`${cssVar(derivedFrom)}: ([^;]+);`).exec(
|
|
175
|
+
rewritten[mode],
|
|
176
|
+
)?.[1];
|
|
177
|
+
expect(sourceValue).toBeDefined();
|
|
178
|
+
rewritten[mode] = rewritten[mode].replace(
|
|
179
|
+
/(--color-surface-raised: )[^;]+;/,
|
|
180
|
+
`$1${sourceValue};`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
expect(themed).toBe(rewritten.light + rewritten.dark);
|
|
70
185
|
});
|
|
71
186
|
|
|
72
187
|
it("treats a bare `theme:` key as no theme, rather than throwing", () => {
|
|
@@ -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 {
|