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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file owner-workspaces.ts
|
|
3
|
+
* @description The ONE site→workspace resolution in `apps/api`.
|
|
4
|
+
*
|
|
5
|
+
* A `Lead` is keyed on `siteId` and a `Workspace` on `id`, but the two are the
|
|
6
|
+
* SAME value space named per namespace — `database/config/schema.prisma` says
|
|
7
|
+
* as much of `ProjectListing`, "the one keyed on `siteId` rather than
|
|
8
|
+
* `workspaceId`". So ownership is the direct membership test
|
|
9
|
+
* `siteId ∈ workspaceIds`, never a join through a mapping table, and every
|
|
10
|
+
* caller that needs it resolves it here rather than writing a second path: a
|
|
11
|
+
* divergent isolation path across two PII surfaces is exactly the thing that
|
|
12
|
+
* turns into rework when the multi-tenant product arrives.
|
|
13
|
+
*
|
|
14
|
+
* Callers: `lead-promotion.ts` (lead → user promotion, #3586) and
|
|
15
|
+
* `leads-inbox.ts` (the owner's leads read, #5678).
|
|
16
|
+
*
|
|
17
|
+
* ## Why this is its own file rather than a `lead-promotion.ts` export
|
|
18
|
+
*
|
|
19
|
+
* `paidModuleTemplateExclusions()` (`packages/create-ailk/src/module-architecture.ts`)
|
|
20
|
+
* walks the import graph from each paid module's `ownedFiles` to a fixed point
|
|
21
|
+
* and drops every file that reaches one, so that a keyless scaffold carries no
|
|
22
|
+
* paid source. `lead-promotion.ts` is inside that closure and always has been,
|
|
23
|
+
* which means anything importing it is dropped too. Putting this function
|
|
24
|
+
* there and importing it from the leads read would have carried the closure
|
|
25
|
+
* through `leads-inbox.ts` and `routes/leads/list.ts` into `server.ts`, and a
|
|
26
|
+
* scaffold with no server entry point does not build — the same base-breaking
|
|
27
|
+
* cascade `module-architecture.ts` records twice in its own comments, measured
|
|
28
|
+
* here before this file existed. Keeping the resolution in a module that
|
|
29
|
+
* reaches nothing excluded is what lets both callers share it.
|
|
30
|
+
*
|
|
31
|
+
* Nothing in this file may name `tenant-context.js` in an import form,
|
|
32
|
+
* including inside a comment: the closure's scanner is a regular expression
|
|
33
|
+
* over the file's text and does not know which lines are code.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { Id } from "@working-theory/ids";
|
|
37
|
+
|
|
38
|
+
import { isModuleEnabled } from "../config/modules.js";
|
|
39
|
+
import { loadSlice } from "../lib/slice-load.js";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The tenant-context module is a multi-tenant SLICE the OSS strip removes
|
|
43
|
+
* (#5182), and `listTenancy` is a VALUE — a runtime call, not an erasable type
|
|
44
|
+
* — so a static import, or a specifier written literally at the call site,
|
|
45
|
+
* would be TS2307 on the public tree: `tsc` resolves a string-literal
|
|
46
|
+
* specifier whether or not the branch containing it can ever run.
|
|
47
|
+
*
|
|
48
|
+
* The remedy is the one `server.ts` and `lead-promotion.ts` already use: gate
|
|
49
|
+
* the call on `isModuleEnabled('multi-tenant')` and read the specifier from
|
|
50
|
+
* this constant, so it is not a literal at the call site. What defeats
|
|
51
|
+
* resolution is the INDIRECTION, not the type of the value. The constant form
|
|
52
|
+
* matters too — `@vercel/nft` constant-propagates a module-level binding into
|
|
53
|
+
* the dynamic call and emits the file, which a table lookup would hide from it
|
|
54
|
+
* (#5304 D3).
|
|
55
|
+
*
|
|
56
|
+
* `apps/api` is a `tsc`-only surface with no bundler, which is the
|
|
57
|
+
* precondition that makes this sound here.
|
|
58
|
+
*/
|
|
59
|
+
const TENANT_CONTEXT_SPECIFIER = "./tenant-context.js";
|
|
60
|
+
|
|
61
|
+
/** The one export this file needs from the multi-tenant slice. */
|
|
62
|
+
interface TenantContextSlice {
|
|
63
|
+
listTenancy: (params: {
|
|
64
|
+
userId: Id<"user">;
|
|
65
|
+
sessionTenant: undefined;
|
|
66
|
+
}) => Promise<
|
|
67
|
+
| {
|
|
68
|
+
ok: true;
|
|
69
|
+
value: {
|
|
70
|
+
organizations: Array<{
|
|
71
|
+
role: string;
|
|
72
|
+
workspaces: Array<{ id: string }>;
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
| { ok: false; error: { message: string } }
|
|
77
|
+
>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One workspace the caller belongs to, carrying the role they hold in its
|
|
82
|
+
* organization. `role` is the organization-level `Membership.role`
|
|
83
|
+
* (`owner | admin | member`) the tenancy assembler already returns — typed as a
|
|
84
|
+
* plain string here rather than `MembershipRole`, because that type lives in
|
|
85
|
+
* the stripped slice this file may not name.
|
|
86
|
+
*/
|
|
87
|
+
export type OwnerWorkspace = { id: string; role: string };
|
|
88
|
+
|
|
89
|
+
export type ResolveOwnerWorkspacesResult =
|
|
90
|
+
| { ok: true; value: OwnerWorkspace[] }
|
|
91
|
+
| { ok: false; error: { message: string } };
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Resolve the caller's OWN workspaces.
|
|
95
|
+
*
|
|
96
|
+
* Delegates to the sole cross-namespace tenancy assembler — this file never
|
|
97
|
+
* issues a raw membership or workspace model query of its own, which
|
|
98
|
+
* `architecture.yaml`'s tenant-scope forbidden pattern reserves to the
|
|
99
|
+
* tenant-context and tenant-db modules.
|
|
100
|
+
*
|
|
101
|
+
* Module-conditional, and #5677 changed what that MEANS rather than what it
|
|
102
|
+
* does. `services/default-tenant.ts` now resolves the seeded default
|
|
103
|
+
* Workspace on every build, so a module-less deployment does have a
|
|
104
|
+
* resolvable workspace — the empty result below is a deliberate scope limit,
|
|
105
|
+
* not a statement that there is nothing to resolve. Wiring this resolver to
|
|
106
|
+
* that default is a behavior change for BOTH callers at once (promotion would
|
|
107
|
+
* begin stamping `promotedUserId` on single-tenant leads, which
|
|
108
|
+
* `/v1/flow-checkouts/me` and `/v1/waitlist-signups/me` join through), and it
|
|
109
|
+
* needs the deployment to pin `AILK_SITE_ID` to `DEFAULT_WORKSPACE_ID` —
|
|
110
|
+
* `siteId` comes from `lib/site.ts` while `workspaceId` is the sentinel, and
|
|
111
|
+
* `siteId ∈ workspaceIds` only matches when those agree.
|
|
112
|
+
*/
|
|
113
|
+
export async function resolveOwnerWorkspaces(
|
|
114
|
+
userId: Id<"user">,
|
|
115
|
+
): Promise<ResolveOwnerWorkspacesResult> {
|
|
116
|
+
if (!isModuleEnabled("multi-tenant")) return { ok: true, value: [] };
|
|
117
|
+
|
|
118
|
+
const specifier: string = TENANT_CONTEXT_SPECIFIER;
|
|
119
|
+
const { listTenancy } = (await loadSlice(
|
|
120
|
+
specifier,
|
|
121
|
+
() => import(specifier),
|
|
122
|
+
)) as TenantContextSlice;
|
|
123
|
+
const tenancy = await listTenancy({ userId, sessionTenant: undefined });
|
|
124
|
+
if (!tenancy.ok) return { ok: false, error: tenancy.error };
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
value: tenancy.value.organizations.flatMap((org) =>
|
|
129
|
+
org.workspaces.map((workspace) => ({ id: workspace.id, role: org.role })),
|
|
130
|
+
),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
import { createHash, randomBytes } from "node:crypto";
|
|
45
45
|
|
|
46
46
|
import type { Prisma, Role } from "@working-theory/database";
|
|
47
|
+
import {
|
|
48
|
+
DEFAULT_WORKSPACE_SLUG,
|
|
49
|
+
resolveSentinelWorkspaceRow,
|
|
50
|
+
} from "@working-theory/database/tenancy";
|
|
47
51
|
import { logger } from "@working-theory/observability";
|
|
48
52
|
import {
|
|
49
53
|
clampWindowDays,
|
|
@@ -217,6 +221,7 @@ const PROJECT_LISTING_SELECT = {
|
|
|
217
221
|
name: true,
|
|
218
222
|
oneLiner: true,
|
|
219
223
|
problem: true,
|
|
224
|
+
solution: true,
|
|
220
225
|
url: true,
|
|
221
226
|
product: true,
|
|
222
227
|
commitment: true,
|
|
@@ -362,6 +367,7 @@ function toResponseRow(
|
|
|
362
367
|
headline: row.headline,
|
|
363
368
|
oneLiner: row.oneLiner,
|
|
364
369
|
problem: row.problem,
|
|
370
|
+
solution: row.solution,
|
|
365
371
|
url: row.url,
|
|
366
372
|
product: row.product as ProjectListing["product"],
|
|
367
373
|
commitment: row.commitment as ProjectListing["commitment"],
|
|
@@ -428,8 +434,21 @@ async function attachWaitlistCounts(
|
|
|
428
434
|
const counts = new Map<string, number>();
|
|
429
435
|
await Promise.all(
|
|
430
436
|
experimentKeys.map(async (experimentKey) => {
|
|
437
|
+
// fence: workspaceId === the scope id this query already filters siteId by
|
|
438
|
+
// (workspace_site_agree CHECK, 20260922120000_tenancy_fence). Filtering the
|
|
439
|
+
// CONSTRAINED column makes the scope correct by construction; the format
|
|
440
|
+
// CHECK makes a raw alias match nothing rather than by accident.
|
|
441
|
+
//
|
|
442
|
+
// What that id IS depends on the caller, and only two of the six are
|
|
443
|
+
// route-resolved: `listProjectListings` and `getProjectListing` receive
|
|
444
|
+
// `tenant.workspaceId`, so for them this is an isolation control.
|
|
445
|
+
// `listMyProjectListings` and the two `publicProjectListings` call sites
|
|
446
|
+
// pass a ProjectListing row's OWN `siteId`, so for them it is a
|
|
447
|
+
// CONSISTENCY control — the count agrees with the row it is reported
|
|
448
|
+
// against — exactly as `project-listing-decision.ts#waitlistCountFor`
|
|
449
|
+
// spells out (#6258 § 1.6 / § 5 D8, security review L-2).
|
|
431
450
|
const count = await prisma.waitlistScore.count({
|
|
432
|
-
where: { siteId, experimentKey },
|
|
451
|
+
where: { siteId, workspaceId: siteId, experimentKey },
|
|
433
452
|
});
|
|
434
453
|
counts.set(experimentKey, count);
|
|
435
454
|
}),
|
|
@@ -602,7 +621,113 @@ export type StartProjectListingDraftResult =
|
|
|
602
621
|
*/
|
|
603
622
|
verifyNonce: string;
|
|
604
623
|
}
|
|
605
|
-
| { ok: false; error: { kind: "invalid_input"; message: string } }
|
|
624
|
+
| { ok: false; error: { kind: "invalid_input"; message: string } }
|
|
625
|
+
// #6203 — the resolved `siteId` names no workspace this database holds.
|
|
626
|
+
// Distinct from `invalid_input` (a body problem): this is a server-side
|
|
627
|
+
// tenancy misconfiguration, and the route answers 503, never 400.
|
|
628
|
+
| { ok: false; error: { kind: "tenant_unresolved"; message: string } };
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Resolves an ALREADY-RESOLVED `siteId` (never a caller-supplied string — see
|
|
632
|
+
* `startProjectListingDraft`'s own header) to its workspace id (#6203).
|
|
633
|
+
*
|
|
634
|
+
* Mirrors `apps/api/src/lib/site.ts`'s `resolveSiteWorkspaceId()` — the SAME
|
|
635
|
+
* two-branch alias mapping the #6203 migration's own backfill `CASE`
|
|
636
|
+
* encodes: the literal alias `"default"` resolves to the seeded sentinel
|
|
637
|
+
* workspace IFF that row exists and hangs off the sentinel Organization;
|
|
638
|
+
* any other alias resolves only by exact `workspaces.id` equality. Kept
|
|
639
|
+
* LOCAL and PARAMETERIZED rather than reusing `resolveSiteWorkspaceId()`
|
|
640
|
+
* directly: that function is hardwired to `currentSiteId()`
|
|
641
|
+
* (`AILK_SITE_ID`), while a `ProjectListing`'s `siteId` can legitimately
|
|
642
|
+
* name a DIFFERENT site under a multi-key deployment
|
|
643
|
+
* (`listing-config.ts#siteIdForPublicKey`'s `SiteListingConfig.publicKey`
|
|
644
|
+
* path) — the workspace stamped here must correspond to the SAME site the
|
|
645
|
+
* row's own `siteId` already names, not the deployment's default. NOT
|
|
646
|
+
* memoized (unlike `resolveSiteWorkspaceId()`): a different `siteId` per
|
|
647
|
+
* call means the sentinel-only cache that function keeps would be wrong
|
|
648
|
+
* here.
|
|
649
|
+
*/
|
|
650
|
+
async function resolveWorkspaceIdForSite(siteId: string): Promise<string | null> {
|
|
651
|
+
if (siteId === DEFAULT_WORKSPACE_SLUG) {
|
|
652
|
+
const row = await resolveSentinelWorkspaceRow(prisma);
|
|
653
|
+
return row?.id ?? null;
|
|
654
|
+
}
|
|
655
|
+
const row = await prisma.workspace.findUnique({ where: { id: siteId } });
|
|
656
|
+
return row ? siteId : null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* #6314 — resolve the capturing listicle magnet, SERVER-SIDE, from the slug
|
|
661
|
+
* the body named.
|
|
662
|
+
*
|
|
663
|
+
* All four terms in ONE query, and only `slug` came from the caller:
|
|
664
|
+
* `siteId` and `workspaceId` are the value `startProjectListingDraft` has
|
|
665
|
+
* already resolved from the site's PUBLIC KEY (`siteIdForPublicKey`, then
|
|
666
|
+
* `resolveWorkspaceIdForSite`), never from the body. `@@unique([siteId,
|
|
667
|
+
* slug])` makes the match at most one row.
|
|
668
|
+
*
|
|
669
|
+
* `kind: "listicle"` is LOAD-BEARING, not cosmetic: without it a visitor
|
|
670
|
+
* could name a contact or newsletter magnet's slug — both are public path
|
|
671
|
+
* segments — and file an application against it.
|
|
672
|
+
*
|
|
673
|
+
* NO `status` term, deliberately. A magnet can be flipped `live -> draft`
|
|
674
|
+
* while a visitor is mid-application, and dropping attribution on that flip
|
|
675
|
+
* would silently punish the operator's own edit. The cost is bounded: a draft
|
|
676
|
+
* magnet's slug is the operator's own, the resulting row lands only in that
|
|
677
|
+
* operator's own workspace, and the Applications wire row carries `status`
|
|
678
|
+
* so a never-submitted row stays legible as one.
|
|
679
|
+
*
|
|
680
|
+
* Returns `unresolved` for a slug that names nothing, names another
|
|
681
|
+
* workspace's magnet, or names a magnet of the wrong kind — those three are
|
|
682
|
+
* deliberately indistinguishable, and all three write NULL.
|
|
683
|
+
*
|
|
684
|
+
* `failed` is a FOURTH outcome and is NOT one of them: the lookup itself threw.
|
|
685
|
+
* It also writes NULL, so the wire is unchanged, but the caller must not
|
|
686
|
+
* describe it as "no such magnet" — during a database incident that would send
|
|
687
|
+
* an operator hunting a magnet-configuration problem that does not exist. This
|
|
688
|
+
* function has already logged the accurate line by the time it returns `failed`.
|
|
689
|
+
*/
|
|
690
|
+
type CapturingFlowResolution =
|
|
691
|
+
| { kind: "resolved"; id: string }
|
|
692
|
+
| { kind: "unresolved" }
|
|
693
|
+
| { kind: "failed" };
|
|
694
|
+
|
|
695
|
+
async function resolveCapturingFlowId(
|
|
696
|
+
workspaceId: string,
|
|
697
|
+
slug: string,
|
|
698
|
+
): Promise<CapturingFlowResolution> {
|
|
699
|
+
try {
|
|
700
|
+
const row = await prisma.flowDefinition.findFirst({
|
|
701
|
+
where: {
|
|
702
|
+
slug,
|
|
703
|
+
// Both scope terms, both from the value resolved above. They are equal
|
|
704
|
+
// by the `flow_definitions_workspace_site_agree` CHECK the fence added;
|
|
705
|
+
// carrying both is what makes the scope explicit at the query rather
|
|
706
|
+
// than implied by a constraint a future migration could move.
|
|
707
|
+
siteId: workspaceId,
|
|
708
|
+
workspaceId,
|
|
709
|
+
kind: "listicle",
|
|
710
|
+
},
|
|
711
|
+
select: { id: true },
|
|
712
|
+
});
|
|
713
|
+
return row === null ? { kind: "unresolved" } : { kind: "resolved", id: row.id };
|
|
714
|
+
} catch (err) {
|
|
715
|
+
// The lookup is OPTIONAL attribution on a never-gate anonymous route: an
|
|
716
|
+
// unavailable read (statement timeout, connection blip) must degrade
|
|
717
|
+
// exactly like a slug that names nothing — NULL, and the application is
|
|
718
|
+
// kept — rather than turn into a 500 that loses a real founder's
|
|
719
|
+
// submission. Both outcomes are NULL on the row, so this line is the only
|
|
720
|
+
// thing that tells an operator "lookup failed" from "no such magnet".
|
|
721
|
+
// PII-SAFE, same idiom as the unresolved-slug line below: routing
|
|
722
|
+
// metadata plus the caught error; never the founder's identity, the
|
|
723
|
+
// resume token or the verify nonce (none is in scope here).
|
|
724
|
+
logger.warn(
|
|
725
|
+
{ workspaceId, capturedByFlowSlug: slug, err },
|
|
726
|
+
"[project-listings] capturedByFlowSlug lookup failed; application stored unattributed",
|
|
727
|
+
);
|
|
728
|
+
return { kind: "failed" };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
606
731
|
|
|
607
732
|
/**
|
|
608
733
|
* Create a `draft` from step one — the founder's name, email and consent.
|
|
@@ -621,11 +746,27 @@ export type StartProjectListingDraftResult =
|
|
|
621
746
|
*
|
|
622
747
|
* No slug yet: it is derived from `name` at SUBMIT (#5033 D2), which is the
|
|
623
748
|
* first moment a name exists.
|
|
749
|
+
*
|
|
750
|
+
* WORKSPACE STAMP (#6203). The row's `workspaceId` is resolved from `siteId`
|
|
751
|
+
* itself (`resolveWorkspaceIdForSite` above) — never a client-supplied
|
|
752
|
+
* value, and never a fixed deployment default. An unresolved result rejects
|
|
753
|
+
* the write before any row is created (spec AC-4).
|
|
624
754
|
*/
|
|
625
755
|
export async function startProjectListingDraft(
|
|
626
756
|
siteId: string,
|
|
627
757
|
input: ProjectListingStartInput,
|
|
628
758
|
): Promise<StartProjectListingDraftResult> {
|
|
759
|
+
const workspaceId = await resolveWorkspaceIdForSite(siteId);
|
|
760
|
+
if (workspaceId === null) {
|
|
761
|
+
return {
|
|
762
|
+
ok: false,
|
|
763
|
+
error: {
|
|
764
|
+
kind: "tenant_unresolved",
|
|
765
|
+
message: "This site is not configured to accept applications.",
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
629
770
|
const { requestedWindowDays } = input;
|
|
630
771
|
// #5407 — the NOT NULL display column. `composeFounderName` builds it from
|
|
631
772
|
// the PARTS wherever both are written (even beside a `founderName` the body
|
|
@@ -684,10 +825,30 @@ export async function startProjectListingDraft(
|
|
|
684
825
|
// wording, which nobody was shown.
|
|
685
826
|
const consents = input.consents;
|
|
686
827
|
const locale = input.locale ?? "en";
|
|
828
|
+
// #6314 — the capturing magnet, resolved SERVER-SIDE from the slug the body
|
|
829
|
+
// named. Only runs when the key is present, so an existing caller costs
|
|
830
|
+
// nothing; when it is, it is one probe of `@@unique([siteId, slug])`.
|
|
831
|
+
const capture =
|
|
832
|
+
input.capturedByFlowSlug === undefined
|
|
833
|
+
? undefined
|
|
834
|
+
: await resolveCapturingFlowId(workspaceId, input.capturedByFlowSlug);
|
|
835
|
+
const capturedByFlowId = capture?.kind === "resolved" ? capture.id : null;
|
|
687
836
|
const row = await prisma.$transaction(async (tx) => {
|
|
688
837
|
const created = await tx.projectListing.create({
|
|
689
838
|
data: {
|
|
690
|
-
|
|
839
|
+
// #6254 AC-3/AC-6/AC-10 — the RESOLVED workspace id, not the raw
|
|
840
|
+
// `siteId` parameter: `project_listings` now requires
|
|
841
|
+
// `workspace_id = site_id`
|
|
842
|
+
// (database/migrations/20260922120000_tenancy_fence), and this
|
|
843
|
+
// column is the isolation key every downstream reader (including
|
|
844
|
+
// `submitProjectListingDraft`'s `routeLead` call) reads back as
|
|
845
|
+
// `draft.siteId`. `siteId` (the public-facing param, possibly the
|
|
846
|
+
// raw `"default"` alias) is still used above to look up
|
|
847
|
+
// `listingConfigFor`/`consentConfigForSite` — those are independent,
|
|
848
|
+
// unfenced reads keyed on the site concept, not this row.
|
|
849
|
+
siteId: workspaceId,
|
|
850
|
+
// #6203 — the isolation key, resolved above from this SAME siteId.
|
|
851
|
+
workspaceId,
|
|
691
852
|
founderName,
|
|
692
853
|
// #5407 — the parts as sent, beside the composed line. Absent where a
|
|
693
854
|
// caller sent only a display line, which is the shape every row
|
|
@@ -705,6 +866,11 @@ export async function startProjectListingDraft(
|
|
|
705
866
|
resumeTokenHash: hashResumeToken(resumeToken),
|
|
706
867
|
emailVerifyNonceHash: hashVerifyNonce(verifyNonce),
|
|
707
868
|
windowDays,
|
|
869
|
+
// #6314 — WRITE-ONCE, and only here. `draftUpdateData` never learns
|
|
870
|
+
// this key, so no patch body and no submit can re-attribute a draft
|
|
871
|
+
// to a different magnet after the fact — not by its holder, and not
|
|
872
|
+
// by anyone who obtains its resume token.
|
|
873
|
+
...(capturedByFlowId !== null && { capturedByFlowId }),
|
|
708
874
|
},
|
|
709
875
|
select: { id: true },
|
|
710
876
|
});
|
|
@@ -717,6 +883,33 @@ export async function startProjectListingDraft(
|
|
|
717
883
|
return created;
|
|
718
884
|
});
|
|
719
885
|
|
|
886
|
+
// #6314 — an unresolved slug is the operator's ONLY signal, precisely
|
|
887
|
+
// because the response deliberately carries none (a distinguishable error
|
|
888
|
+
// would be a cross-workspace magnet-slug oracle on an anonymous route).
|
|
889
|
+
// Without this line, a mis-wired or stale consuming site yields a
|
|
890
|
+
// permanently empty Applications list an operator cannot tell from "no
|
|
891
|
+
// applications yet".
|
|
892
|
+
//
|
|
893
|
+
// PII-SAFE, in this route family's existing idiom: routing metadata only.
|
|
894
|
+
// Never `founderEmail`, `founderName`, the resume token or the verify
|
|
895
|
+
// nonce — the last two never leave this function except inside the email.
|
|
896
|
+
// ONLY the `unresolved` outcome, never `failed`. Both store NULL and both
|
|
897
|
+
// return the same 201, but only one of them establishes "no listicle magnet
|
|
898
|
+
// of that slug in this site". Asserting it on the throw path would put a
|
|
899
|
+
// false magnet-configuration signal into the log during a database incident,
|
|
900
|
+
// which is exactly when an operator is grepping for a cause. `failed` has its
|
|
901
|
+
// own, accurate line, emitted inside `resolveCapturingFlowId`.
|
|
902
|
+
if (capture?.kind === "unresolved") {
|
|
903
|
+
logger.warn(
|
|
904
|
+
{
|
|
905
|
+
siteId,
|
|
906
|
+
listingId: row.id,
|
|
907
|
+
capturedByFlowSlug: input.capturedByFlowSlug,
|
|
908
|
+
},
|
|
909
|
+
"[project-listings] capturedByFlowSlug resolved to no listicle magnet in this site; application stored unattributed",
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
|
|
720
913
|
return { ok: true, id: row.id, resumeToken, verifyNonce };
|
|
721
914
|
}
|
|
722
915
|
|
|
@@ -891,6 +1084,7 @@ function draftUpdateData(
|
|
|
891
1084
|
...(patch.headline !== undefined && { headline: patch.headline }),
|
|
892
1085
|
...(patch.oneLiner !== undefined && { oneLiner: patch.oneLiner }),
|
|
893
1086
|
...(patch.problem !== undefined && { problem: patch.problem }),
|
|
1087
|
+
...(patch.solution !== undefined && { solution: patch.solution }),
|
|
894
1088
|
...(patch.url !== undefined && { url: patch.url }),
|
|
895
1089
|
...(patch.product !== undefined && { product: patch.product }),
|
|
896
1090
|
...(patch.commitment !== undefined && { commitment: patch.commitment }),
|
|
@@ -1067,15 +1261,16 @@ function applicationFromRow(
|
|
|
1067
1261
|
address: row.address,
|
|
1068
1262
|
...(row.founderCompany !== null && { founderCompany: row.founderCompany }),
|
|
1069
1263
|
questions: row.questions,
|
|
1070
|
-
// #5285 — the
|
|
1071
|
-
// application schema, so a row that never carried one simply omits it
|
|
1072
|
-
// `submit` is unaffected. `requestedWindowDays` reads back the CLAMPED
|
|
1264
|
+
// #5285/#5096 — the four fields that became writable. Each is optional in
|
|
1265
|
+
// the application schema, so a row that never carried one simply omits it
|
|
1266
|
+
// and `submit` is unaffected. `requestedWindowDays` reads back the CLAMPED
|
|
1073
1267
|
// `windowDays` the row actually holds, not what was asked for: the
|
|
1074
1268
|
// request is not stored anywhere, and re-asserting it would be asserting
|
|
1075
1269
|
// a number nothing ever agreed to.
|
|
1076
1270
|
...(row.logoMarkUrl !== null && { logoMarkUrl: row.logoMarkUrl }),
|
|
1077
1271
|
...(row.logoWordmarkUrl !== null && { logoWordmarkUrl: row.logoWordmarkUrl }),
|
|
1078
1272
|
...(row.windowDays !== null && { requestedWindowDays: row.windowDays }),
|
|
1273
|
+
...(row.solution !== null && { solution: row.solution }),
|
|
1079
1274
|
};
|
|
1080
1275
|
return Object.fromEntries(
|
|
1081
1276
|
Object.entries(all).filter(([key]) => collected.has(key)),
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-read site resolution — which workspace an UNAUTHENTICATED content
|
|
3
|
+
* read serves (#5852, roadmap D4 read half).
|
|
4
|
+
*
|
|
5
|
+
* THE GUARD MODEL IS FAIL-CLOSED. The only branch that yields a workspace is an
|
|
6
|
+
* unambiguously one-workspace deployment, answered by the same predicate the
|
|
7
|
+
* authenticated path uses (`services/default-tenant.ts#resolveDefaultWorkspaceId`)
|
|
8
|
+
* — one predicate, reached two ways, never two predicates. Zero workspaces, two
|
|
9
|
+
* or more, or a sole workspace that is not the seeded default all resolve to
|
|
10
|
+
* `unresolved`, and the caller answers 404 before any content query runs.
|
|
11
|
+
*
|
|
12
|
+
* IT READS NO HOST. Every deployment this resolver serves today is
|
|
13
|
+
* one-workspace, so a host lookup would be dead code — and there is nothing to
|
|
14
|
+
* look up: `WorkspaceSiteIdentity` carries no host column (the value lives
|
|
15
|
+
* inside the unindexed `data.url`), and `request.hostname` resolves from
|
|
16
|
+
* `X-Forwarded-Host` under this server's `trustProxy: true` (`server.ts`),
|
|
17
|
+
* which is caller-suppliable and therefore not a tenancy input. The
|
|
18
|
+
* multi-tenant host branch is #3635's, and must resolve against a server-side
|
|
19
|
+
* binding rather than a forwarded header.
|
|
20
|
+
*
|
|
21
|
+
* THE CLIENT NEVER SUPPLIES A WORKSPACE. No path parameter, query string,
|
|
22
|
+
* header or body field is read here, on any read. That is roadmap D4's own
|
|
23
|
+
* STOP boundary, not a default.
|
|
24
|
+
*
|
|
25
|
+
* IT IS A NO-OP OFF THE DATABASE ADAPTER. Unless `CONTENT_ADAPTER=database`,
|
|
26
|
+
* the function returns `not-required` having issued no query and loaded no
|
|
27
|
+
* database module — which is what keeps the MDX default path byte-identical
|
|
28
|
+
* and keeps the no-`DATABASE_URL` scaffold starting.
|
|
29
|
+
*/
|
|
30
|
+
import {
|
|
31
|
+
resolvedAdapterKind,
|
|
32
|
+
type ArtifactPrismaClient,
|
|
33
|
+
type ContentAdapter,
|
|
34
|
+
} from "@working-theory/content-adapters";
|
|
35
|
+
import type { FastifyRequest } from "fastify";
|
|
36
|
+
|
|
37
|
+
import { getContentAdapter } from "../lib/content-adapter.js";
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The three answers a public read can get. `reason` exists for `request.log`
|
|
41
|
+
* only: the route decides its response from `kind` alone, so no resolution
|
|
42
|
+
* detail reaches an unauthenticated caller (#5852 D7, D8).
|
|
43
|
+
*/
|
|
44
|
+
export type PublicSiteResolution =
|
|
45
|
+
| { kind: "not-required" }
|
|
46
|
+
| { kind: "resolved"; workspaceId: string }
|
|
47
|
+
| { kind: "unresolved"; reason: "ambiguous" | "host-unmatched" };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolves the workspace a public content read serves, or refuses to.
|
|
51
|
+
*
|
|
52
|
+
* The `request` argument is the site identifier's eventual source (#3635) and
|
|
53
|
+
* is deliberately unused today — see the no-host note in this file's header.
|
|
54
|
+
*/
|
|
55
|
+
export async function resolvePublicWorkspace(
|
|
56
|
+
_request: FastifyRequest,
|
|
57
|
+
): Promise<PublicSiteResolution> {
|
|
58
|
+
if (resolvedAdapterKind() !== "database") {
|
|
59
|
+
return { kind: "not-required" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Deferred — see the module-load note in this file's header (#5852 D6).
|
|
63
|
+
const { resolveDefaultWorkspaceId } = await import("./default-tenant.js");
|
|
64
|
+
const workspaceId = await resolveDefaultWorkspaceId();
|
|
65
|
+
if (workspaceId === null) {
|
|
66
|
+
return { kind: "unresolved", reason: "ambiguous" };
|
|
67
|
+
}
|
|
68
|
+
return { kind: "resolved", workspaceId };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The adapter a public content read should use, or `null` when it must refuse.
|
|
73
|
+
*
|
|
74
|
+
* This is the ONE place the three-armed resolution collapses into a decision,
|
|
75
|
+
* so a second public read route cannot grow a second copy of it: `resolved`
|
|
76
|
+
* and `not-required` both yield an adapter, `unresolved` yields `null`. The
|
|
77
|
+
* caller answers `null` with its OWN existing not-found body — which is what
|
|
78
|
+
* keeps a refusal byte-identical to a missing page on `get-page`, and keeps
|
|
79
|
+
* each route's 404 shape its own (#5852 D8). The reason is logged here and
|
|
80
|
+
* never leaves the server.
|
|
81
|
+
*/
|
|
82
|
+
export async function resolvePublicContentAdapter(
|
|
83
|
+
request: FastifyRequest,
|
|
84
|
+
): Promise<ContentAdapter | null> {
|
|
85
|
+
const site = await resolvePublicWorkspace(request);
|
|
86
|
+
if (site.kind === "unresolved") {
|
|
87
|
+
request.log.warn(
|
|
88
|
+
{ reason: site.reason },
|
|
89
|
+
"[Content] no site resolved for public read",
|
|
90
|
+
);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
if (site.kind === "not-required") {
|
|
94
|
+
return getContentAdapter();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The database path, and the only caller that needs apps/api's client. The
|
|
98
|
+
// import is deferred for the reason the header gives (#5852 D6), and the
|
|
99
|
+
// generated client is structurally wider than the narrow read surface the
|
|
100
|
+
// adapter declares — the cast names that, and nothing else.
|
|
101
|
+
const { prisma } = await import("../lib/prisma.js");
|
|
102
|
+
return getContentAdapter({
|
|
103
|
+
workspaceId: site.workspaceId,
|
|
104
|
+
prisma: prisma as unknown as ArtifactPrismaClient,
|
|
105
|
+
});
|
|
106
|
+
}
|