create-ailk 0.4.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 +12248 -0
- package/component-catalog.md +193 -68
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- 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 +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.js +4 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +60 -8
- package/dist/module-architecture.js +587 -242
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +209 -36
- package/dist/template-manifest.d.ts +88 -3
- package/dist/template-manifest.js +281 -29
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +2 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
- package/site-templates/minimalism-site/ailk-template.json +22 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -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/.claude/skills/README.md +16 -25
- package/templates/.env.example +102 -12
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- 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 +29 -2
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +7 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- 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 +10 -1
- 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 +241 -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 +521 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -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/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- 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 +521 -55
- 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-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -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 +300 -0
- 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 +160 -0
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +284 -0
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- 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 +58 -25
- 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 +12 -9
- 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/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +8 -2
- 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 +61 -37
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- 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 +9 -7
- 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 +74 -8
- package/templates/apps/web/CLAUDE.md +46 -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 +67 -14
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +308 -65
- 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/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +15 -19
- 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 +50 -6
- 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/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
- package/templates/apps/web/messages/en.json +216 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +8 -3
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -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__/seed.test.ts +1 -1
- 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/eslint.config.mjs +69 -0
- 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 +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- 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/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /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
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -58,11 +58,20 @@ const SHARED_TENANT_ROUTES = [
|
|
|
58
58
|
{ method: 'GET' as const, url: `/v1/workspaces/${WS}/members` },
|
|
59
59
|
];
|
|
60
60
|
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Routes that must NOT register under `workspace` alone — the gate's other
|
|
63
|
+
* half. `/v1/waitlist-experiments` is on this list because the review surface
|
|
64
|
+
* is gated on `multi-tenant || signal-collectors` (#5677), neither of which is
|
|
65
|
+
* enabled in this file's `workspace`-only cases; it is no longer
|
|
66
|
+
* multi-tenant-EXCLUSIVE, and its own matrix is
|
|
67
|
+
* `review-surface-registration.test.ts`.
|
|
68
|
+
*/
|
|
62
69
|
const MULTI_TENANT_ONLY_ROUTES = [
|
|
63
70
|
{ method: 'GET' as const, url: '/v1/workspaces' },
|
|
64
71
|
{ method: 'GET' as const, url: '/v1/billing' },
|
|
65
72
|
{ method: 'GET' as const, url: '/v1/waitlist-experiments' },
|
|
73
|
+
{ method: 'POST' as const, url: `/v1/workspaces/${WS}/activate` },
|
|
74
|
+
{ method: 'GET' as const, url: `/v1/workspaces/${WS}/sessions` },
|
|
66
75
|
];
|
|
67
76
|
|
|
68
77
|
async function withEnabledModules<T>(
|
|
@@ -54,6 +54,41 @@ export interface Config {
|
|
|
54
54
|
// origin, which stays correct for any agent that fetched the spec from this
|
|
55
55
|
// API. See resolveServers() in src/openapi/spec.ts.
|
|
56
56
|
AILK_PUBLIC_API_URL?: string;
|
|
57
|
+
|
|
58
|
+
// Request-and-confirm (#5970) — the callback → confirm → Payment Link path.
|
|
59
|
+
// All five optional and all read through services/request-confirm.ts, never
|
|
60
|
+
// from here directly; they are declared so the env contract is complete and
|
|
61
|
+
// one place lists them.
|
|
62
|
+
//
|
|
63
|
+
// AILK_REQUEST_CONFIRM_ENABLED is the switch: "true" turns the path on,
|
|
64
|
+
// anything else (including absent, the default) leaves the callback route
|
|
65
|
+
// behaving byte-for-byte as it did before the feature existed. TO and FROM
|
|
66
|
+
// fall back to the AILK_LEAD_NOTIFICATION_* pair; the link's base is the
|
|
67
|
+
// existing AILK_PUBLIC_API_URL above, since the confirm route is on this
|
|
68
|
+
// API. None of the five is a secret, so project.yaml's required_secrets
|
|
69
|
+
// gains nothing.
|
|
70
|
+
AILK_REQUEST_CONFIRM_ENABLED?: string;
|
|
71
|
+
AILK_REQUEST_CONFIRM_TO?: string;
|
|
72
|
+
AILK_REQUEST_CONFIRM_FROM?: string;
|
|
73
|
+
AILK_REQUEST_CONFIRM_CURRENCY?: string;
|
|
74
|
+
AILK_REQUEST_CONFIRM_MAX_MINOR?: string;
|
|
75
|
+
|
|
76
|
+
// Publish revalidation (#5851) — the signed webhook services/revalidation.ts
|
|
77
|
+
// POSTs at the site's /api/revalidate on publish/unpublish.
|
|
78
|
+
// CONTENT_REVALIDATE_SECRET is the HMAC-SHA256 signing key. Both optional;
|
|
79
|
+
// either absent means "revalidation off" — the service is a logged no-op and
|
|
80
|
+
// the publish that called it still completes.
|
|
81
|
+
CONTENT_REVALIDATE_URL?: string;
|
|
82
|
+
CONTENT_REVALIDATE_SECRET?: string;
|
|
83
|
+
|
|
84
|
+
// Scheduling webhook receiver (#6085, brief H7) — the HMAC-SHA256 key
|
|
85
|
+
// routes/webhooks/calcom.ts verifies POST /v1/webhooks/calcom's
|
|
86
|
+
// x-cal-signature-256 header against (§ D2, § D14). Read the same way
|
|
87
|
+
// STRIPE_WEBHOOK_SECRET is above — through `config`, never `process.env`
|
|
88
|
+
// at the call site. Unset makes the receiver answer 503 and accept
|
|
89
|
+
// nothing (§ D2 row 1) — the same fail-closed posture STRIPE_WEBHOOK_SECRET
|
|
90
|
+
// unset already has.
|
|
91
|
+
CAL_WEBHOOK_SECRET?: string;
|
|
57
92
|
}
|
|
58
93
|
|
|
59
94
|
function validateConfig(): Config {
|
|
@@ -104,6 +139,33 @@ function validateConfig(): Config {
|
|
|
104
139
|
...(process.env.AILK_PUBLIC_API_URL !== undefined && {
|
|
105
140
|
AILK_PUBLIC_API_URL: process.env.AILK_PUBLIC_API_URL,
|
|
106
141
|
}),
|
|
142
|
+
|
|
143
|
+
...(process.env.AILK_REQUEST_CONFIRM_ENABLED !== undefined && {
|
|
144
|
+
AILK_REQUEST_CONFIRM_ENABLED: process.env.AILK_REQUEST_CONFIRM_ENABLED,
|
|
145
|
+
}),
|
|
146
|
+
...(process.env.AILK_REQUEST_CONFIRM_TO !== undefined && {
|
|
147
|
+
AILK_REQUEST_CONFIRM_TO: process.env.AILK_REQUEST_CONFIRM_TO,
|
|
148
|
+
}),
|
|
149
|
+
...(process.env.AILK_REQUEST_CONFIRM_FROM !== undefined && {
|
|
150
|
+
AILK_REQUEST_CONFIRM_FROM: process.env.AILK_REQUEST_CONFIRM_FROM,
|
|
151
|
+
}),
|
|
152
|
+
...(process.env.AILK_REQUEST_CONFIRM_CURRENCY !== undefined && {
|
|
153
|
+
AILK_REQUEST_CONFIRM_CURRENCY: process.env.AILK_REQUEST_CONFIRM_CURRENCY,
|
|
154
|
+
}),
|
|
155
|
+
...(process.env.AILK_REQUEST_CONFIRM_MAX_MINOR !== undefined && {
|
|
156
|
+
AILK_REQUEST_CONFIRM_MAX_MINOR: process.env.AILK_REQUEST_CONFIRM_MAX_MINOR,
|
|
157
|
+
}),
|
|
158
|
+
|
|
159
|
+
...(process.env.CONTENT_REVALIDATE_URL !== undefined && {
|
|
160
|
+
CONTENT_REVALIDATE_URL: process.env.CONTENT_REVALIDATE_URL,
|
|
161
|
+
}),
|
|
162
|
+
...(process.env.CONTENT_REVALIDATE_SECRET !== undefined && {
|
|
163
|
+
CONTENT_REVALIDATE_SECRET: process.env.CONTENT_REVALIDATE_SECRET,
|
|
164
|
+
}),
|
|
165
|
+
|
|
166
|
+
...(process.env.CAL_WEBHOOK_SECRET !== undefined && {
|
|
167
|
+
CAL_WEBHOOK_SECRET: process.env.CAL_WEBHOOK_SECRET,
|
|
168
|
+
}),
|
|
107
169
|
};
|
|
108
170
|
}
|
|
109
171
|
|
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
* Recognized module keys: `docs`, `multi-tenant`, `workspace`, `scheduling`,
|
|
13
13
|
* `signal-collectors`, `commerce` (the `ModuleName` vocabulary
|
|
14
14
|
* `packages/create-ailk/src/module-architecture.ts` already models via
|
|
15
|
-
* `MODULE_SLICES`) plus `content-write` — the Pro
|
|
16
|
-
*
|
|
17
|
-
* `apps/api/src/routes/content
|
|
15
|
+
* `MODULE_SLICES`) plus `content-write` — the Pro content-write surface,
|
|
16
|
+
* which since #5853 means the tenant-guarded editor routes in
|
|
17
|
+
* `apps/api/src/routes/workspaces/content.ts` (the untenanted
|
|
18
|
+
* `create`/`update`/`delete` under `apps/api/src/routes/content/` that it used
|
|
19
|
+
* to name were retired by deletion). The flag's NAME, registration grain and
|
|
20
|
+
* default-off semantics are unchanged (D8) — only its documented meaning
|
|
21
|
+
* widened. `content-write` is intentionally NOT a
|
|
18
22
|
* `MODULE_SLICES` entry: it is not (yet) a `create-ailk --modules`
|
|
19
23
|
* selector — a customer never opts into it at scaffold time, it is a
|
|
20
24
|
* license-tier line drawn inside the single `apps/api` app — so it stays a
|
|
@@ -27,7 +27,14 @@
|
|
|
27
27
|
* idempotency reader) + callback.findMany (booked-call suppression) +
|
|
28
28
|
* waitlistScore.findMany (tier lookup) — used by the #4732
|
|
29
29
|
* waitlist-followup sweep service tests
|
|
30
|
-
* tenancy models (workspace, membership) — used by tenant-context service tests (#3627)
|
|
30
|
+
* tenancy models (workspace, membership) — used by tenant-context service tests (#3627).
|
|
31
|
+
* workspace.findUnique carries NO default (#6335) — see its own comment
|
|
32
|
+
* below. A suite whose route resolves a workspace states the shape it
|
|
33
|
+
* expects through `mockWorkspaceExists()` / `mockNoWorkspace()`, exported
|
|
34
|
+
* at the foot of this file
|
|
35
|
+
* artifact models (artifact, artifactVersion) — the database content
|
|
36
|
+
* adapter's read surface, used by the public-site-resolver + content route
|
|
37
|
+
* tests (#5852)
|
|
31
38
|
* membership.findMany + lead.findMany/updateMany — used by lead-promotion
|
|
32
39
|
* service tests (#3586); lead-promotion.ts never queries membership/workspace
|
|
33
40
|
* directly (it resolves workspaces via tenant-context.ts's listTenancy), so
|
|
@@ -49,6 +56,10 @@
|
|
|
49
56
|
* #3739 M2 — was `findMany` + a JS accumulation loop). Same not-yet-
|
|
50
57
|
* generated-client situation; `usage-aggregation.ts` casts `prisma` to its
|
|
51
58
|
* own structural delegate type, and this mock's runtime shape matches it.
|
|
59
|
+
*
|
|
60
|
+
* userData.create/updateMany (auth) + siteSetting.findUnique (config) — the
|
|
61
|
+
* dunning record's compare-and-swap write and the per-tenant grace-policy
|
|
62
|
+
* read (#6034), used by dunning-policy.test.ts and stripe-dunning.test.ts.
|
|
52
63
|
*/
|
|
53
64
|
|
|
54
65
|
export const prisma = {
|
|
@@ -68,6 +79,13 @@ export const prisma = {
|
|
|
68
79
|
findUnique: jest.fn(),
|
|
69
80
|
upsert: jest.fn(),
|
|
70
81
|
deleteMany: jest.fn(),
|
|
82
|
+
// `create` / `updateMany` — the dunning record's compare-and-swap write
|
|
83
|
+
// (#6034 § D6): the webhook reads `{value, updatedAt}` then either
|
|
84
|
+
// `create`s a fresh row or `updateMany`s guarded on BOTH observed
|
|
85
|
+
// columns, so a concurrent writer makes the predicate miss (`count: 0`)
|
|
86
|
+
// rather than silently losing a revocation.
|
|
87
|
+
create: jest.fn(),
|
|
88
|
+
updateMany: jest.fn(),
|
|
71
89
|
},
|
|
72
90
|
// ── Tenancy namespace (Organization, Workspace, Membership) ──────────────
|
|
73
91
|
organization: {
|
|
@@ -84,6 +102,29 @@ export const prisma = {
|
|
|
84
102
|
updateMany: jest.fn(),
|
|
85
103
|
},
|
|
86
104
|
workspace: {
|
|
105
|
+
// #6335 — NO default implementation, deliberately. Until this issue the
|
|
106
|
+
// delegate carried a PERMISSIVE one (#6203): it resolved ANY requested
|
|
107
|
+
// id as an existing workspace with a sentinel-shaped organizationId, so
|
|
108
|
+
// every tenancy resolver in a route test saw "this workspace exists" no
|
|
109
|
+
// matter what the test had set up. That is DEF-014 — a fixture that
|
|
110
|
+
// fabricates the condition under test: a route that should have failed
|
|
111
|
+
// closed passed, and every isolation-shaped test written against this
|
|
112
|
+
// mock inherited a fake success.
|
|
113
|
+
//
|
|
114
|
+
// A bare `jest.fn()` resolves `undefined`, which every resolver in
|
|
115
|
+
// `apps/api/src/lib/site.ts` reads as "no such workspace" and rejects —
|
|
116
|
+
// so a suite that needs a workspace to resolve must SAY SO, keyed on the
|
|
117
|
+
// id it actually expects to be asked for:
|
|
118
|
+
//
|
|
119
|
+
// mockWorkspaceExists(prisma, SITE_ID) // only SITE_ID resolves
|
|
120
|
+
// mockSentinelWorkspaceExists(prisma) // the "default" alias path
|
|
121
|
+
// mockNoWorkspace(prisma) // nothing resolves
|
|
122
|
+
//
|
|
123
|
+
// Keying on the requested id is the load-bearing part.
|
|
124
|
+
// `resolveWorkspaceIdForAlias` returns the ALIAS on mere truthiness of
|
|
125
|
+
// the row (`site.ts`) — it never reads the row's own id — so a mock that
|
|
126
|
+
// resolves unconditionally is permissive by accident even when it hands
|
|
127
|
+
// back a specific-looking row.
|
|
87
128
|
findUnique: jest.fn(),
|
|
88
129
|
count: jest.fn(),
|
|
89
130
|
},
|
|
@@ -92,6 +133,18 @@ export const prisma = {
|
|
|
92
133
|
findMany: jest.fn(),
|
|
93
134
|
create: jest.fn(),
|
|
94
135
|
},
|
|
136
|
+
// ── Artifact store (the database content adapter's read surface, #5852) ──
|
|
137
|
+
// Only the two readers `ArtifactPrismaClient` declares for reads
|
|
138
|
+
// (packages/content-adapters/src/database/adapter.ts): the public content
|
|
139
|
+
// routes never write, so no `create`/`updateMany`/`deleteMany` is faked here.
|
|
140
|
+
artifact: {
|
|
141
|
+
findFirst: jest.fn(),
|
|
142
|
+
findMany: jest.fn(),
|
|
143
|
+
},
|
|
144
|
+
artifactVersion: {
|
|
145
|
+
findFirst: jest.fn(),
|
|
146
|
+
findMany: jest.fn(),
|
|
147
|
+
},
|
|
95
148
|
// ── Config namespace (per-workspace site config — #3641/#3634) ───────────
|
|
96
149
|
workspaceTheme: {
|
|
97
150
|
findUnique: jest.fn(),
|
|
@@ -114,6 +167,9 @@ export const prisma = {
|
|
|
114
167
|
// `findUnique` — used by the deliverable-resend service (#3998) to
|
|
115
168
|
// resolve a grant's stored lead email.
|
|
116
169
|
findUnique: jest.fn(),
|
|
170
|
+
// `findFirst` — the leads-inbox cursor lookup (#5678), which resolves a
|
|
171
|
+
// supplied cursor WITHIN the caller's scope before paging from it.
|
|
172
|
+
findFirst: jest.fn(),
|
|
117
173
|
findMany: jest.fn(),
|
|
118
174
|
updateMany: jest.fn(),
|
|
119
175
|
},
|
|
@@ -124,9 +180,16 @@ export const prisma = {
|
|
|
124
180
|
// `findMany` — the #4732 follow-up sweep's booked-call suppression check
|
|
125
181
|
// (D2: a site's Callback rows fetched once per sweep run, filtered in
|
|
126
182
|
// application code for `payload.email` matches).
|
|
183
|
+
// findFirst / update / updateMany joined for the #5970 request-and-confirm
|
|
184
|
+
// lifecycle: the credential mint and the confirmation claim are both guarded
|
|
185
|
+
// `updateMany`s, the GET resolves through `findFirst`, and the Stripe
|
|
186
|
+
// artifact id lands via `update`.
|
|
127
187
|
callback: {
|
|
128
188
|
create: jest.fn(),
|
|
189
|
+
findFirst: jest.fn(),
|
|
129
190
|
findMany: jest.fn(),
|
|
191
|
+
update: jest.fn(),
|
|
192
|
+
updateMany: jest.fn(),
|
|
130
193
|
},
|
|
131
194
|
// ── Inbox namespace (FlowDefinition, FlowSession — #3956) ────────────────
|
|
132
195
|
// findMany on both models — used by the abandon-email sweep (#3962 D4),
|
|
@@ -134,6 +197,10 @@ export const prisma = {
|
|
|
134
197
|
// policy) and every candidate FlowSession per definition.
|
|
135
198
|
flowDefinition: {
|
|
136
199
|
findUnique: jest.fn(),
|
|
200
|
+
// #6314 — `startProjectListingDraft` resolves the capturing listicle
|
|
201
|
+
// magnet through `findFirst` (slug + siteId + workspaceId + kind), so the
|
|
202
|
+
// delegate needs it here as well as `findUnique`.
|
|
203
|
+
findFirst: jest.fn(),
|
|
137
204
|
findMany: jest.fn(),
|
|
138
205
|
upsert: jest.fn(),
|
|
139
206
|
},
|
|
@@ -153,13 +220,21 @@ export const prisma = {
|
|
|
153
220
|
},
|
|
154
221
|
// `update` — the send-outcome stamp (#3998 D1). `findMany` — the
|
|
155
222
|
// deliverable:resend CLI's list mode (#3998 D4), selecting on
|
|
156
|
-
// `sendStatus: null`.
|
|
223
|
+
// `sendStatus: null`. `workspaceId` (#6310 D15) rides `create`'s existing
|
|
224
|
+
// `data` shape — no new delegate method.
|
|
157
225
|
deliverableGrant: {
|
|
158
226
|
create: jest.fn(),
|
|
159
227
|
findUnique: jest.fn(),
|
|
160
228
|
update: jest.fn(),
|
|
161
229
|
findMany: jest.fn(),
|
|
162
230
|
},
|
|
231
|
+
// ── Content namespace (Asset — #5854/#6093, extended #6310 D16) ──────────
|
|
232
|
+
// `findFirst` — the ONLY method `lib/asset-store.ts`'s public read and
|
|
233
|
+
// `lib/deliverable-asset-store.ts`'s two scoped reads need. No write, no
|
|
234
|
+
// `findMany`, no `findUnique` — neither boundary enumerates or mutates.
|
|
235
|
+
asset: {
|
|
236
|
+
findFirst: jest.fn(),
|
|
237
|
+
},
|
|
163
238
|
// ── Inbox namespace (FlowCheckout — #4837 D4) ─────────────────────────────
|
|
164
239
|
// `create` — recordCheckout's one write; `update` — attachStripeSession;
|
|
165
240
|
// `updateMany` — the webhook's guarded status stamp (#4838); `findUnique` /
|
|
@@ -179,6 +254,19 @@ export const prisma = {
|
|
|
179
254
|
siteListingConfig: {
|
|
180
255
|
findUnique: jest.fn(),
|
|
181
256
|
},
|
|
257
|
+
// ── Config namespace (SiteSetting — #6034 § D4) ──────────────────────────
|
|
258
|
+
// `findUnique` — the ONLY read path (`dunningPolicyFor`). #6374 D3 changed
|
|
259
|
+
// its KEY SHAPE, not its arity: `site_settings` is now unique on
|
|
260
|
+
// (workspace_id, key), so the call is
|
|
261
|
+
// `findUnique({ where: { workspaceId_key: { workspaceId, key } } })` and
|
|
262
|
+
// the `key` string still carries the site alias it always did. The
|
|
263
|
+
// delegate itself is unchanged — a `jest.fn()` does not care about the
|
|
264
|
+
// selector shape — but the comment is, so a reader programming this mock
|
|
265
|
+
// writes the compound selector rather than the removed bare `key`. No
|
|
266
|
+
// route in this repo writes SiteSetting; no write delegate is mocked.
|
|
267
|
+
siteSetting: {
|
|
268
|
+
findUnique: jest.fn(),
|
|
269
|
+
},
|
|
182
270
|
// ── Inbox namespace (ProjectListing — #4998, reshaped #5033) ─────────────
|
|
183
271
|
// `create` — the POST route's row write (after routeLead's Lead write).
|
|
184
272
|
// `findFirst` — the tenant-scoped get/patch reads (scoped by siteId + id, a
|
|
@@ -254,6 +342,19 @@ export const prisma = {
|
|
|
254
342
|
createMany: jest.fn(),
|
|
255
343
|
findMany: jest.fn(),
|
|
256
344
|
},
|
|
345
|
+
// ── Inbox namespace (Appointment — #6085, brief H7) ──────────────────────
|
|
346
|
+
// `updateMany` — the § D7(a) guarded write, one per family (lifecycle /
|
|
347
|
+
// payment / no-show); `findFirst` — the disambiguating read on a zero-match
|
|
348
|
+
// updateMany (row absent vs. present-with-newer-watermark); `create` — the
|
|
349
|
+
// create-eligible arm, wrapped in the caller's own `isUniqueViolation`
|
|
350
|
+
// retry. `findMany` — the tenant-scoped GET /v1/schedule/appointments read
|
|
351
|
+
// (apps/api/src/routes/schedule/appointments.ts).
|
|
352
|
+
appointment: {
|
|
353
|
+
create: jest.fn(),
|
|
354
|
+
findFirst: jest.fn(),
|
|
355
|
+
findMany: jest.fn(),
|
|
356
|
+
updateMany: jest.fn(),
|
|
357
|
+
},
|
|
257
358
|
// ── Content namespace (UsageEvent, UsagePeriod — FEAT-047 P1.S1/#3709) ──
|
|
258
359
|
usageEvent: {
|
|
259
360
|
create: jest.fn(),
|
|
@@ -287,3 +388,141 @@ export const prisma = {
|
|
|
287
388
|
$connect: jest.fn(),
|
|
288
389
|
$disconnect: jest.fn(),
|
|
289
390
|
};
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
// ── #6335 — the explicit workspace fixtures ──────────────────────────────
|
|
394
|
+
//
|
|
395
|
+
// These replace the permissive `workspace.findUnique` default. They take the
|
|
396
|
+
// prisma double they are to program as their FIRST argument, rather than
|
|
397
|
+
// reaching for the `prisma` const above: a test importing this file directly
|
|
398
|
+
// gets its own module instance (Jest keeps manual mocks in a registry of
|
|
399
|
+
// their own, separate from the one a plain `require` of this path fills), so
|
|
400
|
+
// a helper that closed over the local `prisma` would program an object no
|
|
401
|
+
// route ever reads. Passing the double the suite already imports from
|
|
402
|
+
// `../lib/prisma.js` is what makes these work from any suite.
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* The structural shape the helpers below need — deliberately NOT
|
|
406
|
+
* `typeof prisma`, so a suite can pass the `prisma` it imported from
|
|
407
|
+
* `lib/prisma.js` (typed as the real client) with no cast at the call site.
|
|
408
|
+
*/
|
|
409
|
+
type WorkspaceMockHost = { workspace: { findUnique: unknown } };
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The sentinel Organization id the seeded default workspace hangs off
|
|
413
|
+
* (`database/tenancy.ts`'s reserved-range predicate, ADR-0031 rule 7).
|
|
414
|
+
* `resolveSentinelWorkspaceRow` rejects a default-workspace row whose
|
|
415
|
+
* `organizationId` falls outside that range, so the `"default"` alias path
|
|
416
|
+
* resolves only when the mocked row carries this value.
|
|
417
|
+
*/
|
|
418
|
+
export const SENTINEL_ORGANIZATION_ID =
|
|
419
|
+
"org_00000000-0000-7000-8000-000000000001";
|
|
420
|
+
|
|
421
|
+
/** The seeded sentinel workspace id — `DEFAULT_WORKSPACE_ID` in `database/tenancy.ts`. */
|
|
422
|
+
export const SENTINEL_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The organization an ORDINARY workspace hangs off, and deliberately NOT a
|
|
426
|
+
* sentinel one: its uuid carries non-zero timestamp bits, so
|
|
427
|
+
* `isSentinel()` (`packages/ids`, `^00000000-0000-7000-8000-…`) is false
|
|
428
|
+
* for it.
|
|
429
|
+
*
|
|
430
|
+
* Stamping the sentinel org on every mocked workspace would be its own
|
|
431
|
+
* DEF-014: `organizationId` is read off this exact row as an AUTHORIZATION
|
|
432
|
+
* input — `tenant-context.ts` denies a stale session when it disagrees with
|
|
433
|
+
* the session's active org, and `isPlatformOperator`
|
|
434
|
+
* (`waitlist-operator-reads.ts`) is `isSentinel(organizationId) && role ===
|
|
435
|
+
* "owner"`, which grants cross-SITE read scope over every site's signup
|
|
436
|
+
* PII. A helper that made every workspace sentinel-org would hand a future
|
|
437
|
+
* ordinary-tenant fixture a platform-operator identity, and would make
|
|
438
|
+
* "two workspaces in different organizations" inexpressible — so the
|
|
439
|
+
* stale-session deny branch could never be driven through it.
|
|
440
|
+
*/
|
|
441
|
+
export const ORDINARY_ORGANIZATION_ID =
|
|
442
|
+
"org_01920000-0000-7000-8000-0000000000aa";
|
|
443
|
+
|
|
444
|
+
/** One workspace row a suite says exists: an id, or an id bound to a named org. */
|
|
445
|
+
export type MockWorkspaceRow =
|
|
446
|
+
| string
|
|
447
|
+
| { readonly id: string; readonly organizationId: string };
|
|
448
|
+
|
|
449
|
+
function workspaceFindUnique(client: WorkspaceMockHost): jest.Mock {
|
|
450
|
+
const delegate = client.workspace?.findUnique;
|
|
451
|
+
if (typeof delegate !== "function" || !("mockImplementation" in delegate)) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
"[#6335] prisma.workspace.findUnique is not a jest mock — the suite " +
|
|
454
|
+
"must jest.mock() its prisma module before calling this helper.",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
return delegate as unknown as jest.Mock;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* States, for one test or one suite, EXACTLY which workspace ids exist
|
|
462
|
+
* (#6335). Every other id — a foreign one, a spoofed one, an unset one —
|
|
463
|
+
* resolves `null`, so a route that resolves a tenancy scope fails closed
|
|
464
|
+
* unless the test named the id under test.
|
|
465
|
+
*
|
|
466
|
+
* Keyed on the REQUESTED id, and that is the load-bearing part:
|
|
467
|
+
* `resolveWorkspaceIdForAlias` (`apps/api/src/lib/site.ts`) returns the
|
|
468
|
+
* ALIAS on mere truthiness of the row it gets back and never reads that
|
|
469
|
+
* row's own id, so a blanket `mockResolvedValue({ … })` resolves whatever
|
|
470
|
+
* it is handed — permissive by another name, and the rejection cases each
|
|
471
|
+
* suite now carries could not tell the two apart.
|
|
472
|
+
*
|
|
473
|
+
* Call it from a suite's `beforeEach`, AFTER any `jest.clearAllMocks()`.
|
|
474
|
+
*/
|
|
475
|
+
export function mockWorkspaceExists(
|
|
476
|
+
client: WorkspaceMockHost,
|
|
477
|
+
...rows: readonly MockWorkspaceRow[]
|
|
478
|
+
): void {
|
|
479
|
+
const existing = new Map<string, string>();
|
|
480
|
+
for (const row of rows) {
|
|
481
|
+
if (typeof row === "string") {
|
|
482
|
+
// The sentinel workspace is the ONE row that hangs off the sentinel
|
|
483
|
+
// organization; everything else gets an ordinary one. See
|
|
484
|
+
// ORDINARY_ORGANIZATION_ID for why this distinction is load-bearing.
|
|
485
|
+
existing.set(
|
|
486
|
+
row,
|
|
487
|
+
row === SENTINEL_WORKSPACE_ID
|
|
488
|
+
? SENTINEL_ORGANIZATION_ID
|
|
489
|
+
: ORDINARY_ORGANIZATION_ID,
|
|
490
|
+
);
|
|
491
|
+
} else {
|
|
492
|
+
existing.set(row.id, row.organizationId);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
workspaceFindUnique(client).mockImplementation(
|
|
496
|
+
({ where }: { where: { id: string } }) =>
|
|
497
|
+
Promise.resolve(
|
|
498
|
+
existing.has(where.id)
|
|
499
|
+
? { id: where.id, organizationId: existing.get(where.id) }
|
|
500
|
+
: null,
|
|
501
|
+
),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The `"default"` site alias's resolution path (#6335): `currentSiteId()`
|
|
507
|
+
* returns `"default"`, `resolveWorkspaceIdForAlias` hands that to
|
|
508
|
+
* `resolveSentinelWorkspaceRow`, and only the well-known sentinel workspace
|
|
509
|
+
* — hanging off the sentinel Organization — satisfies it. Any further ids
|
|
510
|
+
* passed resolve beside it.
|
|
511
|
+
*/
|
|
512
|
+
export function mockSentinelWorkspaceExists(
|
|
513
|
+
client: WorkspaceMockHost,
|
|
514
|
+
...alsoExisting: readonly MockWorkspaceRow[]
|
|
515
|
+
): void {
|
|
516
|
+
mockWorkspaceExists(client, SENTINEL_WORKSPACE_ID, ...alsoExisting);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* The REJECTION path (#6335): no workspace resolves, whatever is asked for.
|
|
521
|
+
* Every suite that depends on an explicit `mockWorkspaceExists` carries at
|
|
522
|
+
* least one test using this, so that explicit setup is proven load-bearing
|
|
523
|
+
* rather than decorative — a route that quietly stopped resolving a tenancy
|
|
524
|
+
* scope would otherwise pass both ways.
|
|
525
|
+
*/
|
|
526
|
+
export function mockNoWorkspace(client: WorkspaceMockHost): void {
|
|
527
|
+
workspaceFindUnique(client).mockResolvedValue(null);
|
|
528
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file auth-email-budget.test.ts — the per-recipient hourly send budget (#5962 § D8c).
|
|
3
|
+
*
|
|
4
|
+
* Written before the implementation. The five properties that matter:
|
|
5
|
+
* - the bucket counts per (recipient, kind) and caps at three per hour;
|
|
6
|
+
* - the window rolls, so an hour later the recipient is clear again;
|
|
7
|
+
* - the store is bounded (MAX_BUCKETS eviction), because the key is a
|
|
8
|
+
* caller-supplied address and an attacker can mint unlimited distinct ones;
|
|
9
|
+
* - a recipient is keyed by a HASH, never by the plaintext address — this
|
|
10
|
+
* module is memory an operator can dump;
|
|
11
|
+
* - the site threshold is an operator SIGNAL, never a refusal. A site-wide
|
|
12
|
+
* cap an attacker can hold open is an indefinitely-holdable lockout
|
|
13
|
+
* (§ D8c, Q6), so nothing here may deny a send because of it.
|
|
14
|
+
*
|
|
15
|
+
* Time is injected rather than faked: the module reads `getClock().now()` by
|
|
16
|
+
* default and accepts an explicit `nowMs`, which is the seam this suite uses.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
consumeRecipientBudget,
|
|
21
|
+
siteVolumeAlert,
|
|
22
|
+
RECIPIENT_HOURLY_MAX,
|
|
23
|
+
SITE_HOURLY_ALERT_THRESHOLD,
|
|
24
|
+
__resetAuthEmailBudget,
|
|
25
|
+
__authEmailBucketCount,
|
|
26
|
+
__authEmailBudgetKeys,
|
|
27
|
+
} from "../auth-email-budget.js";
|
|
28
|
+
|
|
29
|
+
const T0 = 1_700_000_000_000;
|
|
30
|
+
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
__resetAuthEmailBudget();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("consumeRecipientBudget()", () => {
|
|
37
|
+
it("allows the first RECIPIENT_HOURLY_MAX sends and refuses the next", () => {
|
|
38
|
+
for (let i = 0; i < RECIPIENT_HOURLY_MAX; i++) {
|
|
39
|
+
expect(
|
|
40
|
+
consumeRecipientBudget("user@example.com", "password-reset", {
|
|
41
|
+
nowMs: T0,
|
|
42
|
+
}),
|
|
43
|
+
).toBe(true);
|
|
44
|
+
}
|
|
45
|
+
expect(
|
|
46
|
+
consumeRecipientBudget("user@example.com", "password-reset", {
|
|
47
|
+
nowMs: T0,
|
|
48
|
+
}),
|
|
49
|
+
).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("counts per (recipient, kind) — a reset flood cannot exhaust the notice", () => {
|
|
53
|
+
for (let i = 0; i < RECIPIENT_HOURLY_MAX; i++) {
|
|
54
|
+
consumeRecipientBudget("user@example.com", "password-reset", {
|
|
55
|
+
nowMs: T0,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
expect(
|
|
59
|
+
consumeRecipientBudget("user@example.com", "password-changed", {
|
|
60
|
+
nowMs: T0,
|
|
61
|
+
}),
|
|
62
|
+
).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("is case- and whitespace-insensitive on the recipient", () => {
|
|
66
|
+
for (let i = 0; i < RECIPIENT_HOURLY_MAX; i++) {
|
|
67
|
+
consumeRecipientBudget("User@Example.com", "password-reset", {
|
|
68
|
+
nowMs: T0,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
expect(
|
|
72
|
+
consumeRecipientBudget(" user@example.com ", "password-reset", {
|
|
73
|
+
nowMs: T0,
|
|
74
|
+
}),
|
|
75
|
+
).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("clears itself when the hour rolls", () => {
|
|
79
|
+
for (let i = 0; i < RECIPIENT_HOURLY_MAX; i++) {
|
|
80
|
+
consumeRecipientBudget("user@example.com", "password-reset", {
|
|
81
|
+
nowMs: T0,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
expect(
|
|
85
|
+
consumeRecipientBudget("user@example.com", "password-reset", {
|
|
86
|
+
nowMs: T0,
|
|
87
|
+
}),
|
|
88
|
+
).toBe(false);
|
|
89
|
+
expect(
|
|
90
|
+
consumeRecipientBudget("user@example.com", "password-reset", {
|
|
91
|
+
nowMs: T0 + ONE_HOUR_MS + 1,
|
|
92
|
+
}),
|
|
93
|
+
).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("bounds the store — a fresh identity at capacity evicts the oldest", () => {
|
|
97
|
+
const cap = 50;
|
|
98
|
+
for (let i = 0; i < cap + 10; i++) {
|
|
99
|
+
consumeRecipientBudget(`user${i}@example.com`, "password-reset", {
|
|
100
|
+
nowMs: T0,
|
|
101
|
+
maxBuckets: cap,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
expect(__authEmailBucketCount()).toBeLessThanOrEqual(cap);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("never stores the plaintext recipient", () => {
|
|
108
|
+
consumeRecipientBudget("secret-person@example.com", "password-reset", {
|
|
109
|
+
nowMs: T0,
|
|
110
|
+
});
|
|
111
|
+
expect(__authEmailBudgetKeys().join("|")).not.toContain("secret-person");
|
|
112
|
+
expect(__authEmailBudgetKeys().join("|")).not.toContain("@example.com");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("siteVolumeAlert()", () => {
|
|
117
|
+
it("does not fire below the threshold", () => {
|
|
118
|
+
for (let i = 0; i < SITE_HOURLY_ALERT_THRESHOLD - 1; i++) {
|
|
119
|
+
expect(siteVolumeAlert({ nowMs: T0 })).toBe(false);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("fires ONCE per hour past the threshold, and never refuses a send", () => {
|
|
124
|
+
let fired = 0;
|
|
125
|
+
for (let i = 0; i < SITE_HOURLY_ALERT_THRESHOLD + 25; i++) {
|
|
126
|
+
if (siteVolumeAlert({ nowMs: T0 })) fired++;
|
|
127
|
+
}
|
|
128
|
+
expect(fired).toBe(1);
|
|
129
|
+
|
|
130
|
+
// The signal is not a gate: a recipient under their own budget still sends.
|
|
131
|
+
expect(
|
|
132
|
+
consumeRecipientBudget("fresh@example.com", "password-reset", {
|
|
133
|
+
nowMs: T0,
|
|
134
|
+
}),
|
|
135
|
+
).toBe(true);
|
|
136
|
+
|
|
137
|
+
let firedNextHour = 0;
|
|
138
|
+
for (let i = 0; i < SITE_HOURLY_ALERT_THRESHOLD + 1; i++) {
|
|
139
|
+
if (siteVolumeAlert({ nowMs: T0 + ONE_HOUR_MS + 1 })) firedNextHour++;
|
|
140
|
+
}
|
|
141
|
+
expect(firedNextHour).toBe(1);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
* (a) CONTENT_ADAPTER unset → MDX adapter (existing behavior preserved)
|
|
6
6
|
* (b) CONTENT_ADAPTER=projection + valid manifest → projection adapter, read parity vs MDX
|
|
7
7
|
* (c) CONTENT_ADAPTER=projection + missing manifest → clear actionable error at construction
|
|
8
|
+
* (d) CONTENT_ADAPTER=database with no workspaceId → the resolver's named error
|
|
9
|
+
* (e) CONTENT_ADAPTER=database with a workspaceId → an adapter, not a throw (#5852)
|
|
8
10
|
*/
|
|
9
11
|
import { join, resolve } from "node:path";
|
|
10
12
|
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
11
13
|
import { tmpdir } from "node:os";
|
|
12
14
|
|
|
15
|
+
import { DEFAULT_WORKSPACE_ID } from "@working-theory/database/tenancy";
|
|
16
|
+
|
|
13
17
|
import {
|
|
14
18
|
getContentAdapter,
|
|
15
19
|
getContentRoot,
|
|
@@ -151,6 +155,25 @@ describe("getContentAdapter() factory", () => {
|
|
|
151
155
|
expect(() => getContentAdapter()).toThrow("pnpm content:compile");
|
|
152
156
|
});
|
|
153
157
|
});
|
|
158
|
+
|
|
159
|
+
describe("(d) CONTENT_ADAPTER=database → the resolver's workspaceId error", () => {
|
|
160
|
+
test("throws naming workspaceId, because apps/api does not resolve a tenant yet (#5323)", () => {
|
|
161
|
+
process.env.CONTENT_ADAPTER = "database";
|
|
162
|
+
|
|
163
|
+
expect(() => getContentAdapter()).toThrow(/workspaceId/);
|
|
164
|
+
expect(() => getContentAdapter()).toThrow(/CONTENT_ADAPTER=database/);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe("(e) CONTENT_ADAPTER=database + a workspaceId → an adapter (#5852)", () => {
|
|
169
|
+
test("builds the database adapter instead of throwing, once the caller resolves a tenant", () => {
|
|
170
|
+
process.env.CONTENT_ADAPTER = "database";
|
|
171
|
+
|
|
172
|
+
const adapter = getContentAdapter({ workspaceId: DEFAULT_WORKSPACE_ID });
|
|
173
|
+
expect(adapter).toBeDefined();
|
|
174
|
+
expect(typeof adapter.getPageBySlug).toBe("function");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
154
177
|
});
|
|
155
178
|
|
|
156
179
|
describe("getContentRoot()", () => {
|