create-ailk 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/cli.js +54 -5
- package/dist/copy.d.ts +14 -0
- package/dist/copy.js +38 -1
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/extract-module-bundle.d.ts +20 -1
- package/dist/lib/extract-module-bundle.js +64 -5
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/lib/module-license-gate.d.ts +6 -1
- package/dist/lib/module-license-gate.js +6 -1
- package/dist/lib/pro-callout.js +1 -1
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/programmatic.js +14 -10
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/DESIGN.md +148 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/DESIGN.md +148 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/DESIGN.md +148 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/saas-product/DESIGN.md +148 -0
- package/site-templates/simple-service/DESIGN.md +148 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/CLAUDE.md +2 -3
- package/templates/DESIGN.md +155 -0
- package/templates/README.md +42 -8
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/PRODUCT.md +86 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +135 -20
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -8
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/middleware.ts +1 -1
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +93 -0
- package/templates/content/_site.mdx +18 -3
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +3 -2
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- package/templates/apps/web/public/logo-dark.svg +0 -21
- package/templates/apps/web/public/logo-light.svg +0 -21
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commerce license-claim guard for the commerce PRIMITIVES that live in the
|
|
3
|
+
* base tree (DECISION-LOG D-64, epic #5770, #5796).
|
|
4
|
+
*
|
|
5
|
+
* The checkout launcher, the Stripe webhook receiver, customer mapping and
|
|
6
|
+
* `lib/stripe.ts` ship unconditionally now — they are no longer registered
|
|
7
|
+
* behind `isModuleEnabled("commerce")`. What decides whether they SERVE is the
|
|
8
|
+
* JWT `features[]` claim `commerce`, read at CALL time through `checkLicense()`
|
|
9
|
+
* from `@working-theory/license`.
|
|
10
|
+
*
|
|
11
|
+
* This is the same shape as `./require-paid-tier.ts` — that file is the
|
|
12
|
+
* PATTERN, not the code (D-64): this gate reads
|
|
13
|
+
* `result.features.includes("commerce")` and never looks at `result.tier` as
|
|
14
|
+
* the claim test. A `commerce` claim can ride any paid level, so a tier test
|
|
15
|
+
* would be the wrong question.
|
|
16
|
+
*
|
|
17
|
+
* Response without the claim: the SAME module-disabled response the
|
|
18
|
+
* module-conditional path produced — a 404. `server.ts`'s root-endpoint
|
|
19
|
+
* comment (§ `deriveRootEndpoints`) documents that a build with commerce off
|
|
20
|
+
* simply does not advertise these routes and answers 404 on them, and
|
|
21
|
+
* `src/__tests__/module-exclusion.test.ts` measures exactly that status. The
|
|
22
|
+
* body is `notFoundError()` from `./errors.ts` — this repo's one reusable 404
|
|
23
|
+
* body — rather than a newly invented `module_disabled` code, which exists
|
|
24
|
+
* nowhere in the tree. The message deliberately does not mention commerce: a
|
|
25
|
+
* site without the claim is indistinguishable from a site built without the
|
|
26
|
+
* module, which is the point.
|
|
27
|
+
*
|
|
28
|
+
* ── TRI-STATE, not a boolean ────────────────────────────────────────────────
|
|
29
|
+
*
|
|
30
|
+
* Security review 2026-09-16, High; widened by the operator in round 2. A
|
|
31
|
+
* single `false` for "no claim" and "could not tell" is a data-loss bug on the
|
|
32
|
+
* webhook: a signed `checkout.session.completed` on a live commerce site whose
|
|
33
|
+
* licence read merely FAILED would be acked 200 and dropped, Stripe would
|
|
34
|
+
* record success and never retry, and the charge would never be mapped.
|
|
35
|
+
*
|
|
36
|
+
* So `resolveCommerceClaim()` answers `claimed | unclaimed | undetermined`:
|
|
37
|
+
*
|
|
38
|
+
* claimed — read successfully; `features[]` carries `commerce`.
|
|
39
|
+
* unclaimed — a positively-established "no": either a licence that reads
|
|
40
|
+
* successfully and simply lacks the claim, or a KEYLESS
|
|
41
|
+
* deployment that configures no licence at all.
|
|
42
|
+
* undetermined — the read established no "no". These states reach it:
|
|
43
|
+
* 1. `checkLicense()` threw.
|
|
44
|
+
* 2. `revoked === true` — a revoked licence is still a
|
|
45
|
+
* PAYING site inside its JWT window (ADR 0014).
|
|
46
|
+
* 3. a licence IS configured yet resolves free — the JWT
|
|
47
|
+
* expired, or no longer verifies.
|
|
48
|
+
* 4. (3) also absorbs a configured-but-unparseable key.
|
|
49
|
+
* 5. the licence STORAGE FILE could not be read — EACCES,
|
|
50
|
+
* EIO, EMFILE, a directory at its path (#5820).
|
|
51
|
+
*
|
|
52
|
+
* Why (3) needs the env read. `checkLicense()` returns the SAME `FREE_RESULT`
|
|
53
|
+
* for expired, invalid and keyless alike (`check-license.ts` — `expiresAt` is
|
|
54
|
+
* `null` and `revoked` is `false` in all three), so nothing on `LicenseResult`
|
|
55
|
+
* separates "this deployment's licence stopped verifying" from "this
|
|
56
|
+
* deployment never had one". The one signal that does is whether a licence is
|
|
57
|
+
* CONFIGURED at all, which is an env read this app already makes elsewhere
|
|
58
|
+
* (`services/enrichment-shim.ts`). No JWT is parsed here and no verification
|
|
59
|
+
* is re-implemented — presence is the entire test.
|
|
60
|
+
*
|
|
61
|
+
* ── The storage file, read for what it actually says (#5820) ────────────────
|
|
62
|
+
*
|
|
63
|
+
* A licence sourced from the refresh STORAGE FILE is covered on both axes.
|
|
64
|
+
* `readStoredJwtState()` — added to `@working-theory/license` for this gate —
|
|
65
|
+
* answers `absent | unreadable | present` where `readStoredJwt()` answered one
|
|
66
|
+
* `null` for the first two. Only `absent` (no file, or a file holding nothing
|
|
67
|
+
* but whitespace) is evidence that a deployment is unlicensed; a file that
|
|
68
|
+
* merely could not be READ is evidence of nothing, so it holds. An EXPIRED
|
|
69
|
+
* storage licence (`present`, resolving free) holds too.
|
|
70
|
+
*
|
|
71
|
+
* Before that distinction existed, a transient EACCES on a storage-licensed
|
|
72
|
+
* site read as keyless: a signed `checkout.session.completed` arriving in that
|
|
73
|
+
* window was acked 200, Stripe never redelivered it, and the charge was never
|
|
74
|
+
* mapped. `readStoredJwt()`'s own contract is untouched — `checkLicense()` and
|
|
75
|
+
* every other caller still see `null` for both states.
|
|
76
|
+
*
|
|
77
|
+
* ── Why the two routes answer differently ───────────────────────────────────
|
|
78
|
+
*
|
|
79
|
+
* The checkout launcher answers the module-disabled 404 on BOTH non-serving
|
|
80
|
+
* states: a checkout that cannot be started loses nothing, so there is no call
|
|
81
|
+
* to leak a "configured but unreadable" shape on it. The webhook receiver
|
|
82
|
+
* holds an `undetermined` event with a retryable 503 + `Retry-After`, because
|
|
83
|
+
* by the time it arrives the buyer's money has already moved.
|
|
84
|
+
*
|
|
85
|
+
* Usage as a Fastify preHandler (the checkout launcher):
|
|
86
|
+
*
|
|
87
|
+
* fastify.post("/path", { preHandler: requireCommerceClaim }, handler);
|
|
88
|
+
*
|
|
89
|
+
* The Stripe webhook receiver does NOT use the preHandler: D3 puts signature
|
|
90
|
+
* verification BEFORE the claim check, so it consults `resolveCommerceClaim()`
|
|
91
|
+
* from inside its handler. See `routes/webhooks/README.md` § Commerce claim.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
import {
|
|
95
|
+
checkLicense,
|
|
96
|
+
readStoredJwtState,
|
|
97
|
+
type ProFeature,
|
|
98
|
+
} from "@working-theory/license";
|
|
99
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
100
|
+
|
|
101
|
+
import { notFoundError } from "./errors.js";
|
|
102
|
+
|
|
103
|
+
/** The claim the commerce primitives serve behind (`ProFeature`, verify-jwt.ts). */
|
|
104
|
+
export const COMMERCE_FEATURE: ProFeature = "commerce";
|
|
105
|
+
|
|
106
|
+
const MODULE_DISABLED_MESSAGE = "Not found";
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What the licence read actually established. See the header for the four
|
|
110
|
+
* states that reach `undetermined`.
|
|
111
|
+
*/
|
|
112
|
+
export type CommerceClaimState = "claimed" | "unclaimed" | "undetermined";
|
|
113
|
+
|
|
114
|
+
/** The module-disabled response — a 404, the canonical `not_found` body. */
|
|
115
|
+
export function commerceDisabledResponse() {
|
|
116
|
+
return notFoundError(MODULE_DISABLED_MESSAGE);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The retryable answer for `undetermined` on the webhook. A local error
|
|
121
|
+
* vocabulary (`{ error: "..." }`), matching what `routes/webhooks/stripe.ts`
|
|
122
|
+
* already emits for its own failure modes, rather than `errors.ts`'s closed
|
|
123
|
+
* `ErrorCode` set, which has no service-unavailable member and whose MCP twin
|
|
124
|
+
* would have to grow one too.
|
|
125
|
+
*
|
|
126
|
+
* `Retry-After` is in SECONDS and is read by Stripe: five minutes is long
|
|
127
|
+
* enough for a transient licence-read failure or a refresh round-trip to
|
|
128
|
+
* clear, and short enough that a queued `checkout.session.completed` is mapped
|
|
129
|
+
* well inside Stripe's three-day redelivery window.
|
|
130
|
+
*/
|
|
131
|
+
export const LICENSE_UNDETERMINED_STATUS = 503;
|
|
132
|
+
export const LICENSE_UNDETERMINED_BODY = { error: "license-undetermined" };
|
|
133
|
+
export const LICENSE_UNDETERMINED_RETRY_AFTER_SECONDS = 300;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* What this deployment's licence CONFIGURATION says — three answers, not two.
|
|
137
|
+
*
|
|
138
|
+
* configured — an env key is set, or the storage file holds a token.
|
|
139
|
+
* absent — no env key, and the storage file is genuinely not there (or
|
|
140
|
+
* holds nothing but whitespace). The only state that is
|
|
141
|
+
* evidence of an unlicensed site.
|
|
142
|
+
* unreadable — no env key, and the storage file could not be read at all.
|
|
143
|
+
*
|
|
144
|
+
* Presence only — the token is never parsed, verified or logged. This is the
|
|
145
|
+
* one signal that separates an EXPIRED/unverifiable licence from a keyless
|
|
146
|
+
* site, because `checkLicense()` collapses both into the same free result.
|
|
147
|
+
*
|
|
148
|
+
* It asks the question the SAME WAY `check-license.ts#resolveToken` does —
|
|
149
|
+
* `AILK_LICENSE_KEY`, else the refresh storage file — because that is where
|
|
150
|
+
* `checkLicense()` itself just looked. An env-only test would miss a
|
|
151
|
+
* deployment licensed purely from storage, so an EXPIRED storage licence would
|
|
152
|
+
* read as keyless and its already-charged events would be dropped.
|
|
153
|
+
*
|
|
154
|
+
* `unreadable` is kept separate from `absent` rather than folded into
|
|
155
|
+
* `configured` so the caller can LOG why it is holding (#5820): "this site has
|
|
156
|
+
* no licence" and "this site's licence could not be read" are different
|
|
157
|
+
* operator problems, and only the first is a fact.
|
|
158
|
+
*/
|
|
159
|
+
type LicenseConfigurationState = "configured" | "absent" | "unreadable";
|
|
160
|
+
|
|
161
|
+
async function resolveLicenseConfiguration(): Promise<LicenseConfigurationState> {
|
|
162
|
+
const key = process.env["AILK_LICENSE_KEY"];
|
|
163
|
+
if (typeof key === "string" && key.length > 0) return "configured";
|
|
164
|
+
|
|
165
|
+
const stored = await readStoredJwtState();
|
|
166
|
+
if (stored.state === "present") return "configured";
|
|
167
|
+
if (stored.state === "absent") return "absent";
|
|
168
|
+
|
|
169
|
+
// `console.warn` rather than a request logger: `resolveCommerceClaim()` is
|
|
170
|
+
// called from the webhook handler with no logger threaded through, and the
|
|
171
|
+
// routes are out of scope for this change. The cause's `code` is the whole
|
|
172
|
+
// point of the line — EACCES and EISDIR are different operator fixes. The
|
|
173
|
+
// token is never in scope here, so nothing secret can reach the log.
|
|
174
|
+
const code =
|
|
175
|
+
(stored.cause as NodeJS.ErrnoException | undefined)?.code ?? "UNKNOWN";
|
|
176
|
+
console.warn(
|
|
177
|
+
"commerce-claim: licence storage file unreadable — holding rather than dropping",
|
|
178
|
+
{ outcome: "license_storage_unreadable", code },
|
|
179
|
+
);
|
|
180
|
+
return "unreadable";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Resolve what this deployment's licence says about `commerce`.
|
|
185
|
+
*
|
|
186
|
+
* Never throws: a failed read is a state, not an exception, because both
|
|
187
|
+
* callers must be able to tell "no" from "could not tell".
|
|
188
|
+
*/
|
|
189
|
+
export async function resolveCommerceClaim(): Promise<CommerceClaimState> {
|
|
190
|
+
let result;
|
|
191
|
+
try {
|
|
192
|
+
result = await checkLicense();
|
|
193
|
+
} catch {
|
|
194
|
+
// The read FAILED. Not a "no" — callers hold rather than drop.
|
|
195
|
+
return "undetermined";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// A revoked licence keeps verifying locally until JWT `exp` (ADR 0014), so
|
|
199
|
+
// this is a PAYING commerce site mid-window whose events are still real.
|
|
200
|
+
// Closing the gate is right; DROPPING them is the security review's High
|
|
201
|
+
// under a different label.
|
|
202
|
+
if (result.revoked === true) return "undetermined";
|
|
203
|
+
|
|
204
|
+
if (result.features.includes(COMMERCE_FEATURE)) return "claimed";
|
|
205
|
+
|
|
206
|
+
if (result.tier === "free") {
|
|
207
|
+
const configuration = await resolveLicenseConfiguration();
|
|
208
|
+
// A licence is configured yet resolved free: expired, or no longer
|
|
209
|
+
// verifying. `checkLicense()` cannot say which, and neither is a "no".
|
|
210
|
+
if (configuration === "configured") return "undetermined";
|
|
211
|
+
// The storage file could not be read (#5820). An unreadable file is not
|
|
212
|
+
// evidence of a keyless site, and dropping a signed Stripe event needs
|
|
213
|
+
// evidence — so this holds and Stripe retries.
|
|
214
|
+
if (configuration === "unreadable") return "undetermined";
|
|
215
|
+
// `absent` — no env key and no storage file. A positively-established
|
|
216
|
+
// "no", and the only state on this path that may drop an event.
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return "unclaimed";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Fastify preHandler for the checkout launcher: serve only with the claim.
|
|
224
|
+
*
|
|
225
|
+
* Both non-serving states answer the module-disabled 404 — see the header:
|
|
226
|
+
* an unstartable checkout loses nothing, so the route stays indistinguishable
|
|
227
|
+
* from a build without the module rather than advertising that a licence
|
|
228
|
+
* exists but could not be read. The `undetermined` case is still logged, so
|
|
229
|
+
* an operator can tell a lapsed licence from a deliberate free build.
|
|
230
|
+
*
|
|
231
|
+
* Returns void (proceed) with the claim and short-circuits with `reply.send`
|
|
232
|
+
* otherwise, matching Fastify's preHandler contract.
|
|
233
|
+
*/
|
|
234
|
+
export async function requireCommerceClaim(
|
|
235
|
+
request: FastifyRequest,
|
|
236
|
+
reply: FastifyReply,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
const state = await resolveCommerceClaim();
|
|
239
|
+
if (state === "claimed") return;
|
|
240
|
+
|
|
241
|
+
if (state === "undetermined") {
|
|
242
|
+
request.log.warn(
|
|
243
|
+
{ outcome: "license_undetermined" },
|
|
244
|
+
"commerce licence could not be read — checkout answering the module-disabled response",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const err = commerceDisabledResponse();
|
|
249
|
+
return reply.status(err.status).send(err.body);
|
|
250
|
+
}
|
|
@@ -1,16 +1,215 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file site.ts
|
|
3
|
-
* @description The current-site resolution helper (#3958 §3.2)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
3
|
+
* @description The current-site resolution helper (#3958 §3.2), extended by
|
|
4
|
+
* #6155 (spec D2/D5/D6, ownership brief
|
|
5
|
+
* `docs/internal/decisions/workspace-owns-data-surfaces-render.md`) with the
|
|
6
|
+
* ALIAS -> WORKSPACE resolution the brief requires: `currentSiteId()`'s
|
|
7
|
+
* env-var alias is resolved to a real `workspaces.id` ONCE at startup,
|
|
8
|
+
* memoized, and ASSERTED — so a misconfigured deployment refuses to boot
|
|
9
|
+
* instead of serving empty lists (AC-5).
|
|
10
|
+
*
|
|
11
|
+
* `currentSiteId()` is extracted verbatim from `routes/flows/index.ts`'s
|
|
12
|
+
* private `currentSiteId()` so the flow-preset seed
|
|
13
|
+
* (`services/flow-engine.ts` -> `bin/seed-presets.ts`) resolves the SAME
|
|
14
|
+
* site id the `/v1/flows/*` routes resolve — a seed hard-coding `"default"`
|
|
15
|
+
* would write presets the API cannot find whenever `AILK_SITE_ID` is set (a
|
|
16
|
+
* silent, environment-dependent 404). Behavior is byte-identical to the
|
|
17
|
+
* pre-extraction helper: same env var, same fallback, same call sites.
|
|
18
|
+
* `currentSiteId()` stays SYNCHRONOUS and byte-identical (#6155 D5) — the
|
|
19
|
+
* new resolution is async (it reads the database), and making
|
|
20
|
+
* `currentSiteId()` async would edit every one of its dozen non-test call
|
|
21
|
+
* sites across routes/services/bins, including the two route files NFR1
|
|
22
|
+
* requires to stay byte-unchanged.
|
|
12
23
|
*/
|
|
24
|
+
import { resolvedAdapterKind } from "@working-theory/content-adapters";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_WORKSPACE_ID,
|
|
27
|
+
DEFAULT_WORKSPACE_SLUG,
|
|
28
|
+
resolveSentinelWorkspaceRow,
|
|
29
|
+
} from "@working-theory/database/tenancy";
|
|
30
|
+
import { logger } from "@working-theory/observability";
|
|
31
|
+
|
|
32
|
+
import { prisma } from "./prisma.js";
|
|
13
33
|
|
|
14
34
|
export function currentSiteId(): string {
|
|
15
35
|
return process.env.AILK_SITE_ID ?? "default";
|
|
16
36
|
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The resolved workspace id, memoized after the first SUCCESSFUL resolution
|
|
40
|
+
* ("resolves the alias once at startup", ownership brief § Layer 2 / #6155
|
|
41
|
+
* D5). `null` is never cached — a failed resolution has already thrown from
|
|
42
|
+
* `build()` (D6), so there is no negative-cache case to hold, and a later
|
|
43
|
+
* retry (a test, a re-seeded database) is free to resolve on its next call.
|
|
44
|
+
*/
|
|
45
|
+
let cachedWorkspaceId: string | null = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolves `currentSiteId()`'s alias to a real `workspaces.id`, per the
|
|
49
|
+
* two-and-only-two rule (#6155 spec D2):
|
|
50
|
+
*
|
|
51
|
+
* 1. The literal alias `"default"` resolves to the seeded sentinel
|
|
52
|
+
* workspace — `DEFAULT_WORKSPACE_ID` — IFF that row exists and hangs
|
|
53
|
+
* off the sentinel Organization
|
|
54
|
+
* (`@working-theory/database/tenancy`'s `resolveSentinelWorkspaceRow`,
|
|
55
|
+
* which carries NO `workspace.count()` gate — unlike
|
|
56
|
+
* `resolveDefaultWorkspaceRow`, whose single-tenancy meaning this unit
|
|
57
|
+
* must NOT widen; see D2's "why a NEW resolver"). This is what lets a
|
|
58
|
+
* correctly-configured MULTI-workspace deployment that has not pinned
|
|
59
|
+
* `AILK_SITE_ID` still resolve — `resolveDefaultWorkspaceRow`'s count
|
|
60
|
+
* gate would refuse it.
|
|
61
|
+
* 2. Any OTHER alias resolves ONLY by exact `workspaces.id` equality.
|
|
62
|
+
* 3. Anything that resolves neither way is `null` — never a fallback,
|
|
63
|
+
* never a guess (`assertSiteAliasResolves` below is what turns a `null`
|
|
64
|
+
* into a startup refusal).
|
|
65
|
+
*
|
|
66
|
+
* Memoized (D5): the SECOND call issues zero Prisma queries once the first
|
|
67
|
+
* has resolved successfully. `__resetSiteWorkspaceCacheForTests()` clears
|
|
68
|
+
* the memo for test isolation.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveSiteWorkspaceId(): Promise<string | null> {
|
|
71
|
+
if (cachedWorkspaceId !== null) {
|
|
72
|
+
return cachedWorkspaceId;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const resolved = await resolveWorkspaceIdForAlias(currentSiteId());
|
|
76
|
+
if (resolved !== null) {
|
|
77
|
+
cachedWorkspaceId = resolved;
|
|
78
|
+
}
|
|
79
|
+
return resolved;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The two-and-only-two resolution rule (#6155 spec D2), extracted so a
|
|
84
|
+
* write site holding an EXPLICIT `siteId` parameter (not necessarily
|
|
85
|
+
* `currentSiteId()` — e.g. a `ProjectListing` resolved through its own
|
|
86
|
+
* `SiteListingConfig.siteId`) can resolve it the same way
|
|
87
|
+
* `resolveSiteWorkspaceId()` resolves the env alias, without going through
|
|
88
|
+
* that function's `currentSiteId()`-only memo (#6254 spec D1a): the write
|
|
89
|
+
* paths that still stamp a raw, unresolved alias resolve it first, through
|
|
90
|
+
* this resolver, before it ever reaches a `workspace_id NOT NULL` /
|
|
91
|
+
* `CHECK (workspace_id = site_id)` column.
|
|
92
|
+
*
|
|
93
|
+
* 1. The literal alias `"default"` resolves to the seeded sentinel
|
|
94
|
+
* workspace IFF that row exists (`resolveSentinelWorkspaceRow`).
|
|
95
|
+
* 2. Any OTHER alias resolves ONLY by exact `workspaces.id` equality.
|
|
96
|
+
* 3. Anything that resolves neither way is `null` — never a fallback,
|
|
97
|
+
* never a guess.
|
|
98
|
+
*
|
|
99
|
+
* Not memoized — callers passing a per-request `siteId` would otherwise
|
|
100
|
+
* share a cache keyed on nothing. `resolveSiteWorkspaceId()` above keeps
|
|
101
|
+
* its own single-value memo for the hot, env-alias-only path.
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveWorkspaceIdForAlias(
|
|
104
|
+
alias: string,
|
|
105
|
+
): Promise<string | null> {
|
|
106
|
+
if (alias === DEFAULT_WORKSPACE_SLUG) {
|
|
107
|
+
const row = await resolveSentinelWorkspaceRow(prisma);
|
|
108
|
+
return row?.id ?? null;
|
|
109
|
+
}
|
|
110
|
+
const row = await prisma.workspace.findUnique({ where: { id: alias } });
|
|
111
|
+
return row ? alias : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Asserts `currentSiteId()`'s alias resolves to a real workspace — the
|
|
116
|
+
* misconfigured-deployment refusal AC-5 requires (D6). Awaited from
|
|
117
|
+
* `build()` (`apps/api/src/server.ts`), beside the two startup assertions
|
|
118
|
+
* already there; `apps/api/src/index.ts` already catches, logs and
|
|
119
|
+
* `process.exit(1)`s on a `build()` throw, so this is a loud refusal to
|
|
120
|
+
* start with no new exit-path code.
|
|
121
|
+
*
|
|
122
|
+
* THE CARVE-OUT (D6): when the alias genuinely CANNOT be checked — or NEED
|
|
123
|
+
* not be — this logs a warning and returns rather than throwing: a
|
|
124
|
+
* "database-less dev boot," never a misconfiguration. Three arms feed it, and
|
|
125
|
+
* ALL THREE are presence checks made BEFORE any query is attempted — never a
|
|
126
|
+
* catch around a query that already ran, which would just as readily swallow a
|
|
127
|
+
* genuine connection failure as it would an absent delegate (code-review
|
|
128
|
+
* finding, #6155):
|
|
129
|
+
*
|
|
130
|
+
* 0. **The content adapter is not `database`** (#6228) — the one arm that is
|
|
131
|
+
* "need not," not "cannot." The alias→workspace resolution exists so a
|
|
132
|
+
* database-backed content read has a workspace to read within; under
|
|
133
|
+
* `mdx` or `projection` NO read takes a `workspaceId` at all, so the
|
|
134
|
+
* assertion guards nothing and its only effect is to require a REACHABLE
|
|
135
|
+
* database of a deployment that never queries one. That is the #6228
|
|
136
|
+
* defect: `create-ailk`'s scaffold check sets a placeholder
|
|
137
|
+
* `DATABASE_URL` (presence is all `validateConfig()` wants, and Prisma
|
|
138
|
+
* connects lazily), so arm 1 does not fire, and the generated MDX site
|
|
139
|
+
* crashed at boot on `Can't reach database server`. Read through
|
|
140
|
+
* `resolvedAdapterKind()` — the ONE switch over `CONTENT_ADAPTER`
|
|
141
|
+
* (`packages/content-adapters/src/resolve.ts`) — rather than a second,
|
|
142
|
+
* drifting read of the env var here.
|
|
143
|
+
*
|
|
144
|
+
* 1. **No `DATABASE_URL` configured** — the literal `requireDb()` guard
|
|
145
|
+
* `apps/api/src/services/flow-engine.ts` uses for the flow services:
|
|
146
|
+
* presence, not reachability. (`apps/api/src/config/index.ts`'s own
|
|
147
|
+
* eager `validateConfig()` already refuses a real `apps/api` boot with
|
|
148
|
+
* `DATABASE_URL` unset before `build()` is ever reached, so this arm is
|
|
149
|
+
* for any OTHER caller of `build()`.)
|
|
150
|
+
* 2. **`prisma.workspace.findUnique` is not even a function** — the case
|
|
151
|
+
* that surfaced this arm: `apps/api`'s shared/global prisma mocks
|
|
152
|
+
* (`lib/__mocks__/prisma-client.js`, the module-map-level double every
|
|
153
|
+
* test gets; the many hand-rolled hoisted factories in route/service
|
|
154
|
+
* suites that never touch tenancy) predate this unit and declare no
|
|
155
|
+
* `workspace` delegate at all — the FIRST real Prisma read `build()`
|
|
156
|
+
* itself has ever issued. Widening dozens of unrelated test files'
|
|
157
|
+
* prisma doubles to add a resolving `workspace.findUnique` has a far
|
|
158
|
+
* larger and harder-to-audit blast radius than making THIS one
|
|
159
|
+
* assertion resilient to "the check itself is unavailable." A resolution
|
|
160
|
+
* that could not even be ATTEMPTED is not a misconfiguration to fail
|
|
161
|
+
* loudly about — but a resolution that WAS attempted and threw (a real
|
|
162
|
+
* connection error, a permissions error, a bug in the resolver) is
|
|
163
|
+
* exactly the class D6/AC-5 exist to surface, so it is deliberately NOT
|
|
164
|
+
* caught here: it propagates out of `resolveSiteWorkspaceId()` and
|
|
165
|
+
* crashes the boot, the same as any other unhandled `build()` throw.
|
|
166
|
+
*
|
|
167
|
+
* A reachable database with no matching workspace is the failure case, and
|
|
168
|
+
* it throws. A reachable database that ERRORS mid-query throws too — that is
|
|
169
|
+
* NOT the carve-out, it is the misconfiguration AC-5 asks to fail loudly
|
|
170
|
+
* about.
|
|
171
|
+
*/
|
|
172
|
+
export async function assertSiteAliasResolves(): Promise<void> {
|
|
173
|
+
const adapterKind = resolvedAdapterKind();
|
|
174
|
+
if (adapterKind !== "database") {
|
|
175
|
+
logger.warn(
|
|
176
|
+
{ alias: currentSiteId(), contentAdapter: adapterKind },
|
|
177
|
+
"[site] content adapter is not `database` — skipping the site-alias workspace assertion (no read takes a workspaceId on this boot)",
|
|
178
|
+
);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!process.env.DATABASE_URL) {
|
|
183
|
+
logger.warn(
|
|
184
|
+
{ alias: currentSiteId() },
|
|
185
|
+
"[site] DATABASE_URL not set — skipping the site-alias workspace assertion (database-less boot)",
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof prisma.workspace?.findUnique !== "function") {
|
|
191
|
+
logger.warn(
|
|
192
|
+
{ alias: currentSiteId() },
|
|
193
|
+
"[site] prisma.workspace is unavailable — skipping the site-alias workspace assertion (the resolution could not be attempted)",
|
|
194
|
+
);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const resolved = await resolveSiteWorkspaceId();
|
|
199
|
+
if (resolved !== null) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const alias = currentSiteId();
|
|
204
|
+
throw new Error(
|
|
205
|
+
`AILK_SITE_ID="${alias}" names no workspace. The site alias must be an ` +
|
|
206
|
+
'existing workspaces.id, or the literal "default" (the seeded ' +
|
|
207
|
+
`sentinel workspace ${DEFAULT_WORKSPACE_ID}). Run \`pnpm db:seed\`, or ` +
|
|
208
|
+
"set AILK_SITE_ID to a workspace this database holds.",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Test-only: clears the memoized resolution (#6155 D5). */
|
|
213
|
+
export function __resetSiteWorkspaceCacheForTests(): void {
|
|
214
|
+
cachedWorkspaceId = null;
|
|
215
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apps/api Stripe shim.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports @working-theory/stripe-client's public surface for use by routes and
|
|
5
|
+
* services under apps/api. This is the SOLE Stripe-touching file in apps/api.
|
|
6
|
+
*
|
|
7
|
+
* Future routes import from here (`from '../lib/stripe'`), not directly from
|
|
8
|
+
* '@working-theory/stripe-client' — single chokepoint for AILK-internal ergonomics
|
|
9
|
+
* (logger context, request-id propagation) without touching every consumer.
|
|
10
|
+
*/
|
|
11
|
+
import { getStripeClient } from "@working-theory/stripe-client";
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
assertSandboxKey,
|
|
15
|
+
createAdHocPrice,
|
|
16
|
+
createBillingPortalSession,
|
|
17
|
+
createCheckoutSession,
|
|
18
|
+
createPaymentLink,
|
|
19
|
+
createQuotedCheckoutSession,
|
|
20
|
+
getStripeClient,
|
|
21
|
+
// Checkout session read (#6084 D9) — re-exported through this sole
|
|
22
|
+
// Stripe-touching chokepoint, same as every other Stripe call here.
|
|
23
|
+
retrieveCheckoutSession,
|
|
24
|
+
STRIPE_API_VERSION,
|
|
25
|
+
StripeConfigError,
|
|
26
|
+
StripeWebhookSignatureError,
|
|
27
|
+
toSubscriptionLifecycleStatus,
|
|
28
|
+
UnknownProductCodeError,
|
|
29
|
+
UnknownSessionError,
|
|
30
|
+
verifyWebhookSignature,
|
|
31
|
+
WEBHOOK_TOLERANCE_SECONDS,
|
|
32
|
+
} from "@working-theory/stripe-client";
|
|
33
|
+
|
|
34
|
+
export type {
|
|
35
|
+
CheckoutSessionLineItem,
|
|
36
|
+
CreateAdHocPriceInput,
|
|
37
|
+
CreateAdHocPriceOutput,
|
|
38
|
+
CreateBillingPortalSessionInput,
|
|
39
|
+
CreateBillingPortalSessionOutput,
|
|
40
|
+
CreatePaymentLinkInput,
|
|
41
|
+
CreatePaymentLinkOutput,
|
|
42
|
+
CreateCheckoutSessionInput,
|
|
43
|
+
CreateCheckoutSessionOutput,
|
|
44
|
+
CreateCheckoutSessionParams,
|
|
45
|
+
CreateQuotedCheckoutSessionInput,
|
|
46
|
+
CreateQuotedCheckoutSessionOutput,
|
|
47
|
+
RetrieveCheckoutSessionInput,
|
|
48
|
+
RetrieveCheckoutSessionOutput,
|
|
49
|
+
Stripe,
|
|
50
|
+
StripeCheckoutSession,
|
|
51
|
+
StripeWebhookEvent,
|
|
52
|
+
} from "@working-theory/stripe-client";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Expire a Checkout Session (#4912 FR7/D7).
|
|
56
|
+
*
|
|
57
|
+
* The compensation the recommender's attach-miss branch owes: the Stripe
|
|
58
|
+
* session already exists, its id never reached the `FlowCheckout` row, and
|
|
59
|
+
* nothing will ever join the two — so the session is closed rather than left
|
|
60
|
+
* live and payable (#4863). Built on the already-re-exported
|
|
61
|
+
* `getStripeClient`, whose construction asserts the `sk_test_` allow-list, so
|
|
62
|
+
* this call inherits the test-mode gate like every other Stripe call here.
|
|
63
|
+
*/
|
|
64
|
+
export async function expireCheckoutSession(stripeSessionId: string): Promise<void> {
|
|
65
|
+
await getStripeClient().checkout.sessions.expire(stripeSessionId);
|
|
66
|
+
}
|