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
|
@@ -62,11 +62,16 @@ jest.mock("@working-theory/license", () => ({
|
|
|
62
62
|
}),
|
|
63
63
|
}));
|
|
64
64
|
|
|
65
|
+
import { logger } from "@working-theory/observability";
|
|
65
66
|
import {
|
|
66
67
|
resolveConsentStatement,
|
|
67
68
|
SITE_CONSENT_CONFIG_DEFAULTS,
|
|
68
69
|
} from "@working-theory/validation";
|
|
69
70
|
|
|
71
|
+
import {
|
|
72
|
+
mockNoWorkspace,
|
|
73
|
+
mockWorkspaceExists,
|
|
74
|
+
} from "../../../lib/__mocks__/prisma.js";
|
|
70
75
|
import { prisma } from "../../../lib/prisma.js";
|
|
71
76
|
import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
|
|
72
77
|
import {
|
|
@@ -118,6 +123,48 @@ const START_PAYLOAD = {
|
|
|
118
123
|
consent: true,
|
|
119
124
|
};
|
|
120
125
|
|
|
126
|
+
/**
|
|
127
|
+
* #6314 — the magnets `capturedByFlowSlug` may name. The resolve runs against
|
|
128
|
+
* the RESOLVED workspace id (`resolveWorkspaceIdForSite(SITE_ID)`), which this
|
|
129
|
+
* suite's `mockWorkspaceExists(prisma, SITE_ID)` makes `SITE_ID` itself.
|
|
130
|
+
*/
|
|
131
|
+
const WS_OTHER = "wksp_00000000-0000-7000-8000-0000000000bb";
|
|
132
|
+
const LISTICLE_ID = "flow_00000000-0000-7000-8000-0000000000d1";
|
|
133
|
+
const FOREIGN_LISTICLE_ID = "flow_00000000-0000-7000-8000-0000000000d2";
|
|
134
|
+
const CONTACT_FLOW_ID = "flow_00000000-0000-7000-8000-0000000000d3";
|
|
135
|
+
|
|
136
|
+
const FLOW_DEFINITION_ROWS: Row[] = [
|
|
137
|
+
{
|
|
138
|
+
id: LISTICLE_ID,
|
|
139
|
+
siteId: SITE_ID,
|
|
140
|
+
workspaceId: SITE_ID,
|
|
141
|
+
slug: "founder-list",
|
|
142
|
+
kind: "listicle",
|
|
143
|
+
status: "live",
|
|
144
|
+
},
|
|
145
|
+
// A listicle magnet of ANOTHER workspace. Its slug is a public path segment,
|
|
146
|
+
// so a stranger can name it — the resolve's `siteId`/`workspaceId` terms are
|
|
147
|
+
// what make naming it inert.
|
|
148
|
+
{
|
|
149
|
+
id: FOREIGN_LISTICLE_ID,
|
|
150
|
+
siteId: WS_OTHER,
|
|
151
|
+
workspaceId: WS_OTHER,
|
|
152
|
+
slug: "other-list",
|
|
153
|
+
kind: "listicle",
|
|
154
|
+
status: "live",
|
|
155
|
+
},
|
|
156
|
+
// A NON-listicle magnet in THIS workspace: the `kind: "listicle"` term in
|
|
157
|
+
// the resolve is what stops an application being filed against it.
|
|
158
|
+
{
|
|
159
|
+
id: CONTACT_FLOW_ID,
|
|
160
|
+
siteId: SITE_ID,
|
|
161
|
+
workspaceId: SITE_ID,
|
|
162
|
+
slug: "contact-us",
|
|
163
|
+
kind: "contact",
|
|
164
|
+
status: "live",
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
121
168
|
/** Everything the ten steps after step one collect. */
|
|
122
169
|
const COMPLETE_APPLICATION = {
|
|
123
170
|
name: "My Project",
|
|
@@ -162,6 +209,13 @@ function matches(row: Row, where: Record<string, unknown>): boolean {
|
|
|
162
209
|
}
|
|
163
210
|
|
|
164
211
|
function installStore(): void {
|
|
212
|
+
// #6335 — the shared prisma mock no longer resolves ANY requested
|
|
213
|
+
// workspace id. `startProjectListingDraft` stamps the row's `workspaceId`
|
|
214
|
+
// by resolving the site it was filed against (`resolveWorkspaceIdForSite`,
|
|
215
|
+
// services/project-listings.ts), so this suite states the ONE site it is
|
|
216
|
+
// built around. Keyed on the requested id — nothing else resolves.
|
|
217
|
+
mockWorkspaceExists(prisma, SITE_ID);
|
|
218
|
+
|
|
165
219
|
rows = [];
|
|
166
220
|
nextId = 1;
|
|
167
221
|
|
|
@@ -215,6 +269,7 @@ function installStore(): void {
|
|
|
215
269
|
headline: null,
|
|
216
270
|
logoMarkUrl: null,
|
|
217
271
|
logoWordmarkUrl: null,
|
|
272
|
+
solution: null,
|
|
218
273
|
featured: false,
|
|
219
274
|
decidedAt: null,
|
|
220
275
|
decidedBy: null,
|
|
@@ -261,6 +316,16 @@ function installStore(): void {
|
|
|
261
316
|
(prisma.consentGrant.createMany as jest.Mock).mockImplementation(
|
|
262
317
|
({ data }: { data: Row[] }) => Promise.resolve({ count: data.length }),
|
|
263
318
|
);
|
|
319
|
+
|
|
320
|
+
// #6314 — the capturing-magnet resolve. Honours EVERY term the service
|
|
321
|
+
// passes, so a resolve that dropped `workspaceId` or `kind` would match a
|
|
322
|
+
// row it must not and fail the assertions below.
|
|
323
|
+
(prisma.flowDefinition.findFirst as jest.Mock).mockImplementation(
|
|
324
|
+
({ where }: { where: Record<string, unknown> }) =>
|
|
325
|
+
Promise.resolve(
|
|
326
|
+
FLOW_DEFINITION_ROWS.find((row) => matches(row, where)) ?? null,
|
|
327
|
+
),
|
|
328
|
+
);
|
|
264
329
|
}
|
|
265
330
|
|
|
266
331
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
@@ -479,6 +544,253 @@ describe("the draft lifecycle (#5036 D1)", () => {
|
|
|
479
544
|
expect(data[0]!["statement"]).not.toContain("You agree to being contacted");
|
|
480
545
|
});
|
|
481
546
|
|
|
547
|
+
// ── #6314 — the capturing magnet, written at START ────────────────────────
|
|
548
|
+
|
|
549
|
+
it("AC-5: a capturedByFlowSlug naming a live listicle magnet of the resolved site writes that magnet's ID, and the response body is byte-unchanged", async () => {
|
|
550
|
+
const res = await app.inject({
|
|
551
|
+
method: "POST",
|
|
552
|
+
url: "/v1/project-listings",
|
|
553
|
+
payload: { ...START_PAYLOAD, capturedByFlowSlug: "founder-list" },
|
|
554
|
+
});
|
|
555
|
+
expect(res.statusCode).toBe(201);
|
|
556
|
+
// The hard-floor response contract does not move: `{ id, resumeToken }`
|
|
557
|
+
// and nothing else. The column is NEVER echoed back to the anonymous
|
|
558
|
+
// caller (spec § 9 SEC-12).
|
|
559
|
+
expect(Object.keys(res.json().data).sort()).toEqual(["id", "resumeToken"]);
|
|
560
|
+
|
|
561
|
+
expect(rows[0]!["capturedByFlowId"]).toBe(LISTICLE_ID);
|
|
562
|
+
|
|
563
|
+
// D1 — ALL FOUR terms in ONE query, and only the slug came from the body.
|
|
564
|
+
const call = (prisma.flowDefinition.findFirst as jest.Mock).mock
|
|
565
|
+
.calls[0]?.[0] as { where: Record<string, unknown> };
|
|
566
|
+
expect(call.where).toEqual({
|
|
567
|
+
slug: "founder-list",
|
|
568
|
+
siteId: SITE_ID,
|
|
569
|
+
workspaceId: SITE_ID,
|
|
570
|
+
kind: "listicle",
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it("a start body carrying NO capturedByFlowSlug writes NULL, and makes no resolve query at all", async () => {
|
|
575
|
+
await startDraft(app);
|
|
576
|
+
expect(rows[0]!["capturedByFlowId"] ?? null).toBeNull();
|
|
577
|
+
expect(prisma.flowDefinition.findFirst).not.toHaveBeenCalled();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it.each([
|
|
581
|
+
["names nothing at all", "no-such-magnet"],
|
|
582
|
+
["names ANOTHER workspace's listicle magnet", "other-list"],
|
|
583
|
+
["names a NON-listicle magnet in this workspace", "contact-us"],
|
|
584
|
+
])(
|
|
585
|
+
"AC-6: a capturedByFlowSlug that %s writes NULL and still returns a 201 indistinguishable from a body that named no slug",
|
|
586
|
+
async (_label, slug) => {
|
|
587
|
+
const withSlug = await app.inject({
|
|
588
|
+
method: "POST",
|
|
589
|
+
url: "/v1/project-listings",
|
|
590
|
+
payload: { ...START_PAYLOAD, capturedByFlowSlug: slug },
|
|
591
|
+
});
|
|
592
|
+
expect(withSlug.statusCode).toBe(201);
|
|
593
|
+
expect(rows[0]!["capturedByFlowId"] ?? null).toBeNull();
|
|
594
|
+
|
|
595
|
+
// Indistinguishable on the wire from the no-slug case: same status, same
|
|
596
|
+
// key set, same envelope. A distinguishable error would turn this
|
|
597
|
+
// anonymous route into a cross-workspace magnet-slug oracle (SEC-02).
|
|
598
|
+
const withoutSlug = await app.inject({
|
|
599
|
+
method: "POST",
|
|
600
|
+
url: "/v1/project-listings",
|
|
601
|
+
payload: START_PAYLOAD,
|
|
602
|
+
});
|
|
603
|
+
expect(withoutSlug.statusCode).toBe(withSlug.statusCode);
|
|
604
|
+
expect(Object.keys(withoutSlug.json().data).sort()).toEqual(
|
|
605
|
+
Object.keys(withSlug.json().data).sort(),
|
|
606
|
+
);
|
|
607
|
+
},
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
it("AC-13: an UNRESOLVED slug is logged, PII-safe — routing metadata only, never the founder's identity or either credential", async () => {
|
|
611
|
+
const warn = jest.spyOn(logger, "warn").mockImplementation(() => undefined);
|
|
612
|
+
try {
|
|
613
|
+
const res = await app.inject({
|
|
614
|
+
method: "POST",
|
|
615
|
+
url: "/v1/project-listings",
|
|
616
|
+
payload: {
|
|
617
|
+
...START_PAYLOAD,
|
|
618
|
+
capturedByFlowSlug: "no-such-magnet",
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
expect(res.statusCode).toBe(201);
|
|
622
|
+
|
|
623
|
+
const call = warn.mock.calls.find(
|
|
624
|
+
(c) =>
|
|
625
|
+
typeof c[0] === "object" &&
|
|
626
|
+
c[0] !== null &&
|
|
627
|
+
"capturedByFlowSlug" in (c[0] as Record<string, unknown>),
|
|
628
|
+
);
|
|
629
|
+
expect(call).toBeDefined();
|
|
630
|
+
const meta = call![0] as Record<string, unknown>;
|
|
631
|
+
// Routing metadata ONLY. Silence here is what would make a stale or
|
|
632
|
+
// mis-wired consuming site produce a permanently empty Applications list
|
|
633
|
+
// indistinguishable from "no applications yet" (SEC-03).
|
|
634
|
+
expect(Object.keys(meta).sort()).toEqual([
|
|
635
|
+
"capturedByFlowSlug",
|
|
636
|
+
"listingId",
|
|
637
|
+
"siteId",
|
|
638
|
+
]);
|
|
639
|
+
const serialized = JSON.stringify(meta);
|
|
640
|
+
expect(serialized).not.toContain("ada@example.com");
|
|
641
|
+
expect(serialized).not.toContain("Ada Founder");
|
|
642
|
+
expect(serialized).not.toContain(rows[0]!["resumeTokenHash"]);
|
|
643
|
+
expect(serialized).not.toContain(rows[0]!["emailVerifyNonceHash"]);
|
|
644
|
+
} finally {
|
|
645
|
+
warn.mockRestore();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("AC-6: a capturedByFlowSlug lookup that THROWS degrades like an unresolvable slug — 201, NULL attribution, and a PII-safe warn distinct from the unresolved line", async () => {
|
|
650
|
+
(prisma.flowDefinition.findFirst as jest.Mock).mockRejectedValueOnce(
|
|
651
|
+
new Error("statement timeout"),
|
|
652
|
+
);
|
|
653
|
+
const warn = jest.spyOn(logger, "warn").mockImplementation(() => undefined);
|
|
654
|
+
try {
|
|
655
|
+
const res = await app.inject({
|
|
656
|
+
method: "POST",
|
|
657
|
+
url: "/v1/project-listings",
|
|
658
|
+
payload: { ...START_PAYLOAD, capturedByFlowSlug: "founder-list" },
|
|
659
|
+
});
|
|
660
|
+
expect(res.statusCode).toBe(201);
|
|
661
|
+
expect(rows[0]!["capturedByFlowId"] ?? null).toBeNull();
|
|
662
|
+
|
|
663
|
+
const call = warn.mock.calls.find(
|
|
664
|
+
(c) => typeof c[1] === "string" && c[1].includes("lookup failed"),
|
|
665
|
+
);
|
|
666
|
+
expect(call).toBeDefined();
|
|
667
|
+
const meta = call![0] as Record<string, unknown>;
|
|
668
|
+
expect(Object.keys(meta).sort()).toEqual([
|
|
669
|
+
"capturedByFlowSlug",
|
|
670
|
+
"err",
|
|
671
|
+
"workspaceId",
|
|
672
|
+
]);
|
|
673
|
+
// `JSON.stringify` cannot see an Error's NON-ENUMERABLE own properties —
|
|
674
|
+
// `message` and `stack` among them — so stringifying `meta` alone would
|
|
675
|
+
// pass whatever the error held and assert nothing. Pull them out by name.
|
|
676
|
+
const err = meta["err"] as Error;
|
|
677
|
+
const serialized = [
|
|
678
|
+
JSON.stringify(meta),
|
|
679
|
+
err?.message ?? "",
|
|
680
|
+
err?.stack ?? "",
|
|
681
|
+
].join("\n");
|
|
682
|
+
expect(serialized).not.toContain("ada@example.com");
|
|
683
|
+
expect(serialized).not.toContain("Ada Founder");
|
|
684
|
+
|
|
685
|
+
// The real guarantee is STRUCTURAL, not this assertion: the catch wraps
|
|
686
|
+
// only the `findFirst`, whose scope holds no founder field at all. This
|
|
687
|
+
// check is a tripwire over that property, never the proof of it.
|
|
688
|
+
const line = call![1] as string;
|
|
689
|
+
expect(line).toContain("lookup failed");
|
|
690
|
+
expect(line).not.toContain("resolved to no listicle magnet");
|
|
691
|
+
} finally {
|
|
692
|
+
warn.mockRestore();
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it("a lookup that THROWS does NOT also emit the unresolved-slug line — during a DB incident that would be a false magnet-configuration signal", async () => {
|
|
697
|
+
(prisma.flowDefinition.findFirst as jest.Mock).mockRejectedValueOnce(
|
|
698
|
+
new Error("statement timeout"),
|
|
699
|
+
);
|
|
700
|
+
const warn = jest.spyOn(logger, "warn").mockImplementation(() => undefined);
|
|
701
|
+
try {
|
|
702
|
+
const res = await app.inject({
|
|
703
|
+
method: "POST",
|
|
704
|
+
url: "/v1/project-listings",
|
|
705
|
+
payload: { ...START_PAYLOAD, capturedByFlowSlug: "founder-list" },
|
|
706
|
+
});
|
|
707
|
+
expect(res.statusCode).toBe(201);
|
|
708
|
+
expect(rows[0]!["capturedByFlowId"] ?? null).toBeNull();
|
|
709
|
+
|
|
710
|
+
const messages = warn.mock.calls.map((c) => String(c[1] ?? ""));
|
|
711
|
+
// Exactly one line, and it is the accurate one. The throw never
|
|
712
|
+
// established that no such magnet exists.
|
|
713
|
+
expect(
|
|
714
|
+
messages.filter((m) => m.includes("resolved to no listicle magnet")),
|
|
715
|
+
).toHaveLength(0);
|
|
716
|
+
expect(messages.filter((m) => m.includes("lookup failed"))).toHaveLength(1);
|
|
717
|
+
} finally {
|
|
718
|
+
warn.mockRestore();
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("a RESOLVED slug logs nothing — the warn line is the unresolved case's signal, not a per-application line", async () => {
|
|
723
|
+
const warn = jest.spyOn(logger, "warn").mockImplementation(() => undefined);
|
|
724
|
+
try {
|
|
725
|
+
await app.inject({
|
|
726
|
+
method: "POST",
|
|
727
|
+
url: "/v1/project-listings",
|
|
728
|
+
payload: { ...START_PAYLOAD, capturedByFlowSlug: "founder-list" },
|
|
729
|
+
});
|
|
730
|
+
expect(
|
|
731
|
+
warn.mock.calls.filter(
|
|
732
|
+
(c) =>
|
|
733
|
+
typeof c[0] === "object" &&
|
|
734
|
+
c[0] !== null &&
|
|
735
|
+
"capturedByFlowSlug" in (c[0] as Record<string, unknown>),
|
|
736
|
+
),
|
|
737
|
+
).toHaveLength(0);
|
|
738
|
+
} finally {
|
|
739
|
+
warn.mockRestore();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("AC-7: capturedByFlowId is WRITE-ONCE — no patch and no submit changes it, and no patch body can reach it", async () => {
|
|
744
|
+
const { id, resumeToken } = (await (async () => {
|
|
745
|
+
const res = await app.inject({
|
|
746
|
+
method: "POST",
|
|
747
|
+
url: "/v1/project-listings",
|
|
748
|
+
payload: { ...START_PAYLOAD, capturedByFlowSlug: "founder-list" },
|
|
749
|
+
});
|
|
750
|
+
expect(res.statusCode).toBe(201);
|
|
751
|
+
return res.json().data as { id: string; resumeToken: string };
|
|
752
|
+
})());
|
|
753
|
+
|
|
754
|
+
expect(rows[0]!["capturedByFlowId"]).toBe(LISTICLE_ID);
|
|
755
|
+
|
|
756
|
+
// A patch that tries to name the column outright is a `.strict()` 400 —
|
|
757
|
+
// the key is not on any patch schema, so no body can reach the column.
|
|
758
|
+
const injected = await app.inject({
|
|
759
|
+
method: "PATCH",
|
|
760
|
+
url: `/v1/project-listings/drafts/${id}`,
|
|
761
|
+
headers: bearer(resumeToken),
|
|
762
|
+
payload: { capturedByFlowId: FOREIGN_LISTICLE_ID },
|
|
763
|
+
});
|
|
764
|
+
expect(injected.statusCode).toBe(400);
|
|
765
|
+
expect(rows[0]!["capturedByFlowId"]).toBe(LISTICLE_ID);
|
|
766
|
+
|
|
767
|
+
// A legitimate patch leaves it untouched.
|
|
768
|
+
const patched = await app.inject({
|
|
769
|
+
method: "PATCH",
|
|
770
|
+
url: `/v1/project-listings/drafts/${id}`,
|
|
771
|
+
headers: bearer(resumeToken),
|
|
772
|
+
payload: { name: "My Project" },
|
|
773
|
+
});
|
|
774
|
+
expect(patched.statusCode).toBe(200);
|
|
775
|
+
expect(rows[0]!["capturedByFlowId"]).toBe(LISTICLE_ID);
|
|
776
|
+
|
|
777
|
+
// And so does submit.
|
|
778
|
+
await app.inject({
|
|
779
|
+
method: "PATCH",
|
|
780
|
+
url: `/v1/project-listings/drafts/${id}`,
|
|
781
|
+
headers: bearer(resumeToken),
|
|
782
|
+
payload: COMPLETE_APPLICATION,
|
|
783
|
+
});
|
|
784
|
+
const submitted = await app.inject({
|
|
785
|
+
method: "POST",
|
|
786
|
+
url: `/v1/project-listings/drafts/${id}/submit`,
|
|
787
|
+
headers: bearer(resumeToken),
|
|
788
|
+
});
|
|
789
|
+
expect(submitted.statusCode).toBe(200);
|
|
790
|
+
expect(rows[0]!["status"]).toBe("requested");
|
|
791
|
+
expect(rows[0]!["capturedByFlowId"]).toBe(LISTICLE_ID);
|
|
792
|
+
});
|
|
793
|
+
|
|
482
794
|
// ── patch ──
|
|
483
795
|
|
|
484
796
|
it("patch: a SINGLE-field patch round-trips", async () => {
|
|
@@ -884,3 +1196,42 @@ describe("the draft lifecycle (#5036 D1)", () => {
|
|
|
884
1196
|
await logging.close();
|
|
885
1197
|
});
|
|
886
1198
|
});
|
|
1199
|
+
|
|
1200
|
+
// ── #6335 — the explicit workspace setup above is load-bearing ────────────
|
|
1201
|
+
//
|
|
1202
|
+
// Every case in this file states that this suite's site has a workspace.
|
|
1203
|
+
// This one takes that away, and proves the statement was doing work rather
|
|
1204
|
+
// than decorating the setup: `startProjectListingDraft` resolves the site it
|
|
1205
|
+
// is filing against (`resolveWorkspaceIdForSite`, services/project-listings.ts)
|
|
1206
|
+
// and refuses BEFORE any row is written. With the shared prisma mock's old
|
|
1207
|
+
// permissive default in place this case could not be written at all — every
|
|
1208
|
+
// id resolved, so the fail-closed path was unreachable from a route test.
|
|
1209
|
+
describe("#6335 — start fails closed when the site names no workspace", () => {
|
|
1210
|
+
let app: FastifyInstance;
|
|
1211
|
+
|
|
1212
|
+
beforeEach(async () => {
|
|
1213
|
+
jest.clearAllMocks();
|
|
1214
|
+
installStore();
|
|
1215
|
+
mockNoWorkspace(prisma);
|
|
1216
|
+
app = await buildApp();
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
afterEach(async () => {
|
|
1220
|
+
await app.close();
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
it("answers 503 tenant_unresolved and creates no ProjectListing", async () => {
|
|
1224
|
+
const res = await app.inject({
|
|
1225
|
+
method: "POST",
|
|
1226
|
+
url: "/v1/project-listings",
|
|
1227
|
+
payload: START_PAYLOAD,
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
expect(res.statusCode).toBe(503);
|
|
1231
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe(
|
|
1232
|
+
"tenant_unresolved",
|
|
1233
|
+
);
|
|
1234
|
+
expect(prisma.projectListing.create).not.toHaveBeenCalled();
|
|
1235
|
+
expect(rows).toHaveLength(0);
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
@@ -56,6 +56,10 @@ import Fastify from "fastify";
|
|
|
56
56
|
import type { FastifyInstance } from "fastify";
|
|
57
57
|
|
|
58
58
|
import { setupLeadFileEnv } from "../../../__tests__/factories/lead-fixture";
|
|
59
|
+
import {
|
|
60
|
+
mockNoWorkspace,
|
|
61
|
+
mockWorkspaceExists,
|
|
62
|
+
} from "../../../lib/__mocks__/prisma.js";
|
|
59
63
|
import { prisma } from "../../../lib/prisma.js";
|
|
60
64
|
import { patchProjectListingDraftRoute } from "../patch-draft.js";
|
|
61
65
|
import { startProjectListingRoute } from "../start.js";
|
|
@@ -184,6 +188,13 @@ function matches(row: Row, where: Record<string, unknown>): boolean {
|
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
function installStore(): void {
|
|
191
|
+
// #6335 — the shared prisma mock no longer resolves ANY requested
|
|
192
|
+
// workspace id. `startProjectListingDraft` stamps the row's `workspaceId`
|
|
193
|
+
// by resolving the site it was filed against (`resolveWorkspaceIdForSite`,
|
|
194
|
+
// services/project-listings.ts), so this suite states the ONE site it is
|
|
195
|
+
// built around. Keyed on the requested id — nothing else resolves.
|
|
196
|
+
mockWorkspaceExists(prisma, SITE_ID);
|
|
197
|
+
|
|
187
198
|
rows = [];
|
|
188
199
|
nextId = 1;
|
|
189
200
|
resumeEmailEnabled = true;
|
|
@@ -254,6 +265,7 @@ function installStore(): void {
|
|
|
254
265
|
headline: null,
|
|
255
266
|
logoMarkUrl: null,
|
|
256
267
|
logoWordmarkUrl: null,
|
|
268
|
+
solution: null,
|
|
257
269
|
featured: false,
|
|
258
270
|
decidedAt: null,
|
|
259
271
|
decidedBy: null,
|
|
@@ -794,3 +806,42 @@ describe("the resume link's email verification (#5407)", () => {
|
|
|
794
806
|
});
|
|
795
807
|
});
|
|
796
808
|
});
|
|
809
|
+
|
|
810
|
+
// ── #6335 — the explicit workspace setup above is load-bearing ────────────
|
|
811
|
+
//
|
|
812
|
+
// Every case in this file states that this suite's site has a workspace.
|
|
813
|
+
// This one takes that away, and proves the statement was doing work rather
|
|
814
|
+
// than decorating the setup: `startProjectListingDraft` resolves the site it
|
|
815
|
+
// is filing against (`resolveWorkspaceIdForSite`, services/project-listings.ts)
|
|
816
|
+
// and refuses BEFORE any row is written. With the shared prisma mock's old
|
|
817
|
+
// permissive default in place this case could not be written at all — every
|
|
818
|
+
// id resolved, so the fail-closed path was unreachable from a route test.
|
|
819
|
+
describe("#6335 — start fails closed when the site names no workspace", () => {
|
|
820
|
+
let app: FastifyInstance;
|
|
821
|
+
|
|
822
|
+
beforeEach(async () => {
|
|
823
|
+
jest.clearAllMocks();
|
|
824
|
+
installStore();
|
|
825
|
+
mockNoWorkspace(prisma);
|
|
826
|
+
app = await buildApp();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
afterEach(async () => {
|
|
830
|
+
await app.close();
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it("answers 503 tenant_unresolved and creates no ProjectListing", async () => {
|
|
834
|
+
const res = await app.inject({
|
|
835
|
+
method: "POST",
|
|
836
|
+
url: "/v1/project-listings",
|
|
837
|
+
payload: START_PAYLOAD,
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
expect(res.statusCode).toBe(503);
|
|
841
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe(
|
|
842
|
+
"tenant_unresolved",
|
|
843
|
+
);
|
|
844
|
+
expect(prisma.projectListing.create).not.toHaveBeenCalled();
|
|
845
|
+
expect(rows).toHaveLength(0);
|
|
846
|
+
});
|
|
847
|
+
});
|
|
@@ -136,6 +136,7 @@ function row(fixture: ListingFixture) {
|
|
|
136
136
|
headline: "Atlas",
|
|
137
137
|
logoMarkUrl: null,
|
|
138
138
|
logoWordmarkUrl: null,
|
|
139
|
+
solution: null,
|
|
139
140
|
featured: false,
|
|
140
141
|
status: "requested",
|
|
141
142
|
consentAt: fixture.createdAt,
|
|
@@ -419,10 +420,12 @@ describe("GET /v1/project-listings/me — the response shape", () => {
|
|
|
419
420
|
expect(byId["prjl_ada_one"]).toBe(7);
|
|
420
421
|
expect(byId["prjl_ada_two"]).toBe(3);
|
|
421
422
|
expect(prisma.waitlistScore.count).toHaveBeenCalledWith({
|
|
422
|
-
|
|
423
|
+
// #6258 AC-6 — the fenced read now filters the CONSTRAINED column beside
|
|
424
|
+
// `siteId`; the two are equal by the workspace_site_agree CHECK.
|
|
425
|
+
where: { siteId: SITE_A, workspaceId: SITE_A, experimentKey: "atlas-waitlist" },
|
|
423
426
|
});
|
|
424
427
|
expect(prisma.waitlistScore.count).toHaveBeenCalledWith({
|
|
425
|
-
where: { siteId: SITE_B, experimentKey: "atlas-two-waitlist" },
|
|
428
|
+
where: { siteId: SITE_B, workspaceId: SITE_B, experimentKey: "atlas-two-waitlist" },
|
|
426
429
|
});
|
|
427
430
|
});
|
|
428
431
|
|
|
@@ -129,6 +129,7 @@ function makeRow(overrides: Partial<Record<string, unknown>>) {
|
|
|
129
129
|
headline: null,
|
|
130
130
|
logoMarkUrl: null,
|
|
131
131
|
logoWordmarkUrl: null,
|
|
132
|
+
solution: null,
|
|
132
133
|
featured: false,
|
|
133
134
|
consentAt: new Date("2026-08-01T00:00:00.000Z"),
|
|
134
135
|
issueUrl: null,
|
|
@@ -413,7 +414,9 @@ describe("GET /v1/project-listings/public", () => {
|
|
|
413
414
|
it("counts the waitlist through the listing's own experimentKey, in one hop", async () => {
|
|
414
415
|
await app.inject({ method: "GET", url: `/v1/project-listings/public?siteKey=${SITE_KEY}` });
|
|
415
416
|
expect(prisma.waitlistScore.count).toHaveBeenCalledWith({
|
|
416
|
-
|
|
417
|
+
// #6258 AC-6 — the fenced read now filters the CONSTRAINED column
|
|
418
|
+
// beside `siteId`; the two are equal by the workspace_site_agree CHECK.
|
|
419
|
+
where: { siteId: SITE, workspaceId: SITE, experimentKey: "exp-newest" },
|
|
417
420
|
});
|
|
418
421
|
// The id-to-key translation read is gone with #5036 D6d.
|
|
419
422
|
expect(prisma.waitlistExperiment.findMany).not.toHaveBeenCalled();
|
|
@@ -57,6 +57,10 @@ jest.mock("@working-theory/observability", () => ({
|
|
|
57
57
|
},
|
|
58
58
|
}));
|
|
59
59
|
|
|
60
|
+
import {
|
|
61
|
+
mockNoWorkspace,
|
|
62
|
+
mockWorkspaceExists,
|
|
63
|
+
} from "../../../lib/__mocks__/prisma.js";
|
|
60
64
|
import { prisma } from "../../../lib/prisma.js";
|
|
61
65
|
import { startProjectListingRoute } from "../start.js";
|
|
62
66
|
|
|
@@ -108,6 +112,13 @@ function matches(row: Row, where: Record<string, unknown>): boolean {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
function installStore(): void {
|
|
115
|
+
// #6335 — the shared prisma mock no longer resolves ANY requested
|
|
116
|
+
// workspace id. `startProjectListingDraft` stamps the row's `workspaceId`
|
|
117
|
+
// by resolving the site it was filed against (`resolveWorkspaceIdForSite`,
|
|
118
|
+
// services/project-listings.ts), so this suite states the ONE site it is
|
|
119
|
+
// built around. Keyed on the requested id — nothing else resolves.
|
|
120
|
+
mockWorkspaceExists(prisma, SITE_ID);
|
|
121
|
+
|
|
111
122
|
rows = [];
|
|
112
123
|
nextId = 1;
|
|
113
124
|
storedConfig = null;
|
|
@@ -516,6 +527,11 @@ describe("the resume email's send (#5406)", () => {
|
|
|
516
527
|
let capSite = 0;
|
|
517
528
|
beforeEach(() => {
|
|
518
529
|
resolvedSiteId = `site-cap-${++capSite}`;
|
|
530
|
+
// #6335 — this describe files each test against a FRESH site, so the
|
|
531
|
+
// existence statement follows `resolvedSiteId` rather than pinning
|
|
532
|
+
// `SITE_ID`. Still keyed on the requested id: every other one
|
|
533
|
+
// resolves to nothing.
|
|
534
|
+
mockWorkspaceExists(prisma, resolvedSiteId);
|
|
519
535
|
storedConfig = { resumeEmail: { ...RESUME_EMAIL_ON, maxSendsPerHour: 3 } };
|
|
520
536
|
});
|
|
521
537
|
|
|
@@ -600,6 +616,9 @@ describe("the resume email's send (#5406)", () => {
|
|
|
600
616
|
|
|
601
617
|
// A second site, its own config row, its own empty budget.
|
|
602
618
|
const OTHER_SITE = "site-2";
|
|
619
|
+
// #6335 — named explicitly, beside this test's own site: the draft is
|
|
620
|
+
// filed against OTHER_SITE, so its workspace must resolve too.
|
|
621
|
+
mockWorkspaceExists(prisma, resolvedSiteId, OTHER_SITE);
|
|
603
622
|
(prisma.siteListingConfig.findUnique as jest.Mock).mockImplementation(
|
|
604
623
|
({ where }: { where: { siteId?: string; publicKey?: string } }) => {
|
|
605
624
|
if (where.publicKey !== undefined) {
|
|
@@ -812,3 +831,42 @@ describe("the resume email's send (#5406)", () => {
|
|
|
812
831
|
expect(logged).not.toContain("hello@samhenry.org");
|
|
813
832
|
});
|
|
814
833
|
});
|
|
834
|
+
|
|
835
|
+
// ── #6335 — the explicit workspace setup above is load-bearing ────────────
|
|
836
|
+
//
|
|
837
|
+
// Every case in this file states that this suite's site has a workspace.
|
|
838
|
+
// This one takes that away, and proves the statement was doing work rather
|
|
839
|
+
// than decorating the setup: `startProjectListingDraft` resolves the site it
|
|
840
|
+
// is filing against (`resolveWorkspaceIdForSite`, services/project-listings.ts)
|
|
841
|
+
// and refuses BEFORE any row is written. With the shared prisma mock's old
|
|
842
|
+
// permissive default in place this case could not be written at all — every
|
|
843
|
+
// id resolved, so the fail-closed path was unreachable from a route test.
|
|
844
|
+
describe("#6335 — start fails closed when the site names no workspace", () => {
|
|
845
|
+
let app: FastifyInstance;
|
|
846
|
+
|
|
847
|
+
beforeEach(async () => {
|
|
848
|
+
jest.clearAllMocks();
|
|
849
|
+
installStore();
|
|
850
|
+
mockNoWorkspace(prisma);
|
|
851
|
+
app = await buildApp();
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
afterEach(async () => {
|
|
855
|
+
await app.close();
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("answers 503 tenant_unresolved and creates no ProjectListing", async () => {
|
|
859
|
+
const res = await app.inject({
|
|
860
|
+
method: "POST",
|
|
861
|
+
url: "/v1/project-listings",
|
|
862
|
+
payload: START_PAYLOAD,
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(res.statusCode).toBe(503);
|
|
866
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe(
|
|
867
|
+
"tenant_unresolved",
|
|
868
|
+
);
|
|
869
|
+
expect(prisma.projectListing.create).not.toHaveBeenCalled();
|
|
870
|
+
expect(rows).toHaveLength(0);
|
|
871
|
+
});
|
|
872
|
+
});
|