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
|
@@ -56,6 +56,15 @@ export interface ConsentWriteClient {
|
|
|
56
56
|
|
|
57
57
|
interface ConsentGrantRow {
|
|
58
58
|
siteId: string;
|
|
59
|
+
/**
|
|
60
|
+
* #6374 — the tenancy fence's isolation key, equal to `siteId` by
|
|
61
|
+
* construction. Non-optional on purpose: `consent_grants.workspace_id` is
|
|
62
|
+
* NOT NULL as of
|
|
63
|
+
* database/migrations/20260922120000_tenancy_fence, so a row builder that
|
|
64
|
+
* forgot it would fail at runtime; declaring it required here makes the
|
|
65
|
+
* type checker say so instead.
|
|
66
|
+
*/
|
|
67
|
+
workspaceId: string;
|
|
59
68
|
subjectType: string;
|
|
60
69
|
subjectId: string;
|
|
61
70
|
purposeId: string;
|
|
@@ -169,6 +178,12 @@ export async function recordConsent(
|
|
|
169
178
|
const { statement, locale } = resolveConsentStatement(purpose, input.locale);
|
|
170
179
|
rows.push({
|
|
171
180
|
siteId: input.siteId,
|
|
181
|
+
// #6374 — the isolation key, equal to `siteId` by construction.
|
|
182
|
+
// `input.siteId` is the caller's already-resolved site id; the
|
|
183
|
+
// `consent_grants_workspace_site_agree` CHECK makes a disagreement
|
|
184
|
+
// unstorable, and `consent_grants_workspace_id_format` rejects a raw
|
|
185
|
+
// alias loudly. Never read from a request body (AC-6).
|
|
186
|
+
workspaceId: input.siteId,
|
|
172
187
|
subjectType: input.subjectType,
|
|
173
188
|
subjectId: input.subjectId,
|
|
174
189
|
purposeId,
|
|
@@ -222,6 +237,9 @@ export async function recordLegacyConsent(
|
|
|
222
237
|
data: [
|
|
223
238
|
{
|
|
224
239
|
siteId: input.siteId,
|
|
240
|
+
// #6374 — the isolation key, equal to `siteId` by construction; the
|
|
241
|
+
// same stamp the D10 path's row builder above applies.
|
|
242
|
+
workspaceId: input.siteId,
|
|
225
243
|
subjectType: input.subjectType,
|
|
226
244
|
subjectId: input.subjectId,
|
|
227
245
|
purposeId: purpose.id,
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default-tenant resolution — the BASE-build half of the tenant guard (#5677).
|
|
3
|
+
*
|
|
4
|
+
* A single-tenant site is a ONE-WORKSPACE TENANT, not a special case. The
|
|
5
|
+
* default Organization / Workspace / operator Membership are already declared
|
|
6
|
+
* as fixed reserved-range sentinels in `database/tenancy.ts` and already
|
|
7
|
+
* seeded by `database/seed.ts`, so there is nothing to synthesize here — this
|
|
8
|
+
* file resolves the seeded default and nothing else.
|
|
9
|
+
*
|
|
10
|
+
* WHY IT LIVES IN THE BASE BUILD. `services/tenant-context.ts` and
|
|
11
|
+
* `middleware/tenant.ts` are multi-tenant SLICE files (`// oss: false`), so on
|
|
12
|
+
* a build without that module they are not present and the routes behind them
|
|
13
|
+
* were never REGISTERED — not denied (#5677). The default
|
|
14
|
+
* resolution and the `TenantContext` shape it yields are needed on every
|
|
15
|
+
* build, so they live here, in a file that always ships; the multi-tenant
|
|
16
|
+
* slice imports this one rather than carrying its own copy, which is what
|
|
17
|
+
* keeps the two paths from drifting.
|
|
18
|
+
*
|
|
19
|
+
* PRECEDENCE, NOT PARALLELISM. When the multi-tenant module is present,
|
|
20
|
+
* `resolveActiveTenant` (the slice) is the entry point and calls
|
|
21
|
+
* `resolveDefaultTenant` only on the branch it always called it on — the
|
|
22
|
+
* previously-unconditional-deny null-binding branch. Nothing about the
|
|
23
|
+
* multi-tenant path changes. Without the module, `middleware/default-tenant.ts`
|
|
24
|
+
* calls this directly. There is one resolution, reached two ways, never two
|
|
25
|
+
* resolutions.
|
|
26
|
+
*
|
|
27
|
+
* DEFAULT-DENY IS THE FLOOR. `resolveDefaultTenant` returns `null` — never a
|
|
28
|
+
* grant — the instant the deployment is not unambiguously single-tenant
|
|
29
|
+
* (a second Workspace exists, or the sole Workspace is not the well-known
|
|
30
|
+
* default), and `null` again for a caller holding no Membership on the default
|
|
31
|
+
* Organization. Both branches were written for the multi-tenant slice by the
|
|
32
|
+
* owner-pin fix (docs/specs/single-tenant-owner-pin-spec.md, #3631) and are
|
|
33
|
+
* moved here BYTE-FOR-BYTE, not rewritten.
|
|
34
|
+
*
|
|
35
|
+
* This file is a sanctioned exception to architecture.yaml's tenant-scope
|
|
36
|
+
* forbidden pattern, for the same reason `tenant-context.ts` is: it is a
|
|
37
|
+
* tenancy-model boundary, not a domain-data reader.
|
|
38
|
+
*/
|
|
39
|
+
import {
|
|
40
|
+
DEFAULT_ORG_ID,
|
|
41
|
+
OPERATOR_MEMBERSHIP_ID,
|
|
42
|
+
resolveDefaultWorkspaceRow as resolveDefaultWorkspaceRowShared,
|
|
43
|
+
} from "@working-theory/database/tenancy";
|
|
44
|
+
import type { Id } from "@working-theory/ids";
|
|
45
|
+
|
|
46
|
+
import { prisma } from "../lib/prisma.js";
|
|
47
|
+
|
|
48
|
+
import { findVerifiedEmail } from "./verified-email.js";
|
|
49
|
+
|
|
50
|
+
export type MembershipRole = "owner" | "admin" | "member";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The tenant scope every domain query is bound to. Ids carry the ADR-0031
|
|
54
|
+
* brand; values originating from Prisma reads are brand-asserted at the read
|
|
55
|
+
* edge (the DB CHECK constraints are the runtime guarantee).
|
|
56
|
+
*/
|
|
57
|
+
export interface TenantContext {
|
|
58
|
+
userId: Id<"user">;
|
|
59
|
+
organizationId: Id<"organization">;
|
|
60
|
+
workspaceId: Id<"workspace">;
|
|
61
|
+
role: MembershipRole;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type TenantErrorKind = "forbidden" | "not_found" | "conflict";
|
|
65
|
+
|
|
66
|
+
export type TenantResult<T> =
|
|
67
|
+
| { ok: true; value: T }
|
|
68
|
+
| { ok: false; error: { kind: TenantErrorKind; message: string } };
|
|
69
|
+
|
|
70
|
+
export function isUniqueViolation(error: unknown): boolean {
|
|
71
|
+
return (
|
|
72
|
+
typeof error === "object" &&
|
|
73
|
+
error !== null &&
|
|
74
|
+
"code" in error &&
|
|
75
|
+
(error as { code?: unknown }).code === "P2002"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* TOFU-binds the operator Membership for a single-tenant deployment (spec
|
|
81
|
+
* D1–D4) — the replacement for the deleted user-agnostic `owner` grant. The
|
|
82
|
+
* Membership is created with a fixed, well-known id
|
|
83
|
+
* (`OPERATOR_MEMBERSHIP_ID`); the `Membership.id` primary-key uniqueness —
|
|
84
|
+
* not a read-then-write "is this the first user?" guard — enforces exactly
|
|
85
|
+
* one owner under a concurrent first-login race. The losing caller's
|
|
86
|
+
* `create` raises `P2002`, swallowed here: that caller is then unbound and
|
|
87
|
+
* denied by the `membership.findUnique` the caller of this function runs
|
|
88
|
+
* next.
|
|
89
|
+
*
|
|
90
|
+
* Idempotent + at-most-once: a caller who already holds the Membership (a
|
|
91
|
+
* repeat call, or the deployment's steady state after the first bind) hits
|
|
92
|
+
* the same `P2002` path and is a no-op. Reachable ONLY from
|
|
93
|
+
* `resolveDefaultTenant`, itself gated by the
|
|
94
|
+
* `count === 1 && default-workspace-exists` check — unreachable from any
|
|
95
|
+
* multi-tenant state (`count > 1`).
|
|
96
|
+
*
|
|
97
|
+
* When `AILK_OPERATOR_EMAIL` is set (spec D4 — trimmed + lowercased before
|
|
98
|
+
* comparison), the bind is attempted ONLY for the caller whose VERIFIED
|
|
99
|
+
* `User.email` matches it (#5701 AC-2 — the read goes through
|
|
100
|
+
* `findVerifiedEmail`, so an unverified address is refused here and not only
|
|
101
|
+
* at the base-build entry above); every other caller returns without writing
|
|
102
|
+
* and is denied downstream. An empty/whitespace-only `AILK_OPERATOR_EMAIL` normalizes to
|
|
103
|
+
* `""`, which is falsy — treated as unset (pure TOFU), so a blank pin can
|
|
104
|
+
* never "match" a blank `User.email`. When unset, the bind is pure TOFU —
|
|
105
|
+
* first caller wins.
|
|
106
|
+
*/
|
|
107
|
+
async function bindOperatorMembership(userId: Id<"user">): Promise<void> {
|
|
108
|
+
const pinnedEmail = process.env.AILK_OPERATOR_EMAIL?.trim().toLowerCase();
|
|
109
|
+
if (pinnedEmail) {
|
|
110
|
+
// The verified-email read is `services/verified-email.ts` (#5701 D4) — the
|
|
111
|
+
// same one `resolveDefaultTenantForOperator` uses. It returns the address
|
|
112
|
+
// only when `emailVerified === true`, so an address that merely CLAIMS to
|
|
113
|
+
// be the pin can no longer mint `OPERATOR_MEMBERSHIP_ID`. Both refusals
|
|
114
|
+
// (`user_not_found`, `email_unverified`) take the branch's existing early
|
|
115
|
+
// return — the caller is then unbound and denied by the
|
|
116
|
+
// `membership.findUnique` that follows.
|
|
117
|
+
const verified = await findVerifiedEmail(userId);
|
|
118
|
+
if (!verified.ok) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (verified.value.email.trim().toLowerCase() !== pinnedEmail) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await prisma.membership.create({
|
|
127
|
+
data: {
|
|
128
|
+
id: OPERATOR_MEMBERSHIP_ID,
|
|
129
|
+
organizationId: DEFAULT_ORG_ID,
|
|
130
|
+
userId,
|
|
131
|
+
role: "owner",
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (!isUniqueViolation(error)) {
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
// Another caller already holds OPERATOR_MEMBERSHIP_ID (the TOFU race
|
|
139
|
+
// winner, or a prior deployment run) — swallow and fall through.
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The BASE-BUILD entry point: `resolveDefaultTenant` narrowed to the caller
|
|
145
|
+
* whose `User.email` matches `AILK_OPERATOR_EMAIL`, denying everyone when the
|
|
146
|
+
* pin is unset (#5693 security review, High finding).
|
|
147
|
+
*
|
|
148
|
+
* WHY THE BASE PATH NEEDS THIS AND THE MULTI-TENANT PATH DOES NOT. Registering
|
|
149
|
+
* the review surface on a module-less build made two things reachable together
|
|
150
|
+
* that were previously unreachable there: this resolution's TOFU operator bind,
|
|
151
|
+
* and `services/waitlist-dashboard.ts`'s `isPlatformOperator`, which grants
|
|
152
|
+
* `{kind: "all"}` — every site's rows, unfiltered — to an `owner` of the
|
|
153
|
+
* sentinel default Organization whenever `AILK_OPERATOR_EMAIL` is merely SET.
|
|
154
|
+
* That predicate never compares the caller to the pin; the pin is an on/off
|
|
155
|
+
* switch for it.
|
|
156
|
+
*
|
|
157
|
+
* Unpinned TOFU plus that predicate is an escalation. The site's auth
|
|
158
|
+
* auto-provisions a `User` on first login, so on a deployment with the pin
|
|
159
|
+
* unset the FIRST authenticated caller to touch a review route — any public
|
|
160
|
+
* respondent, not necessarily the operator — wins `OPERATOR_MEMBERSHIP_ID`.
|
|
161
|
+
* The real operator is then denied (their `create` raises `P2002`, swallowed),
|
|
162
|
+
* finds an empty dashboard, and sets `AILK_OPERATOR_EMAIL` to fix it — which
|
|
163
|
+
* flips the squatter, not the operator, to `{kind: "all"}`.
|
|
164
|
+
*
|
|
165
|
+
* So the base path requires the pin and requires the caller to BE it. Both
|
|
166
|
+
* halves matter: requiring the pin stops an unpinned race from seating anyone,
|
|
167
|
+
* and re-checking the caller's email on every request denies a squatter who
|
|
168
|
+
* was already bound before this code shipped — a Membership row alone is no
|
|
169
|
+
* longer enough. `bindOperatorMembership` below performs its own email check,
|
|
170
|
+
* but only at bind time and only for the bind; this is the one that gates the
|
|
171
|
+
* grant.
|
|
172
|
+
*
|
|
173
|
+
* The multi-tenant module is unaffected: `resolveActiveTenant` keeps calling
|
|
174
|
+
* `resolveDefaultTenant` directly, on exactly the branch it always called it
|
|
175
|
+
* on. This wrapper is reached only from `middleware/default-tenant.ts`, which
|
|
176
|
+
* only ever runs on a build without that module.
|
|
177
|
+
*
|
|
178
|
+
* `docs/specs/single-tenant-owner-pin-spec.md` D5 already recommended pinning;
|
|
179
|
+
* on this path it is mandatory rather than advisory, and unset fails closed.
|
|
180
|
+
*
|
|
181
|
+
* ONE KNOWN IMPRECISION, deliberate. `User.email` is `@unique` in Postgres,
|
|
182
|
+
* which is byte-exact, while this comparison lowercases both sides — so two
|
|
183
|
+
* distinct rows (`Operator@x.com` and `operator@x.com`) could both satisfy one
|
|
184
|
+
* pin. It is not reachable today: `apps/web/auth.ts` configures social
|
|
185
|
+
* providers only, so the address is provider-asserted and normalized rather
|
|
186
|
+
* than caller-chosen. It becomes reachable the moment a credential provider is
|
|
187
|
+
* enabled, and the durable fix is case-insensitive email uniqueness in the
|
|
188
|
+
* schema (citext, or lowercase-at-write) so "matches the pin" and "is unique"
|
|
189
|
+
* agree. Matching case-sensitively here instead would be worse: it would lock
|
|
190
|
+
* out an operator who typed their own address with different capitalisation.
|
|
191
|
+
*/
|
|
192
|
+
export async function resolveDefaultTenantForOperator(
|
|
193
|
+
userId: Id<"user">,
|
|
194
|
+
): Promise<TenantContext | null> {
|
|
195
|
+
const pinnedEmail = process.env.AILK_OPERATOR_EMAIL?.trim().toLowerCase();
|
|
196
|
+
if (!pinnedEmail) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
// The verified-email read is `services/verified-email.ts` (#5701 D4) — the
|
|
200
|
+
// one this and the four other email-keyed paths share. It returns the
|
|
201
|
+
// address only when `emailVerified === true`, which is what makes a match
|
|
202
|
+
// against the pin a proven claim rather than an asserted one.
|
|
203
|
+
const verified = await findVerifiedEmail(userId);
|
|
204
|
+
if (!verified.ok) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (verified.value.email.trim().toLowerCase() !== pinnedEmail) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return resolveDefaultTenant(userId);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The DEPLOYMENT half of the default resolution, extracted so it exists once
|
|
215
|
+
* (#5852 D2). Returns the sole, well-known default Workspace row, or `null`
|
|
216
|
+
* the instant the deployment is not unambiguously one-workspace — a second
|
|
217
|
+
* Workspace exists, none does, or the sole one is not the seeded default.
|
|
218
|
+
*
|
|
219
|
+
* Carries no authentication: it answers "is this deployment one-workspace, and
|
|
220
|
+
* which workspace is it", nothing about the caller. `resolveDefaultTenant`
|
|
221
|
+
* adds the authentication half on top; `services/public-site-resolver.ts`
|
|
222
|
+
* (unauthenticated public reads) uses the deployment half alone. One
|
|
223
|
+
* predicate, reached two ways — never two predicates.
|
|
224
|
+
*
|
|
225
|
+
* The predicate itself is `@working-theory/database/tenancy`'s
|
|
226
|
+
* `resolveDefaultWorkspaceRow` (#5876 spec D2/D3) — this is now a thin
|
|
227
|
+
* pass-through of `apps/api`'s own `prisma` singleton, kept as a named local
|
|
228
|
+
* function (rather than re-exporting the shared one directly) so every
|
|
229
|
+
* existing caller — this file's own `resolveDefaultWorkspaceId` and
|
|
230
|
+
* `resolveDefaultTenant`, and `services/public-site-resolver.ts`'s dynamic
|
|
231
|
+
* import of the wrapper — is untouched. There is exactly one implementation
|
|
232
|
+
* of the one-workspace predicate in the repository (NFR-7); apps/mcp's own
|
|
233
|
+
* resolution (`apps/mcp/src/lib/public-workspace.ts`) delegates to the same
|
|
234
|
+
* shared export.
|
|
235
|
+
*/
|
|
236
|
+
async function resolveDefaultWorkspaceRow(): Promise<{
|
|
237
|
+
id: string;
|
|
238
|
+
organizationId: string;
|
|
239
|
+
} | null> {
|
|
240
|
+
return resolveDefaultWorkspaceRowShared(prisma);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The one-workspace deployment predicate, as a workspace id (#5852 D2).
|
|
245
|
+
* `null` means the deployment is not unambiguously one-workspace, and every
|
|
246
|
+
* caller fails closed on it.
|
|
247
|
+
*/
|
|
248
|
+
export async function resolveDefaultWorkspaceId(): Promise<Id<"workspace"> | null> {
|
|
249
|
+
const workspace = await resolveDefaultWorkspaceRow();
|
|
250
|
+
return workspace ? (workspace.id as Id<"workspace">) : null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Resolves the default single-tenant scope when the deployment is
|
|
255
|
+
* unambiguously single-tenant — the default Workspace (`DEFAULT_WORKSPACE_ID`)
|
|
256
|
+
* exists AND it is the ONLY Workspace (`workspace.count() === 1`), the exact
|
|
257
|
+
* mirror of the Phase-1 provision guard's `count === 0` check (spec D2).
|
|
258
|
+
* Returns `null` (never grants) the instant a second Workspace exists, or the
|
|
259
|
+
* sole Workspace is not the well-known default — the caller falls through to
|
|
260
|
+
* its own default-deny in that case.
|
|
261
|
+
*
|
|
262
|
+
* Converges on the SAME Membership-based authz the multi-tenant path uses
|
|
263
|
+
* (spec D1 — the fix): attempts to TOFU-bind the caller as operator
|
|
264
|
+
* (`bindOperatorMembership`, a no-op once an operator is already bound or
|
|
265
|
+
* the caller doesn't match the optional env pin), then runs the real
|
|
266
|
+
* `membership.findUnique` check. A caller holding no Membership on the
|
|
267
|
+
* default Org — including every caller after the first once TOFU has bound
|
|
268
|
+
* someone else — resolves `null` here, and the caller denies. There is no
|
|
269
|
+
* user-agnostic `owner` grant.
|
|
270
|
+
*
|
|
271
|
+
* Formerly `resolveSingleTenantDefault` in `services/tenant-context.ts`
|
|
272
|
+
* (#3631); moved here verbatim by #5677 so the base build can reach it.
|
|
273
|
+
*/
|
|
274
|
+
export async function resolveDefaultTenant(
|
|
275
|
+
userId: Id<"user">,
|
|
276
|
+
): Promise<TenantContext | null> {
|
|
277
|
+
const workspace = await resolveDefaultWorkspaceRow();
|
|
278
|
+
if (!workspace) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
await bindOperatorMembership(userId);
|
|
282
|
+
const membership = await prisma.membership.findUnique({
|
|
283
|
+
where: {
|
|
284
|
+
organizationId_userId: {
|
|
285
|
+
organizationId: workspace.organizationId,
|
|
286
|
+
userId,
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
select: { role: true },
|
|
290
|
+
});
|
|
291
|
+
if (!membership) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
userId,
|
|
296
|
+
organizationId: workspace.organizationId as Id<"organization">,
|
|
297
|
+
workspaceId: workspace.id as Id<"workspace">,
|
|
298
|
+
role: membership.role as MembershipRole,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
@@ -45,13 +45,30 @@
|
|
|
45
45
|
* PII boundary: logs carry only `{ siteId, flowSlug, sessionId, outcome }` —
|
|
46
46
|
* never email/asset-key/copy values (mirrors flow-engine.ts's + abandon-
|
|
47
47
|
* email.ts's boundary).
|
|
48
|
+
*
|
|
49
|
+
* #6310 D1/D2/D4/D5/D15 — a non-`playbook:` `assetKey` now resolves against
|
|
50
|
+
* the workspace-scoped `Asset` store FIRST, via the named boundary
|
|
51
|
+
* `lib/deliverable-asset-store.ts` (never a raw `prisma.asset` call here —
|
|
52
|
+
* that access is banned by architecture.yaml's tenant-scope
|
|
53
|
+
* `forbidden_pattern`), falling back to the LEGACY `DeliverableAsset` store
|
|
54
|
+
* on a miss (D3 — existing rows keep delivering, unmigrated). The workspace
|
|
55
|
+
* is read from the magnet's own `FlowDefinition.workspaceId`, ONE
|
|
56
|
+
* `findUnique` issued only on this branch (never for a `playbook:` key, and
|
|
57
|
+
* never before the `no_entry` exit) — a `null`/absent value skips the
|
|
58
|
+
* `Asset` lookup entirely and falls straight to the legacy arm, never
|
|
59
|
+
* guessing a workspace (D5, mirroring #6155 D3's posture). The grant is
|
|
60
|
+
* minted with `workspaceId` ONLY on the `Asset` arm — the same value that
|
|
61
|
+
* authorized the resolution — restoring the download route's serve-time
|
|
62
|
+
* tenancy re-check for this new grant class (D15).
|
|
48
63
|
*/
|
|
49
64
|
|
|
50
65
|
import { sendDeliverableEmail } from "@working-theory/email";
|
|
51
66
|
import { logger } from "@working-theory/observability";
|
|
52
67
|
import { flowConfigSchema, type FlowConfig } from "@working-theory/validation";
|
|
53
68
|
|
|
69
|
+
import { findDeliverableAssetIdByKey } from "../lib/deliverable-asset-store.js";
|
|
54
70
|
import { prisma } from "../lib/prisma.js";
|
|
71
|
+
import { resolveWorkspaceIdForAlias } from "../lib/site.js";
|
|
55
72
|
|
|
56
73
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
57
74
|
|
|
@@ -194,6 +211,15 @@ async function stampOutcome(
|
|
|
194
211
|
now: Date,
|
|
195
212
|
): Promise<void> {
|
|
196
213
|
try {
|
|
214
|
+
// fence: global-key — `DeliverableGrant.id`, the capability token itself
|
|
215
|
+
// (#3961 D5: the grant IS the token). Every caller reaches this line
|
|
216
|
+
// inside an already-scoped frame — `fulfillDeliverable` has just MINTED
|
|
217
|
+
// this id under the session's workspace, and `resendDeliverableGrant` has
|
|
218
|
+
// just read the row back by the same opaque id — so the id is the row's
|
|
219
|
+
// own primary key, not a tenant selector, and a `workspaceId` term here
|
|
220
|
+
// could only re-state what the mint already fenced. It would also stop
|
|
221
|
+
// stamping LEGACY grants, whose `workspaceId` is NULL by the #6254 § 10.1
|
|
222
|
+
// Q2 carve-out, turning a post-mint audit stamp into a silent no-op.
|
|
197
223
|
await prisma.deliverableGrant.update({
|
|
198
224
|
where: { id: grantId },
|
|
199
225
|
data: { sendStatus: outcome, sendAt: now },
|
|
@@ -312,9 +338,17 @@ export async function fulfillDeliverable(
|
|
|
312
338
|
const playbookKey = playbookExperimentKey(entry.assetKey);
|
|
313
339
|
|
|
314
340
|
let assetId: string;
|
|
341
|
+
// #6310 D15 — written ONLY on the Asset arm; stays undefined (no column
|
|
342
|
+
// written) for a playbook: or legacy dasset_ grant.
|
|
343
|
+
let grantWorkspaceId: string | undefined;
|
|
344
|
+
|
|
315
345
|
if (playbookKey !== undefined) {
|
|
346
|
+
// #6257 — `siteId` is the session's own, resolved by the route that
|
|
347
|
+
// opened it (#6254/#6374 AC-7), so `workspaceId` rides beside it: a lead
|
|
348
|
+
// id and an experiment key alone would select a row whose tenant the
|
|
349
|
+
// query never stated.
|
|
316
350
|
const instance = await prisma.playbookInstance.findFirst({
|
|
317
|
-
where: { siteId, leadId, experimentKey: playbookKey },
|
|
351
|
+
where: { siteId, workspaceId: siteId, leadId, experimentKey: playbookKey },
|
|
318
352
|
select: { id: true },
|
|
319
353
|
});
|
|
320
354
|
if (!instance) {
|
|
@@ -323,24 +357,75 @@ export async function fulfillDeliverable(
|
|
|
323
357
|
}
|
|
324
358
|
assetId = entry.assetKey;
|
|
325
359
|
} else {
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
360
|
+
// #6310 D4 — resolve the workspace from the magnet's own live
|
|
361
|
+
// FlowDefinition, ONE query, issued only on this branch. D5 — a null or
|
|
362
|
+
// absent workspaceId skips the Asset lookup entirely; it is never
|
|
363
|
+
// guessed.
|
|
364
|
+
const definition = (await prisma.flowDefinition.findUnique({
|
|
365
|
+
where: { siteId_slug: { siteId, slug: flowSlug } },
|
|
366
|
+
select: { workspaceId: true },
|
|
367
|
+
})) as { workspaceId: string | null } | null;
|
|
368
|
+
const workspaceId = definition?.workspaceId ?? null;
|
|
369
|
+
|
|
370
|
+
const resolvedAssetId =
|
|
371
|
+
workspaceId !== null
|
|
372
|
+
? await findDeliverableAssetIdByKey(workspaceId, entry.assetKey)
|
|
373
|
+
: null;
|
|
374
|
+
|
|
375
|
+
if (resolvedAssetId !== null && workspaceId !== null) {
|
|
376
|
+
// #6310 D2 — Asset resolves first; D15 — the grant carries the SAME
|
|
377
|
+
// workspaceId that authorized this resolution.
|
|
378
|
+
assetId = resolvedAssetId;
|
|
379
|
+
grantWorkspaceId = workspaceId;
|
|
380
|
+
} else {
|
|
381
|
+
// #6310 D2/D3 — legacy fallback. A missing row (config references an
|
|
382
|
+
// un-uploaded key) → same skip-and-log posture as before.
|
|
383
|
+
// #6257 — the LEGACY store's read, scoped by the same resolved
|
|
384
|
+
// workspace id the `Asset` arm above already resolves through
|
|
385
|
+
// `lib/deliverable-asset-store.ts`. The compound `[siteId, key]` names
|
|
386
|
+
// one row; `workspaceId` is what makes the tenant the QUERY's statement.
|
|
387
|
+
const asset = (await prisma.deliverableAsset.findUnique({
|
|
388
|
+
where: { siteId_key: { siteId, key: entry.assetKey }, workspaceId: siteId },
|
|
389
|
+
})) as DeliverableAssetRow | null;
|
|
390
|
+
|
|
391
|
+
if (!asset) {
|
|
392
|
+
logOutcome(logCtx, "asset_missing");
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
assetId = asset.id;
|
|
335
396
|
}
|
|
336
|
-
assetId = asset.id;
|
|
337
397
|
}
|
|
338
398
|
|
|
339
399
|
// D5: the grant IS the token — minted before the send is attempted so a
|
|
340
400
|
// later email-config gap or send failure never wastes it (a later
|
|
341
401
|
// manual/operator resend can reuse the same grant id).
|
|
342
402
|
const grant = (await prisma.deliverableGrant.create({
|
|
343
|
-
data: {
|
|
403
|
+
data: {
|
|
404
|
+
siteId,
|
|
405
|
+
assetId,
|
|
406
|
+
leadId,
|
|
407
|
+
sessionId,
|
|
408
|
+
// #6374 D4 / AC-8 — the SECOND DeliverableGrant mint site. The column
|
|
409
|
+
// stays NULLABLE for LEGACY rows (NULL is the serve path's fail-closed
|
|
410
|
+
// sentinel, #6254 § 10.1 Q2) and the migration touches none of them —
|
|
411
|
+
// but a grant minted HERE always knows its workspace, so it is never
|
|
412
|
+
// left to fail closed.
|
|
413
|
+
//
|
|
414
|
+
// `grantWorkspaceId` is #6310 D15's value, resolved from the magnet's
|
|
415
|
+
// own FlowDefinition on the `asset_` arm; that read is scoped by
|
|
416
|
+
// `siteId`, and the keystone's `flow_definitions_workspace_site_agree`
|
|
417
|
+
// CHECK makes `workspaceId = siteId` on that row, so the two are the
|
|
418
|
+
// SAME value and the `??` is a fallback for the legacy `dasset_` and
|
|
419
|
+
// virtual `playbook:` arms rather than a choice between two scopes.
|
|
420
|
+
// The new `deliverable_grants_workspace_site_agree` CHECK would reject
|
|
421
|
+
// a disagreement loudly either way.
|
|
422
|
+
//
|
|
423
|
+
// This does NOT change which bytes the download route serves: that
|
|
424
|
+
// route branches on the `assetId` PREFIX, and consults
|
|
425
|
+
// `grant.workspaceId` only on the `asset_` arm, where this value is
|
|
426
|
+
// what it already was.
|
|
427
|
+
workspaceId: grantWorkspaceId ?? siteId,
|
|
428
|
+
},
|
|
344
429
|
select: { id: true },
|
|
345
430
|
})) as { id: string };
|
|
346
431
|
|
|
@@ -400,6 +485,13 @@ type FlowSessionConfigRow = {
|
|
|
400
485
|
export async function resendDeliverableGrant(
|
|
401
486
|
grantId: string,
|
|
402
487
|
): Promise<ResendGrantResult> {
|
|
488
|
+
// fence: global-key — `DeliverableGrant.id`, the capability token itself
|
|
489
|
+
// (#3961 D5). This is the resolver: the operator hands the
|
|
490
|
+
// `deliverable:resend` CLI one opaque grant id and nothing else, and the
|
|
491
|
+
// WORKSPACE is what this read produces (`grant.siteId`, carried into
|
|
492
|
+
// `sendAndStamp` below), so a tenant term here would have to come from the
|
|
493
|
+
// answer it is computing. Holding the id is the capability, exactly as it
|
|
494
|
+
// is for the download route this resend re-sends a link to.
|
|
403
495
|
const grant = (await prisma.deliverableGrant.findUnique({
|
|
404
496
|
where: { id: grantId },
|
|
405
497
|
select: { id: true, siteId: true, leadId: true, sessionId: true },
|
|
@@ -514,9 +606,45 @@ type DeliverableGrantListRow = {
|
|
|
514
606
|
export async function listSendlessGrants(
|
|
515
607
|
opts: ListSendlessGrantsOptions,
|
|
516
608
|
): Promise<SendlessGrantRow[]> {
|
|
609
|
+
// #6257 — `opts.siteId` is the one value in this service that arrives as a
|
|
610
|
+
// RAW ALIAS: the `deliverable:resend` CLI passes `--site` through verbatim.
|
|
611
|
+
// It is resolved here, once, the way `dunning-policy.ts` resolves its own
|
|
612
|
+
// parameter — an alias that names no workspace lists nothing rather than
|
|
613
|
+
// falling back to the unscoped read it used to make.
|
|
614
|
+
const workspaceId = await resolveWorkspaceIdForAlias(opts.siteId);
|
|
615
|
+
if (workspaceId === null) {
|
|
616
|
+
logger.warn(
|
|
617
|
+
{ siteId: opts.siteId, outcome: "site_unresolved" },
|
|
618
|
+
"[deliverable-fulfillment] --site names no workspace — listing nothing",
|
|
619
|
+
);
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// #6257 — LEGACY grants are NOT in this list, and the narrowing is stated
|
|
624
|
+
// rather than left for an operator to discover. Two terms exclude them: the
|
|
625
|
+
// `workspaceId` filter below (their column is NULL by the #6254 § 10.1 Q2
|
|
626
|
+
// carve-out) and the resolved `siteId` (the keystone migration deliberately
|
|
627
|
+
// does NOT normalize `deliverable_grants.site_id` — migration.sql:1059 — so
|
|
628
|
+
// a legacy row on a `"default"`-aliased deployment still carries the alias).
|
|
629
|
+
//
|
|
630
|
+
// This is a genuine TIGHTENING beyond the serve path, not parity with it:
|
|
631
|
+
// `routes/deliverables/index.ts` consults `grant.workspaceId` only on the
|
|
632
|
+
// `asset_` arm, so a legacy `dasset_` or `playbook:` grant still downloads.
|
|
633
|
+
// The fence forces it — AC-2 forbids keying this read on the raw alias the
|
|
634
|
+
// CLI was given, and a legacy row carries no workspace key to resolve — so
|
|
635
|
+
// the list is narrower by construction and the recovery path for such a row
|
|
636
|
+
// is `deliverable:resend --grant <id>`, which is unaffected.
|
|
637
|
+
logger.info(
|
|
638
|
+
{ siteId: workspaceId, outcome: "legacy_grants_excluded" },
|
|
639
|
+
"[deliverable-fulfillment] grants with no workspace key are NOT listed — resend one by id with --grant",
|
|
640
|
+
);
|
|
641
|
+
|
|
517
642
|
const rows = (await prisma.deliverableGrant.findMany({
|
|
518
643
|
where: {
|
|
519
|
-
siteId:
|
|
644
|
+
siteId: workspaceId,
|
|
645
|
+
// #6257 — the fence term. See the note above for what it excludes and
|
|
646
|
+
// why that is a tightening rather than serve-path parity.
|
|
647
|
+
workspaceId,
|
|
520
648
|
sendStatus: null,
|
|
521
649
|
...(opts.since !== undefined && { createdAt: { gte: opts.since } }),
|
|
522
650
|
},
|