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,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file project-listing-consent-at.db.test.ts
|
|
3
|
+
*
|
|
4
|
+
* LIVE-DATABASE proof that `ProjectListing.consentAt` round-trips through
|
|
5
|
+
* the real Prisma Client with timezone information preserved (#6171).
|
|
6
|
+
* Requires DATABASE_URL pointing at a Postgres with the migrations applied
|
|
7
|
+
* (`prisma migrate deploy`) — the whole suite SKIPS when DATABASE_URL is
|
|
8
|
+
* unset, keeping the workspace Jest run DB-free (the repo convention; see
|
|
9
|
+
* `prefixed-ids.db.test.ts` / `appointment.db.test.ts`).
|
|
10
|
+
*
|
|
11
|
+
* The bug this guards: `database/inbox/schema.prisma`'s `consentAt` field
|
|
12
|
+
* declared `DateTime @default(now()) @map("consent_at")` with no native-type
|
|
13
|
+
* annotation, so Prisma treated it as `TIMESTAMP(3)` (no time zone) by
|
|
14
|
+
* default, while its migration
|
|
15
|
+
* (`20260909180000_listing_draft_and_fields/migration.sql`) created the
|
|
16
|
+
* actual column as `TIMESTAMPTZ` — deliberately, since it records a legal
|
|
17
|
+
* consent timestamp. The fix adds `@db.Timestamptz(3)` so the schema's
|
|
18
|
+
* declared type agrees with what the column actually is.
|
|
19
|
+
*
|
|
20
|
+
* Spawns `project-listing-consent-probe.ts` via `tsx` (a real ESM process)
|
|
21
|
+
* rather than importing the generated client directly, for the same reason
|
|
22
|
+
* `appointment.db.test.ts` does: the generated client is ESM-only
|
|
23
|
+
* (`import.meta.url`) and cannot load under this package's CJS `ts-jest`
|
|
24
|
+
* transform.
|
|
25
|
+
*/
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
27
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
28
|
+
import { join } from "node:path";
|
|
29
|
+
|
|
30
|
+
const describeDb = process.env.DATABASE_URL ? describe : describe.skip;
|
|
31
|
+
|
|
32
|
+
const DATABASE_DIR = join(__dirname, "..");
|
|
33
|
+
const PROBE_SCRIPT = join(
|
|
34
|
+
__dirname,
|
|
35
|
+
"fixtures",
|
|
36
|
+
"project-listing-consent-probe.ts",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Runs with no DATABASE_URL required — a deterministic guard so the schema
|
|
40
|
+
// declaration can't silently regress even in a DB-free run (the live-DB
|
|
41
|
+
// round trip below still runs whenever DATABASE_URL is set).
|
|
42
|
+
describe("ProjectListing.consentAt — schema declares the real column type (#6171)", () => {
|
|
43
|
+
it("carries @db.Timestamptz(3), matching the TIMESTAMPTZ column created by 20260909180000_listing_draft_and_fields", () => {
|
|
44
|
+
const schema = readFileSync(
|
|
45
|
+
join(DATABASE_DIR, "inbox", "schema.prisma"),
|
|
46
|
+
"utf-8",
|
|
47
|
+
);
|
|
48
|
+
const fieldLine = schema
|
|
49
|
+
.split("\n")
|
|
50
|
+
.find((line) => /^\s*consentAt\s+DateTime\b/.test(line));
|
|
51
|
+
expect(fieldLine).toBeDefined();
|
|
52
|
+
expect(fieldLine).toMatch(/@db\.Timestamptz\(3\)/);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Walk up from DATABASE_DIR to find node_modules/.bin/tsx (worktree-safe —
|
|
57
|
+
* mirrors `appointment.db.test.ts`'s own resolution). */
|
|
58
|
+
function findTsxBin(): string {
|
|
59
|
+
let dir = DATABASE_DIR;
|
|
60
|
+
for (let i = 0; i < 5; i++) {
|
|
61
|
+
const candidate = join(dir, "node_modules", ".bin", "tsx");
|
|
62
|
+
if (existsSync(candidate)) return candidate;
|
|
63
|
+
dir = join(dir, "..");
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`tsx binary not found searching up from ${DATABASE_DIR}.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describeDb(
|
|
69
|
+
"ProjectListing.consentAt — live DB round trip via prisma.projectListing (#6171)",
|
|
70
|
+
() => {
|
|
71
|
+
it("preserves the exact instant through create() + a fresh findUniqueOrThrow(), regardless of session timezone", () => {
|
|
72
|
+
const tsxBin = findTsxBin();
|
|
73
|
+
const result = spawnSync(tsxBin, [PROBE_SCRIPT], {
|
|
74
|
+
cwd: DATABASE_DIR,
|
|
75
|
+
encoding: "utf-8",
|
|
76
|
+
env: process.env,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Exit status + a successful stdout parse is the full signal (see
|
|
80
|
+
// appointment.db.test.ts for why this, rather than empty stderr, is
|
|
81
|
+
// the right assertion — a stray Node/tsx diagnostic must not fail an
|
|
82
|
+
// otherwise-successful run).
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`probe exited ${result.status}: ${result.stderr || "(no stderr)"}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const probe = JSON.parse(result.stdout) as {
|
|
90
|
+
writtenIso: string;
|
|
91
|
+
createdConsentAtIso: string;
|
|
92
|
+
rereadConsentAtIso: string;
|
|
93
|
+
liveColumnDataType: string | null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// The instant `create()` returns must be exactly what was written —
|
|
97
|
+
// not shifted by the pinned Asia/Kolkata (+05:30) session timezone.
|
|
98
|
+
expect(probe.createdConsentAtIso).toBe(probe.writtenIso);
|
|
99
|
+
|
|
100
|
+
// A SECOND, independent read (a fresh findUniqueOrThrow, not the
|
|
101
|
+
// object create() already returned) must agree too — the read path,
|
|
102
|
+
// not just the write path, preserves the instant.
|
|
103
|
+
expect(probe.rereadConsentAtIso).toBe(probe.writtenIso);
|
|
104
|
+
|
|
105
|
+
// Ground truth: the live column really is timezone-aware, which is
|
|
106
|
+
// what makes the schema's `@db.Timestamptz(3)` annotation correct
|
|
107
|
+
// rather than merely quiet.
|
|
108
|
+
expect(probe.liveColumnDataType).toBe("timestamp with time zone");
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
);
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file tenancy.test.ts
|
|
3
|
+
* @description Unit coverage for `resolveDefaultWorkspaceRow` — the shared
|
|
4
|
+
* one-workspace deployment predicate (#5876 spec D2) — over a structural
|
|
5
|
+
* `WorkspacePredicateClient` stub. No `@prisma/client` import anywhere in
|
|
6
|
+
* this file or in `../tenancy.ts`; the four arms below are the same ones
|
|
7
|
+
* `apps/api/src/services/__tests__/public-site-resolver.test.ts` and
|
|
8
|
+
* `apps/api/src/middleware/__tests__/default-tenant.test.ts` already pin for
|
|
9
|
+
* the re-pointed caller — this suite is the predicate's OWN coverage.
|
|
10
|
+
*
|
|
11
|
+
* `resolveSentinelWorkspaceRow` (#6155 spec D2 rule 2) is the sibling
|
|
12
|
+
* sentinel-EXISTENCE predicate, covered below on the SAME three-workspace
|
|
13
|
+
* mock that `resolveDefaultWorkspaceRow` refuses — the pair is what proves
|
|
14
|
+
* the two questions ("is this deployment single-tenant" vs "does the
|
|
15
|
+
* sentinel workspace exist") stay separate rather than re-merging.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_ORG_ID,
|
|
20
|
+
DEFAULT_WORKSPACE_ID,
|
|
21
|
+
resolveDefaultWorkspaceRow,
|
|
22
|
+
resolveSentinelWorkspaceRow,
|
|
23
|
+
type WorkspacePredicateClient,
|
|
24
|
+
} from "../tenancy";
|
|
25
|
+
|
|
26
|
+
const NON_SENTINEL_ORG_ID = "org_01930000-0000-7bcd-8000-000000000002";
|
|
27
|
+
|
|
28
|
+
function makeStub(
|
|
29
|
+
count: number,
|
|
30
|
+
found: { organizationId: string } | null,
|
|
31
|
+
): WorkspacePredicateClient {
|
|
32
|
+
return {
|
|
33
|
+
workspace: {
|
|
34
|
+
count: jest.fn().mockResolvedValue(count),
|
|
35
|
+
findUnique: jest.fn().mockResolvedValue(found),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("resolveDefaultWorkspaceRow", () => {
|
|
41
|
+
it("returns null when zero workspaces exist (count() === 0)", async () => {
|
|
42
|
+
const stub = makeStub(0, null);
|
|
43
|
+
|
|
44
|
+
const result = await resolveDefaultWorkspaceRow(stub);
|
|
45
|
+
|
|
46
|
+
expect(result).toBeNull();
|
|
47
|
+
// The deployment is ambiguous at the count check alone — findUnique must
|
|
48
|
+
// never run.
|
|
49
|
+
expect(stub.workspace.findUnique).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns null when two or more workspaces exist (count() === 2)", async () => {
|
|
53
|
+
const stub = makeStub(2, null);
|
|
54
|
+
|
|
55
|
+
const result = await resolveDefaultWorkspaceRow(stub);
|
|
56
|
+
|
|
57
|
+
expect(result).toBeNull();
|
|
58
|
+
expect(stub.workspace.findUnique).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns null when the sole workspace hangs off a non-sentinel Organization", async () => {
|
|
62
|
+
const stub = makeStub(1, { organizationId: NON_SENTINEL_ORG_ID });
|
|
63
|
+
|
|
64
|
+
const result = await resolveDefaultWorkspaceRow(stub);
|
|
65
|
+
|
|
66
|
+
expect(result).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns null when the sole workspace is not found at the default id", async () => {
|
|
70
|
+
const stub = makeStub(1, null);
|
|
71
|
+
|
|
72
|
+
const result = await resolveDefaultWorkspaceRow(stub);
|
|
73
|
+
|
|
74
|
+
expect(result).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns the row when the sole workspace is the well-known default hanging off the sentinel Organization", async () => {
|
|
78
|
+
const stub = makeStub(1, { organizationId: DEFAULT_ORG_ID });
|
|
79
|
+
|
|
80
|
+
const result = await resolveDefaultWorkspaceRow(stub);
|
|
81
|
+
|
|
82
|
+
expect(result).toEqual({
|
|
83
|
+
id: DEFAULT_WORKSPACE_ID,
|
|
84
|
+
organizationId: DEFAULT_ORG_ID,
|
|
85
|
+
});
|
|
86
|
+
expect(stub.workspace.findUnique).toHaveBeenCalledWith({
|
|
87
|
+
where: { id: DEFAULT_WORKSPACE_ID },
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns null on a THREE-workspace deployment even though the sentinel workspace exists — the count gate refuses it before findUnique ever runs", async () => {
|
|
92
|
+
// A correctly-configured multi-workspace deployment: the sentinel is one
|
|
93
|
+
// of three, but resolveDefaultWorkspaceRow answers "is this deployment
|
|
94
|
+
// unambiguously single-tenant", not "does the sentinel exist" — #6155
|
|
95
|
+
// spec D2/R1, the exact case resolveSentinelWorkspaceRow exists for.
|
|
96
|
+
const stub = makeStub(3, { organizationId: DEFAULT_ORG_ID });
|
|
97
|
+
|
|
98
|
+
const result = await resolveDefaultWorkspaceRow(stub);
|
|
99
|
+
|
|
100
|
+
expect(result).toBeNull();
|
|
101
|
+
expect(stub.workspace.findUnique).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("resolveSentinelWorkspaceRow", () => {
|
|
106
|
+
it("resolves the sentinel workspace on a THREE-workspace deployment — no count() gate", async () => {
|
|
107
|
+
// The SAME three-workspace mock resolveDefaultWorkspaceRow refuses above
|
|
108
|
+
// — this is the pair that keeps the two questions separate (#6155 D2
|
|
109
|
+
// rule 2): resolveDefaultWorkspaceRow answers "is this deployment
|
|
110
|
+
// single-tenant" (no); resolveSentinelWorkspaceRow answers "does the
|
|
111
|
+
// seeded sentinel workspace exist" (yes), and it never calls count().
|
|
112
|
+
const stub = makeStub(3, { organizationId: DEFAULT_ORG_ID });
|
|
113
|
+
|
|
114
|
+
const result = await resolveSentinelWorkspaceRow(stub);
|
|
115
|
+
|
|
116
|
+
expect(result).toEqual({
|
|
117
|
+
id: DEFAULT_WORKSPACE_ID,
|
|
118
|
+
organizationId: DEFAULT_ORG_ID,
|
|
119
|
+
});
|
|
120
|
+
expect(stub.workspace.count).not.toHaveBeenCalled();
|
|
121
|
+
expect(stub.workspace.findUnique).toHaveBeenCalledWith({
|
|
122
|
+
where: { id: DEFAULT_WORKSPACE_ID },
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("returns null when no workspace carries the sentinel id", async () => {
|
|
127
|
+
const stub = makeStub(3, null);
|
|
128
|
+
|
|
129
|
+
const result = await resolveSentinelWorkspaceRow(stub);
|
|
130
|
+
|
|
131
|
+
expect(result).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns null when the id resolves but hangs off a non-sentinel Organization", async () => {
|
|
135
|
+
const stub = makeStub(3, { organizationId: NON_SENTINEL_ORG_ID });
|
|
136
|
+
|
|
137
|
+
const result = await resolveSentinelWorkspaceRow(stub);
|
|
138
|
+
|
|
139
|
+
expect(result).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -9,17 +9,45 @@
|
|
|
9
9
|
// invariant #1). Cross-namespace integrity + cascade are the service layer's
|
|
10
10
|
// (ADR-0009 D3), delivered by the Phase 4 workspace-scoped write surface. Every
|
|
11
11
|
// model here is fenced by the architecture.yaml tenant-scope forbidden-pattern.
|
|
12
|
+
//
|
|
13
|
+
// SiteSetting and SiteListingConfig also carry the #6374 tenancy fence. The
|
|
14
|
+
// placement rule stated in database/inbox/schema.prisma's header applies
|
|
15
|
+
// here verbatim: `workspaceId` goes on the line immediately after `siteId`
|
|
16
|
+
// (immediately after `id` where there is no `siteId`), and no comment
|
|
17
|
+
// written for it contains a closing curly, because the drift guard's
|
|
18
|
+
// model-body collector stops at the first one.
|
|
12
19
|
|
|
13
20
|
/// SiteSetting - Key-value settings for the customer site
|
|
14
21
|
/// Use for: feature flags, custom config, cached Platform data
|
|
15
22
|
model SiteSetting {
|
|
16
23
|
id String @id @default(dbgenerated("('siteset_'::text || (uuidv7())::text)")) @db.Text
|
|
17
|
-
|
|
24
|
+
// #6374 D3 — the tenancy fence, in the shape this model's ABSENT site_id
|
|
25
|
+
// forces (operator disposition, 2026-09-20). This table is keyed BY the
|
|
26
|
+
// workspace: `key` loses its global @unique and the pair becomes unique
|
|
27
|
+
// instead, so two sellers may each hold a `feature_flags` row. Existing
|
|
28
|
+
// rows backfill to the SENTINEL workspace — a pre-fence row belonged to
|
|
29
|
+
// "the site", which is exactly what the alias `default` resolves to.
|
|
30
|
+
//
|
|
31
|
+
// The `key` TEXT itself does NOT change. `dunning-policy.ts` builds
|
|
32
|
+
// `commerce.dunning_policy:` + siteId from its UNRESOLVED siteId
|
|
33
|
+
// parameter, because this table never had a site_id column to scope by;
|
|
34
|
+
// the backfill leaves every key verbatim, so the read keeps building the
|
|
35
|
+
// key the same way and adds workspaceId from the resolver. Rebuilding the
|
|
36
|
+
// key from the RESOLVED id would match no row and silently hand every
|
|
37
|
+
// seller the built-in default policy.
|
|
38
|
+
//
|
|
39
|
+
// DO NOT MOVE this field — see the fence-fan-out note in
|
|
40
|
+
// database/inbox/schema.prisma's header: placement immediately after id is
|
|
41
|
+
// load-bearing, not cosmetic.
|
|
42
|
+
workspaceId String @map("workspace_id")
|
|
43
|
+
key String // "feature_flags", "cached_config", "custom_theme"
|
|
18
44
|
value Json
|
|
19
45
|
|
|
20
46
|
createdAt DateTime @default(now()) @map("created_at")
|
|
21
47
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
22
48
|
|
|
49
|
+
@@unique([workspaceId, key])
|
|
50
|
+
@@index([workspaceId])
|
|
23
51
|
@@map("site_settings")
|
|
24
52
|
}
|
|
25
53
|
|
|
@@ -50,7 +78,22 @@ model WorkspaceTheme {
|
|
|
50
78
|
@@map("workspace_themes")
|
|
51
79
|
}
|
|
52
80
|
|
|
53
|
-
/// WorkspaceSiteIdentity — per-workspace SiteIdentitySchema blob (#3641)
|
|
81
|
+
/// WorkspaceSiteIdentity — per-workspace SiteIdentitySchema blob (#3641).
|
|
82
|
+
///
|
|
83
|
+
/// #6254 AC-2 — carries `@@unique([id, workspaceId])`, a named Postgres
|
|
84
|
+
/// unique constraint over a pair already unique today (`id` is the PK, so
|
|
85
|
+
/// the pair is trivially unique too) — redundant now, load-bearing the
|
|
86
|
+
/// moment `workspaceId` ever drops its own `@unique` above (§2.3 of
|
|
87
|
+
/// docs/specs/6254-the-composite-fk-pattern-workspacesiteidentity-spec.md).
|
|
88
|
+
/// It is NOT the target of a Prisma `@relation` from the inbox-namespace
|
|
89
|
+
/// fenced models (Lead, ProjectListing, FlowDefinition, FlowSession):
|
|
90
|
+
/// ADR-0009 invariant #1 forbids a cross-namespace relation (this model is
|
|
91
|
+
/// `config`, they are `inbox`), and `site_id` never holds this model's
|
|
92
|
+
/// `siteid_<uuidv7>` id — it holds the WORKSPACE id, so a real FK would be
|
|
93
|
+
/// unsatisfiable for every existing row. Those four models enforce
|
|
94
|
+
/// site<->workspace agreement instead via a same-table equality CHECK
|
|
95
|
+
/// (`<table>_workspace_site_agree`) — see database/inbox/schema.prisma and
|
|
96
|
+
/// docs/database/namespaces.md.
|
|
54
97
|
model WorkspaceSiteIdentity {
|
|
55
98
|
id String @id @default(dbgenerated("('siteid_'::text || (uuidv7())::text)")) @db.Text
|
|
56
99
|
workspaceId String @unique @map("workspace_id")
|
|
@@ -58,6 +101,7 @@ model WorkspaceSiteIdentity {
|
|
|
58
101
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
59
102
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
60
103
|
|
|
104
|
+
@@unique([id, workspaceId])
|
|
61
105
|
@@map("workspace_site_identities")
|
|
62
106
|
}
|
|
63
107
|
|
|
@@ -72,6 +116,26 @@ model WorkspaceSEO {
|
|
|
72
116
|
@@map("workspace_seos")
|
|
73
117
|
}
|
|
74
118
|
|
|
119
|
+
/// WorkspaceMeetings — per-workspace MeetingsSchema blob: the operator's
|
|
120
|
+
/// enabled-slug gate for meeting types (#6183 S9b, D1). Structurally
|
|
121
|
+
/// identical to its three siblings above — same `@unique` key, same
|
|
122
|
+
/// Json-column convention, validation at the API layer. The field-level
|
|
123
|
+
/// `@map`s are load-bearing (not merely style, unlike the siblings' own
|
|
124
|
+
/// comment implies): `prisma validate` accepts a model without them, and
|
|
125
|
+
/// only `pnpm check:prisma-migration-drift` catches the resulting
|
|
126
|
+
/// camelCase/snake_case mismatch against a hand-written migration — the
|
|
127
|
+
/// exact class that shipped a P2022 on every `Appointment.noShow` write
|
|
128
|
+
/// (#6162). Keep every `@map` in lockstep with the migration's column names.
|
|
129
|
+
model WorkspaceMeetings {
|
|
130
|
+
id String @id @default(dbgenerated("('meet_'::text || (uuidv7())::text)")) @db.Text
|
|
131
|
+
workspaceId String @unique @map("workspace_id")
|
|
132
|
+
data Json
|
|
133
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
134
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
135
|
+
|
|
136
|
+
@@map("workspace_meetings")
|
|
137
|
+
}
|
|
138
|
+
|
|
75
139
|
/// SiteListingConfig — per-SITE project-listings config blob (#5033).
|
|
76
140
|
///
|
|
77
141
|
/// The fourth site-config model, and the one keyed on `siteId` rather than
|
|
@@ -90,6 +154,18 @@ model WorkspaceSEO {
|
|
|
90
154
|
model SiteListingConfig {
|
|
91
155
|
id String @id @default(dbgenerated("('sltcfg_'::text || (uuidv7())::text)")) @db.Text
|
|
92
156
|
siteId String @unique @map("site_id")
|
|
157
|
+
// #6374 — the tenancy fence, extended to this model (spec
|
|
158
|
+
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
159
|
+
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
160
|
+
// site_listing_configs_workspace_site_agree CHECK (workspace_id = site_id)
|
|
161
|
+
// — since siteId IS the resolved workspace id, Postgres itself rejects a
|
|
162
|
+
// row whose two columns disagree. Operator-seeded config: there is no
|
|
163
|
+
// create/upsert of this model in the tree, and its one read
|
|
164
|
+
// (apps/api/src/services/listing-config.ts) already resolves the alias.
|
|
165
|
+
// DO NOT MOVE this field — see the fence-fan-out note in
|
|
166
|
+
// database/inbox/schema.prisma's header: placement immediately after
|
|
167
|
+
// siteId is load-bearing, not cosmetic.
|
|
168
|
+
workspaceId String @map("workspace_id")
|
|
93
169
|
|
|
94
170
|
/// The site's PUBLIC site key (#5048 D2) — the addressing token an anonymous
|
|
95
171
|
/// application carries in place of a siteId, so the server resolves the site
|
|
@@ -106,6 +182,7 @@ model SiteListingConfig {
|
|
|
106
182
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
107
183
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
108
184
|
|
|
185
|
+
@@index([workspaceId])
|
|
109
186
|
@@map("site_listing_configs")
|
|
110
187
|
}
|
|
111
188
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// content namespace — chat + artifact substrate (AgentSession, ConversationEvent,
|
|
2
|
-
// SessionReadReceipt, Artifact, ArtifactVersion)
|
|
2
|
+
// SessionReadReceipt, Artifact, ArtifactVersion, Asset)
|
|
3
3
|
//
|
|
4
4
|
// Backs the event-sourced conversation transport + artifact persistence in
|
|
5
5
|
// docs/research/ploy-capture/BACKEND-REQUIREMENTS.md (§1 transport R-T1..R-T7,
|
|
@@ -75,21 +75,50 @@ model SessionReadReceipt {
|
|
|
75
75
|
@@map("session_read_receipts")
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/// Artifact — a
|
|
78
|
+
/// Artifact — a piece of the WORKSPACE's versioned content (#3846, generalized
|
|
79
|
+
/// by #5800 / roadmap D1). Originally "a session's produced output"; a session
|
|
80
|
+
/// is now PROVENANCE, not ownership — `sessionId` is optional and `SetNull`, so
|
|
81
|
+
/// deleting the conversation that produced a page leaves the page standing.
|
|
82
|
+
///
|
|
83
|
+
/// `contentType` is the discriminator the content store reads: `text/page` is
|
|
84
|
+
/// what the editor writes, `text/document` the legacy chat artifact. A page's
|
|
85
|
+
/// public identity is `(workspaceId, contentType, locale, slug)`; `slug` stays
|
|
86
|
+
/// NULL until publish, and NULLs never collide in a Postgres unique index, so a
|
|
87
|
+
/// workspace may hold any number of unpublished drafts.
|
|
88
|
+
///
|
|
89
|
+
/// `publishedVersionId` is a plain pointer at the ArtifactVersion the public
|
|
90
|
+
/// read serves — deliberately NOT a relation (a FK back into `versions` would
|
|
91
|
+
/// make the delete order of an artifact's own history circular), and
|
|
92
|
+
/// deliberately NOT `ArtifactVersion.isCurrent`, which is the DRAFT TIP and is
|
|
93
|
+
/// never the publish mechanism. Saving a draft moves `isCurrent`; only publish
|
|
94
|
+
/// moves this pointer.
|
|
95
|
+
///
|
|
96
|
+
/// `type` is the LEGACY discriminator. It predates `contentType` and is still
|
|
97
|
+
/// read by apps/api/src/routes/workspaces/sessions.ts (the session-artifacts
|
|
98
|
+
/// route, a hard-floor surface), so #5800 added `contentType` beside it and
|
|
99
|
+
/// backfilled it from `type` rather than renaming the column out from under a
|
|
100
|
+
/// live consumer. New code reads `contentType`; retiring `type` belongs to the
|
|
101
|
+
/// card that moves that route.
|
|
79
102
|
model Artifact {
|
|
80
|
-
id
|
|
81
|
-
sessionId
|
|
82
|
-
workspaceId
|
|
83
|
-
type
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
103
|
+
id String @id @default(dbgenerated("('artif_'::text || (uuidv7())::text)")) @db.Text
|
|
104
|
+
sessionId String? @map("session_id")
|
|
105
|
+
workspaceId String @map("workspace_id")
|
|
106
|
+
type String @default("text/document")
|
|
107
|
+
contentType String @default("text/document") @map("content_type")
|
|
108
|
+
locale String @default("en")
|
|
109
|
+
slug String?
|
|
110
|
+
title String
|
|
111
|
+
publishedVersionId String? @map("published_version_id")
|
|
112
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
113
|
+
updatedAt DateTime @updatedAt @map("updated_at")
|
|
114
|
+
|
|
115
|
+
session AgentSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull)
|
|
89
116
|
versions ArtifactVersion[]
|
|
90
117
|
|
|
118
|
+
@@unique([workspaceId, contentType, locale, slug])
|
|
91
119
|
@@index([sessionId])
|
|
92
120
|
@@index([workspaceId])
|
|
121
|
+
@@index([workspaceId, contentType, updatedAt])
|
|
93
122
|
@@map("artifacts")
|
|
94
123
|
}
|
|
95
124
|
|
|
@@ -110,6 +139,53 @@ model ArtifactVersion {
|
|
|
110
139
|
@@map("artifact_versions")
|
|
111
140
|
}
|
|
112
141
|
|
|
142
|
+
/// Asset — a workspace's uploaded binary (#5800 / roadmap D7), following the
|
|
143
|
+
/// `DeliverableAsset` precedent in the inbox namespace: the bytes live in the
|
|
144
|
+
/// row, not in object storage. That precedent was deliberate — no new
|
|
145
|
+
/// infrastructure — and only the resolver changes if blob storage arrives later.
|
|
146
|
+
///
|
|
147
|
+
/// `workspaceId` is a plain String stable id with no @relation (ADR 0009
|
|
148
|
+
/// invariant 1), and `(workspaceId, key)` is the addressable identity. Ids are
|
|
149
|
+
/// UUIDv7 (ADR 0031), so a re-upload is a NEW id and a served asset can be
|
|
150
|
+
/// cached immutably. The upload route itself is #5802's scope, not this model's.
|
|
151
|
+
///
|
|
152
|
+
/// Since #6310: this is also the store a magnet's `deliverables.assets[].assetKey`
|
|
153
|
+
/// resolves against FIRST (by `(workspaceId, key)`), ahead of the legacy
|
|
154
|
+
/// `DeliverableAsset` fallback (database/inbox/schema.prisma) — read through
|
|
155
|
+
/// the scoped boundary in apps/api/src/lib/deliverable-asset-store.ts, never
|
|
156
|
+
/// a raw query.
|
|
157
|
+
model Asset {
|
|
158
|
+
id String @id @default(dbgenerated("('asset_'::text || (uuidv7())::text)")) @db.Text
|
|
159
|
+
workspaceId String @map("workspace_id")
|
|
160
|
+
/// The caller-chosen name an artifact's content references the asset by.
|
|
161
|
+
key String
|
|
162
|
+
/// Download filename (Content-Disposition). Stored RAW; sanitize at SERVE
|
|
163
|
+
/// time, exactly as the DeliverableAsset download route does.
|
|
164
|
+
filename String
|
|
165
|
+
contentType String @map("content_type")
|
|
166
|
+
bytes Bytes
|
|
167
|
+
size Int
|
|
168
|
+
/// Owner-authored display name, shown above the filename in the lightbox
|
|
169
|
+
/// (#6092, S4/D10). NULL means the owner has not written one — distinct from
|
|
170
|
+
/// an empty string, which means they cleared it. Never backfilled from
|
|
171
|
+
/// `filename`: the two are different facts (demo.html:782 renders both).
|
|
172
|
+
title String?
|
|
173
|
+
/// Owner-authored longer description. Same NULL-vs-empty contract as `title`.
|
|
174
|
+
description String?
|
|
175
|
+
/// Owner-controlled tag SLUGS (`ad-creative`, `workspace-memory`, …), never
|
|
176
|
+
/// the human labels — the label is presentation, resolved by the surface from
|
|
177
|
+
/// its own vocabulary (demo.html:358 `ATAGS`, :794 the Docs set). Free-form
|
|
178
|
+
/// by design: the asset vocabulary and the document vocabulary (D8/D9) ride
|
|
179
|
+
/// this one column, and the "fixed set" of D10 is a UI affordance, not a
|
|
180
|
+
/// database constraint (#6092, D2 of the spec).
|
|
181
|
+
tags String[] @default([])
|
|
182
|
+
createdAt DateTime @default(now()) @map("created_at")
|
|
183
|
+
|
|
184
|
+
@@unique([workspaceId, key])
|
|
185
|
+
@@index([workspaceId])
|
|
186
|
+
@@map("assets")
|
|
187
|
+
}
|
|
188
|
+
|
|
113
189
|
// ============================================================================
|
|
114
190
|
// Usage-billing metering (FEAT-047 P1.S1, #3709 — Epic #3704)
|
|
115
191
|
//
|