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,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file artifact-content-store.db.test.ts
|
|
3
|
+
*
|
|
4
|
+
* LIVE-DATABASE proof of roadmap D1/D7 (#5800) — the generalized `Artifact` and
|
|
5
|
+
* the workspace-scoped `Asset`. Requires DATABASE_URL pointing at a Postgres
|
|
6
|
+
* with the migrations applied (`prisma migrate deploy`); the whole suite SKIPS
|
|
7
|
+
* when DATABASE_URL is unset, matching `prefixed-ids.db.test.ts` and keeping the
|
|
8
|
+
* workspace Jest run DB-free.
|
|
9
|
+
*
|
|
10
|
+
* Raw SQL through `pg` for the same reason that file gives: the generated Prisma
|
|
11
|
+
* client is ESM-only and cannot load under this package's CJS ts-jest transform,
|
|
12
|
+
* and raw SQL is the sharper proof anyway — a constraint must hold on the path
|
|
13
|
+
* no application helper can guard.
|
|
14
|
+
*
|
|
15
|
+
* What it proves:
|
|
16
|
+
* 1. THE SURVIVAL PROPERTY (D1). An artifact produced inside an agent session
|
|
17
|
+
* outlives that session: deleting the session sets `session_id` to NULL and
|
|
18
|
+
* leaves the row, its versions, and its publish pointer intact. Before this
|
|
19
|
+
* migration the FK was `ON DELETE CASCADE` on a NOT NULL column, so the
|
|
20
|
+
* same delete destroyed the workspace's content. This is the whole point of
|
|
21
|
+
* "the session becomes provenance, the workspace owns the artifact".
|
|
22
|
+
* 2. The identity of a published page — `(workspace_id, content_type, locale,
|
|
23
|
+
* slug)` is unique, while unpublished drafts (slug NULL) are unconstrained,
|
|
24
|
+
* because NULLs never collide in a Postgres unique index.
|
|
25
|
+
* 3. `published_version_id` is an independent pointer: it is NOT `is_current`,
|
|
26
|
+
* and appending a newer draft tip does not move it.
|
|
27
|
+
* 4. `assets` is workspace-scoped with a unique `(workspace_id, key)` and a
|
|
28
|
+
* DB-minted `asset_` UUIDv7 id (ADR-0031).
|
|
29
|
+
*/
|
|
30
|
+
import { randomUUID } from "node:crypto";
|
|
31
|
+
|
|
32
|
+
import { Client } from "pg";
|
|
33
|
+
|
|
34
|
+
import { DEFAULT_WORKSPACE_ID } from "../tenancy.js";
|
|
35
|
+
|
|
36
|
+
const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
|
|
37
|
+
|
|
38
|
+
const V7_RE =
|
|
39
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
40
|
+
|
|
41
|
+
/** A run-unique suffix so repeated local runs never collide on the unique key. */
|
|
42
|
+
const RUN = randomUUID().slice(0, 8);
|
|
43
|
+
|
|
44
|
+
describeDb("Artifact content store (roadmap D1/D7, live DB)", () => {
|
|
45
|
+
let db: Client;
|
|
46
|
+
|
|
47
|
+
beforeAll(async () => {
|
|
48
|
+
db = new Client({ connectionString: process.env.DATABASE_URL });
|
|
49
|
+
await db.connect();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterAll(async () => {
|
|
53
|
+
await db.query(
|
|
54
|
+
`DELETE FROM "artifacts" WHERE "title" LIKE $1`,
|
|
55
|
+
[`5800-${RUN}%`],
|
|
56
|
+
);
|
|
57
|
+
await db.query(`DELETE FROM "agent_sessions" WHERE "title" LIKE $1`, [
|
|
58
|
+
`5800-${RUN}%`,
|
|
59
|
+
]);
|
|
60
|
+
await db.query(`DELETE FROM "assets" WHERE "key" LIKE $1`, [
|
|
61
|
+
`5800-${RUN}%`,
|
|
62
|
+
]);
|
|
63
|
+
await db.end();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("the session is provenance — deleting it never destroys content", () => {
|
|
67
|
+
it("sets session_id NULL and leaves the artifact, its versions and its publish pointer", async () => {
|
|
68
|
+
const sessionId = (
|
|
69
|
+
await db.query<{ id: string }>(
|
|
70
|
+
`INSERT INTO "agent_sessions" ("workspace_id", "title", "updated_at")
|
|
71
|
+
VALUES ($1, $2, now()) RETURNING "id"`,
|
|
72
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN} session`],
|
|
73
|
+
)
|
|
74
|
+
).rows[0]!.id;
|
|
75
|
+
|
|
76
|
+
const artifactId = (
|
|
77
|
+
await db.query<{ id: string }>(
|
|
78
|
+
`INSERT INTO "artifacts" ("session_id", "workspace_id", "title", "content_type", "locale", "slug", "updated_at")
|
|
79
|
+
VALUES ($1, $2, $3, 'text/page', 'en', $4, now()) RETURNING "id"`,
|
|
80
|
+
[
|
|
81
|
+
sessionId,
|
|
82
|
+
DEFAULT_WORKSPACE_ID,
|
|
83
|
+
`5800-${RUN} page`,
|
|
84
|
+
`5800-${RUN}-survivor`,
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
).rows[0]!.id;
|
|
88
|
+
|
|
89
|
+
const versionId = (
|
|
90
|
+
await db.query<{ id: string }>(
|
|
91
|
+
`INSERT INTO "artifact_versions" ("artifact_id", "workspace_id", "content", "is_current")
|
|
92
|
+
VALUES ($1, $2, '{"body":"v1"}'::jsonb, TRUE) RETURNING "id"`,
|
|
93
|
+
[artifactId, DEFAULT_WORKSPACE_ID],
|
|
94
|
+
)
|
|
95
|
+
).rows[0]!.id;
|
|
96
|
+
|
|
97
|
+
await db.query(
|
|
98
|
+
`UPDATE "artifacts" SET "published_version_id" = $1 WHERE "id" = $2`,
|
|
99
|
+
[versionId, artifactId],
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// The act under test: the producing session is deleted outright.
|
|
103
|
+
await db.query(`DELETE FROM "agent_sessions" WHERE "id" = $1`, [
|
|
104
|
+
sessionId,
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const { rows } = await db.query<{
|
|
108
|
+
session_id: string | null;
|
|
109
|
+
workspace_id: string;
|
|
110
|
+
published_version_id: string | null;
|
|
111
|
+
}>(
|
|
112
|
+
`SELECT "session_id", "workspace_id", "published_version_id"
|
|
113
|
+
FROM "artifacts" WHERE "id" = $1`,
|
|
114
|
+
[artifactId],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(rows).toHaveLength(1);
|
|
118
|
+
expect(rows[0]!.session_id).toBeNull();
|
|
119
|
+
expect(rows[0]!.workspace_id).toBe(DEFAULT_WORKSPACE_ID);
|
|
120
|
+
expect(rows[0]!.published_version_id).toBe(versionId);
|
|
121
|
+
|
|
122
|
+
const versions = await db.query(
|
|
123
|
+
`SELECT "id" FROM "artifact_versions" WHERE "artifact_id" = $1`,
|
|
124
|
+
[artifactId],
|
|
125
|
+
);
|
|
126
|
+
expect(versions.rows).toHaveLength(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("accepts an artifact with no session at all — the workspace owns it", async () => {
|
|
130
|
+
const { rows } = await db.query<{ id: string; session_id: string | null }>(
|
|
131
|
+
`INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "updated_at")
|
|
132
|
+
VALUES ($1, $2, 'text/page', 'en', now()) RETURNING "id", "session_id"`,
|
|
133
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN} sessionless`],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(rows[0]!.session_id).toBeNull();
|
|
137
|
+
expect(rows[0]!.id).toMatch(/^artif_/);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("published identity", () => {
|
|
142
|
+
it("rejects a second artifact with the same (workspace, contentType, locale, slug)", async () => {
|
|
143
|
+
const slug = `5800-${RUN}-dupe`;
|
|
144
|
+
await db.query(
|
|
145
|
+
`INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "slug", "updated_at")
|
|
146
|
+
VALUES ($1, $2, 'text/page', 'en', $3, now())`,
|
|
147
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN} first`, slug],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
await expect(
|
|
151
|
+
db.query(
|
|
152
|
+
`INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "slug", "updated_at")
|
|
153
|
+
VALUES ($1, $2, 'text/page', 'en', $3, now())`,
|
|
154
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN} second`, slug],
|
|
155
|
+
),
|
|
156
|
+
).rejects.toThrow(/duplicate key|unique/i);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("admits many unpublished drafts — a NULL slug never collides", async () => {
|
|
160
|
+
for (const n of [1, 2, 3]) {
|
|
161
|
+
await db.query(
|
|
162
|
+
`INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "updated_at")
|
|
163
|
+
VALUES ($1, $2, 'text/page', 'en', now())`,
|
|
164
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN} draft ${n}`],
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { rows } = await db.query<{ count: string }>(
|
|
169
|
+
`SELECT count(*)::text AS count FROM "artifacts"
|
|
170
|
+
WHERE "title" LIKE $1 AND "slug" IS NULL`,
|
|
171
|
+
[`5800-${RUN} draft %`],
|
|
172
|
+
);
|
|
173
|
+
expect(rows[0]!.count).toBe("3");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("separates the same slug across locales", async () => {
|
|
177
|
+
const slug = `5800-${RUN}-locale`;
|
|
178
|
+
for (const locale of ["en", "fr"]) {
|
|
179
|
+
await db.query(
|
|
180
|
+
`INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "slug", "updated_at")
|
|
181
|
+
VALUES ($1, $2, 'text/page', $3, $4, now())`,
|
|
182
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN} ${locale}`, locale, slug],
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const { rows } = await db.query<{ count: string }>(
|
|
187
|
+
`SELECT count(*)::text AS count FROM "artifacts" WHERE "slug" = $1`,
|
|
188
|
+
[slug],
|
|
189
|
+
);
|
|
190
|
+
expect(rows[0]!.count).toBe("2");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe("the publish pointer is not the draft tip", () => {
|
|
195
|
+
it("survives a newer is_current version", async () => {
|
|
196
|
+
const artifactId = (
|
|
197
|
+
await db.query<{ id: string }>(
|
|
198
|
+
`INSERT INTO "artifacts" ("workspace_id", "title", "content_type", "locale", "updated_at")
|
|
199
|
+
VALUES ($1, $2, 'text/page', 'en', now()) RETURNING "id"`,
|
|
200
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN} pointer`],
|
|
201
|
+
)
|
|
202
|
+
).rows[0]!.id;
|
|
203
|
+
|
|
204
|
+
const published = (
|
|
205
|
+
await db.query<{ id: string }>(
|
|
206
|
+
`INSERT INTO "artifact_versions" ("artifact_id", "workspace_id", "content", "is_current")
|
|
207
|
+
VALUES ($1, $2, '{"body":"published"}'::jsonb, TRUE) RETURNING "id"`,
|
|
208
|
+
[artifactId, DEFAULT_WORKSPACE_ID],
|
|
209
|
+
)
|
|
210
|
+
).rows[0]!.id;
|
|
211
|
+
|
|
212
|
+
await db.query(
|
|
213
|
+
`UPDATE "artifacts" SET "published_version_id" = $1 WHERE "id" = $2`,
|
|
214
|
+
[published, artifactId],
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// A newer draft tip lands; the publish pointer must NOT follow it.
|
|
218
|
+
await db.query(
|
|
219
|
+
`UPDATE "artifact_versions" SET "is_current" = FALSE WHERE "artifact_id" = $1`,
|
|
220
|
+
[artifactId],
|
|
221
|
+
);
|
|
222
|
+
const draft = (
|
|
223
|
+
await db.query<{ id: string }>(
|
|
224
|
+
`INSERT INTO "artifact_versions" ("artifact_id", "workspace_id", "content", "is_current")
|
|
225
|
+
VALUES ($1, $2, '{"body":"draft"}'::jsonb, TRUE) RETURNING "id"`,
|
|
226
|
+
[artifactId, DEFAULT_WORKSPACE_ID],
|
|
227
|
+
)
|
|
228
|
+
).rows[0]!.id;
|
|
229
|
+
|
|
230
|
+
const { rows } = await db.query<{ published_version_id: string | null }>(
|
|
231
|
+
`SELECT "published_version_id" FROM "artifacts" WHERE "id" = $1`,
|
|
232
|
+
[artifactId],
|
|
233
|
+
);
|
|
234
|
+
expect(rows[0]!.published_version_id).toBe(published);
|
|
235
|
+
expect(rows[0]!.published_version_id).not.toBe(draft);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe("Asset (D7)", () => {
|
|
240
|
+
it("mints an asset_ UUIDv7 id and stores the bytes", async () => {
|
|
241
|
+
const { rows } = await db.query<{ id: string; size: number }>(
|
|
242
|
+
`INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
|
|
243
|
+
VALUES ($1, $2, 'logo.png', 'image/png', $3, 4) RETURNING "id", "size"`,
|
|
244
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN}-logo`, Buffer.from([1, 2, 3, 4])],
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(rows[0]!.id).toMatch(/^asset_/);
|
|
248
|
+
expect(rows[0]!.id.slice("asset_".length)).toMatch(V7_RE);
|
|
249
|
+
expect(rows[0]!.size).toBe(4);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("rejects a second asset with the same (workspace, key)", async () => {
|
|
253
|
+
const key = `5800-${RUN}-dupe-key`;
|
|
254
|
+
await db.query(
|
|
255
|
+
`INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
|
|
256
|
+
VALUES ($1, $2, 'a.png', 'image/png', $3, 1)`,
|
|
257
|
+
[DEFAULT_WORKSPACE_ID, key, Buffer.from([1])],
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
await expect(
|
|
261
|
+
db.query(
|
|
262
|
+
`INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
|
|
263
|
+
VALUES ($1, $2, 'b.png', 'image/png', $3, 1)`,
|
|
264
|
+
[DEFAULT_WORKSPACE_ID, key, Buffer.from([2])],
|
|
265
|
+
),
|
|
266
|
+
).rejects.toThrow(/duplicate key|unique/i);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("rejects a malformed workspace_id (ADR-0031 CHECK)", async () => {
|
|
270
|
+
await expect(
|
|
271
|
+
db.query(
|
|
272
|
+
`INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
|
|
273
|
+
VALUES ('not-a-workspace-id', $1, 'a.png', 'image/png', $2, 1)`,
|
|
274
|
+
[`5800-${RUN}-bad-scope`, Buffer.from([1])],
|
|
275
|
+
),
|
|
276
|
+
).rejects.toThrow(/assets_workspace_id_format|check constraint/i);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// #6092 (S4/D10) — title, description and tags. `tags` stores SLUGS, never
|
|
280
|
+
// the human labels the lightbox renders (D3): 'ad-creative' and
|
|
281
|
+
// 'user-uploaded' are the exact pair the demo's first asset carries
|
|
282
|
+
// (demo.html:357).
|
|
283
|
+
it("stores and returns two tags", async () => {
|
|
284
|
+
const { rows } = await db.query<{ tags: string[] }>(
|
|
285
|
+
`INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size", "tags")
|
|
286
|
+
VALUES ($1, $2, 'tagged.png', 'image/png', $3, 1, $4) RETURNING "tags"`,
|
|
287
|
+
[
|
|
288
|
+
DEFAULT_WORKSPACE_ID,
|
|
289
|
+
`5800-${RUN}-tagged`,
|
|
290
|
+
Buffer.from([1]),
|
|
291
|
+
["ad-creative", "user-uploaded"],
|
|
292
|
+
],
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(rows[0]!.tags).toEqual(["ad-creative", "user-uploaded"]);
|
|
296
|
+
expect(rows[0]!.tags).toHaveLength(2);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("defaults title, description and tags when none of the three columns is named", async () => {
|
|
300
|
+
const { rows } = await db.query<{
|
|
301
|
+
title: string | null;
|
|
302
|
+
description: string | null;
|
|
303
|
+
tags: string[];
|
|
304
|
+
}>(
|
|
305
|
+
`INSERT INTO "assets" ("workspace_id", "key", "filename", "content_type", "bytes", "size")
|
|
306
|
+
VALUES ($1, $2, 'untagged.png', 'image/png', $3, 1)
|
|
307
|
+
RETURNING "title", "description", "tags"`,
|
|
308
|
+
[DEFAULT_WORKSPACE_ID, `5800-${RUN}-untagged`, Buffer.from([1])],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(rows[0]!.title).toBeNull();
|
|
312
|
+
expect(rows[0]!.description).toBeNull();
|
|
313
|
+
expect(rows[0]!.tags).toEqual([]);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file composite-fence.db.test.ts
|
|
3
|
+
*
|
|
4
|
+
* LIVE-DATABASE proof of the #6254 keystone constraints (AC-2, AC-5, AC-7).
|
|
5
|
+
* Requires DATABASE_URL pointing at a Postgres with the migrations applied
|
|
6
|
+
* (`prisma migrate deploy`); the whole suite SKIPS when DATABASE_URL is
|
|
7
|
+
* unset, matching `flow-workspace-key.db.test.ts` / `prefixed-ids.db.test.ts`
|
|
8
|
+
* and keeping the workspace Jest run DB-free.
|
|
9
|
+
*
|
|
10
|
+
* Raw SQL through `pg`, for the same reason those files give: the generated
|
|
11
|
+
* Prisma client is ESM-only and cannot load under this package's CJS ts-jest
|
|
12
|
+
* transform.
|
|
13
|
+
*
|
|
14
|
+
* This suite proves the CONSTRAINTS database/migrations/
|
|
15
|
+
* 20260922120000_tenancy_fence adds — it does not re-prove that migration's
|
|
16
|
+
* own backfill (that is `flow-workspace-key.db.test.ts`'s job for its two
|
|
17
|
+
* models, and this migration's backfill is largely a defensive re-run of
|
|
18
|
+
* work the #6155/#6203 precedent migrations already did — see this
|
|
19
|
+
* migration's own header comment). What it proves (spec § 8.2):
|
|
20
|
+
*
|
|
21
|
+
* 1. `Lead` rejects a row whose `workspaceId` disagrees with its `siteId`
|
|
22
|
+
* — SQLSTATE 23514 (AC-5's core).
|
|
23
|
+
* 2. `Lead` accepts a row whose `workspaceId` AGREES with its `siteId` —
|
|
24
|
+
* without this, assertion 1 would also pass against a table that
|
|
25
|
+
* rejects everything.
|
|
26
|
+
* 3. `FlowDefinition` rejects the same disagreement — AC-5's "at least
|
|
27
|
+
* one retrofit model besides Lead".
|
|
28
|
+
* 4. NULL is unstorable on `Lead` and `FlowDefinition` — SQLSTATE 23502
|
|
29
|
+
* (not_null_violation) — AC-7.
|
|
30
|
+
* 5. `WorkspaceSiteIdentity` carries the AC-2 composite unique constraint,
|
|
31
|
+
* proven at the database, not just in the schema file.
|
|
32
|
+
* 6. The migration's own literal sentinel id equals `DEFAULT_WORKSPACE_ID`
|
|
33
|
+
* — the two cannot drift apart (the #6155/#6203 precedents' own
|
|
34
|
+
* technique).
|
|
35
|
+
*/
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
37
|
+
import { readFileSync } from "node:fs";
|
|
38
|
+
import { join } from "node:path";
|
|
39
|
+
|
|
40
|
+
import { Client } from "pg";
|
|
41
|
+
|
|
42
|
+
import { DEFAULT_WORKSPACE_ID } from "../tenancy.js";
|
|
43
|
+
|
|
44
|
+
const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
|
|
45
|
+
|
|
46
|
+
/** A run-unique suffix so repeated local runs never collide. */
|
|
47
|
+
const RUN = randomUUID().slice(0, 8);
|
|
48
|
+
|
|
49
|
+
const MIGRATION_SQL_PATH = join(
|
|
50
|
+
__dirname,
|
|
51
|
+
"..",
|
|
52
|
+
"migrations",
|
|
53
|
+
"20260922120000_tenancy_fence",
|
|
54
|
+
"migration.sql",
|
|
55
|
+
);
|
|
56
|
+
const MIGRATION_SQL = readFileSync(MIGRATION_SQL_PATH, "utf-8");
|
|
57
|
+
|
|
58
|
+
/** Postgres's own error shape carries `code` as a 5-character SQLSTATE. */
|
|
59
|
+
type PgError = { code?: string };
|
|
60
|
+
|
|
61
|
+
describeDb("the #6254 tenancy-fence constraints (live DB)", () => {
|
|
62
|
+
let db: Client;
|
|
63
|
+
let workspaceAId: string;
|
|
64
|
+
let workspaceBId: string;
|
|
65
|
+
|
|
66
|
+
beforeAll(async () => {
|
|
67
|
+
db = new Client({ connectionString: process.env.DATABASE_URL });
|
|
68
|
+
await db.connect();
|
|
69
|
+
|
|
70
|
+
// Two REAL, distinct workspaces — A and B — so a "disagreement" insert
|
|
71
|
+
// names two values that both actually exist, not a real one versus
|
|
72
|
+
// garbage (which the format CHECK, not the equality CHECK, would catch).
|
|
73
|
+
const org = (
|
|
74
|
+
await db.query<{ id: string }>(
|
|
75
|
+
`INSERT INTO "organizations" ("name", "slug", "updated_at")
|
|
76
|
+
VALUES ($1, $2, now()) RETURNING "id"`,
|
|
77
|
+
[`6254 Org ${RUN}`, `6254-org-${RUN}`],
|
|
78
|
+
)
|
|
79
|
+
).rows[0]!;
|
|
80
|
+
|
|
81
|
+
workspaceAId = (
|
|
82
|
+
await db.query<{ id: string }>(
|
|
83
|
+
`INSERT INTO "workspaces" ("organization_id", "name", "slug", "updated_at")
|
|
84
|
+
VALUES ($1, $2, $3, now()) RETURNING "id"`,
|
|
85
|
+
[org.id, `6254 Workspace A ${RUN}`, `6254-ws-a-${RUN}`],
|
|
86
|
+
)
|
|
87
|
+
).rows[0]!.id;
|
|
88
|
+
|
|
89
|
+
workspaceBId = (
|
|
90
|
+
await db.query<{ id: string }>(
|
|
91
|
+
`INSERT INTO "workspaces" ("organization_id", "name", "slug", "updated_at")
|
|
92
|
+
VALUES ($1, $2, $3, now()) RETURNING "id"`,
|
|
93
|
+
[org.id, `6254 Workspace B ${RUN}`, `6254-ws-b-${RUN}`],
|
|
94
|
+
)
|
|
95
|
+
).rows[0]!.id;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterAll(async () => {
|
|
99
|
+
await db.query(`DELETE FROM "flow_definitions" WHERE "slug" LIKE $1`, [
|
|
100
|
+
`6254-${RUN}-%`,
|
|
101
|
+
]);
|
|
102
|
+
await db.query(`DELETE FROM "leads" WHERE "source" = $1`, [
|
|
103
|
+
`6254-test-${RUN}`,
|
|
104
|
+
]);
|
|
105
|
+
await db.query(`DELETE FROM "workspaces" WHERE "slug" LIKE $1`, [
|
|
106
|
+
`6254-ws-%-${RUN}`,
|
|
107
|
+
]);
|
|
108
|
+
await db.query(`DELETE FROM "organizations" WHERE "slug" = $1`, [
|
|
109
|
+
`6254-org-${RUN}`,
|
|
110
|
+
]);
|
|
111
|
+
await db.end();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("AC-5 — the equality CHECK rejects a site<->workspace disagreement", () => {
|
|
115
|
+
it("Lead: workspace_id disagreeing with site_id raises 23514", async () => {
|
|
116
|
+
await expect(
|
|
117
|
+
db.query(
|
|
118
|
+
`INSERT INTO "leads" ("site_id", "workspace_id", "source", "email", "payload")
|
|
119
|
+
VALUES ($1, $2, $3, $4, '{}'::jsonb)`,
|
|
120
|
+
[workspaceAId, workspaceBId, `6254-test-${RUN}`, "disagree@example.com"],
|
|
121
|
+
),
|
|
122
|
+
).rejects.toMatchObject<{ code: PgError["code"] }>({ code: "23514" });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("Lead: workspace_id AGREEING with site_id succeeds", async () => {
|
|
126
|
+
const result = await db.query<{ id: string }>(
|
|
127
|
+
`INSERT INTO "leads" ("site_id", "workspace_id", "source", "email", "payload")
|
|
128
|
+
VALUES ($1, $1, $2, $3, '{}'::jsonb) RETURNING "id"`,
|
|
129
|
+
[workspaceAId, `6254-test-${RUN}`, "agree@example.com"],
|
|
130
|
+
);
|
|
131
|
+
expect(result.rows).toHaveLength(1);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("FlowDefinition (the second retrofit model, AC-5's own requirement): workspace_id disagreeing with site_id raises 23514", async () => {
|
|
135
|
+
await expect(
|
|
136
|
+
db.query(
|
|
137
|
+
`INSERT INTO "flow_definitions"
|
|
138
|
+
("site_id", "workspace_id", "slug", "config", "title", "status", "updated_at")
|
|
139
|
+
VALUES ($1, $2, $3, '{}'::jsonb, 'Test', 'live', now())`,
|
|
140
|
+
[workspaceAId, workspaceBId, `6254-${RUN}-disagree`],
|
|
141
|
+
),
|
|
142
|
+
).rejects.toMatchObject<{ code: PgError["code"] }>({ code: "23514" });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("FlowDefinition: workspace_id AGREEING with site_id succeeds", async () => {
|
|
146
|
+
const result = await db.query<{ id: string }>(
|
|
147
|
+
`INSERT INTO "flow_definitions"
|
|
148
|
+
("site_id", "workspace_id", "slug", "config", "title", "status", "updated_at")
|
|
149
|
+
VALUES ($1, $1, $2, '{}'::jsonb, 'Test', 'live', now()) RETURNING "id"`,
|
|
150
|
+
[workspaceAId, `6254-${RUN}-agree`],
|
|
151
|
+
);
|
|
152
|
+
expect(result.rows).toHaveLength(1);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("AC-7 — NOT NULL: no row can exist with workspace_id IS NULL", () => {
|
|
157
|
+
it("Lead: omitting workspace_id raises 23502 (not_null_violation)", async () => {
|
|
158
|
+
await expect(
|
|
159
|
+
db.query(
|
|
160
|
+
`INSERT INTO "leads" ("site_id", "source", "email", "payload")
|
|
161
|
+
VALUES ($1, $2, $3, '{}'::jsonb)`,
|
|
162
|
+
[workspaceAId, `6254-test-${RUN}`, "null@example.com"],
|
|
163
|
+
),
|
|
164
|
+
).rejects.toMatchObject<{ code: PgError["code"] }>({ code: "23502" });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("FlowDefinition: omitting workspace_id raises 23502 (not_null_violation)", async () => {
|
|
168
|
+
await expect(
|
|
169
|
+
db.query(
|
|
170
|
+
`INSERT INTO "flow_definitions" ("site_id", "slug", "config", "title", "status", "updated_at")
|
|
171
|
+
VALUES ($1, $2, '{}'::jsonb, 'Test', 'live', now())`,
|
|
172
|
+
[workspaceAId, `6254-${RUN}-null`],
|
|
173
|
+
),
|
|
174
|
+
).rejects.toMatchObject<{ code: PgError["code"] }>({ code: "23502" });
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("AC-2 — WorkspaceSiteIdentity's composite unique exists at the database", () => {
|
|
179
|
+
it("pg_constraint reports a UNIQUE constraint over (id, workspace_id)", async () => {
|
|
180
|
+
const result = await db.query<{ contype: string; conkey_count: number }>(
|
|
181
|
+
`SELECT c.contype, array_length(c.conkey, 1) AS conkey_count
|
|
182
|
+
FROM pg_constraint c
|
|
183
|
+
JOIN pg_class t ON t.oid = c.conrelid
|
|
184
|
+
WHERE t.relname = 'workspace_site_identities'
|
|
185
|
+
AND c.conname = 'workspace_site_identities_id_workspace_id_key'`,
|
|
186
|
+
);
|
|
187
|
+
expect(result.rows).toHaveLength(1);
|
|
188
|
+
expect(result.rows[0]!.contype).toBe("u");
|
|
189
|
+
expect(result.rows[0]!.conkey_count).toBe(2);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("the migration's own literal sentinel id equals DEFAULT_WORKSPACE_ID", () => {
|
|
194
|
+
it("cannot drift apart — read from the shipped file, not a re-typed copy", () => {
|
|
195
|
+
// SQL cannot import database/tenancy.ts (spec § 5.3 step 5), so the
|
|
196
|
+
// migration carries the sentinel as a literal. Pin it against the
|
|
197
|
+
// exported constant by reading the shipped file, exactly as the
|
|
198
|
+
// #6155/#6203 precedents do.
|
|
199
|
+
const occurrences = MIGRATION_SQL.match(/'wksp_[0-9a-f-]+'/g) ?? [];
|
|
200
|
+
expect(occurrences.length).toBeGreaterThan(0);
|
|
201
|
+
for (const literal of occurrences) {
|
|
202
|
+
expect(literal).toBe(`'${DEFAULT_WORKSPACE_ID}'`);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|