create-ailk 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- 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/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/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -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/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/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/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__/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/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- 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 +61 -0
- 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 +142 -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 +1 -1
- 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/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
|
@@ -33,12 +33,15 @@ import { prisma } from "../../lib/prisma.js";
|
|
|
33
33
|
import type { Id } from "@working-theory/ids";
|
|
34
34
|
|
|
35
35
|
import { defaultTenantPreHandlerPlugin } from "../default-tenant.js";
|
|
36
|
+
import { resolveDefaultTenant } from "../../services/default-tenant.js";
|
|
36
37
|
|
|
37
38
|
// The reserved-range sentinel values from database/tenancy.ts (ADR-0031
|
|
38
39
|
// rule 7) — re-declared here as literals so the test pins the WIRE values,
|
|
39
40
|
// not whatever the constants happen to be.
|
|
40
41
|
const DEFAULT_ORG_ID = "org_00000000-0000-7000-8000-000000000001";
|
|
41
42
|
const DEFAULT_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
|
|
43
|
+
const OPERATOR_MEMBERSHIP_ID =
|
|
44
|
+
"member_00000000-0000-7000-8000-000000000001";
|
|
42
45
|
|
|
43
46
|
/** A workspace id belonging to somebody else — never this caller's. */
|
|
44
47
|
const FOREIGN_WORKSPACE_ID = "wksp_01936b2c-0000-7000-8000-00000000beef";
|
|
@@ -103,6 +106,12 @@ function buildApp(
|
|
|
103
106
|
app.get("/workspaces/:workspaceId/config", async (req) => ({
|
|
104
107
|
workspaceId: req.tenant?.workspaceId ?? null,
|
|
105
108
|
}));
|
|
109
|
+
// The `workspace` module's settings-route GET shape (#5371) — the surface
|
|
110
|
+
// #5515 item 1 asks about. Stubbed rather than imported: what is under test
|
|
111
|
+
// is which tenant the GUARD yields to it, not what the handler then reads.
|
|
112
|
+
app.get("/workspaces/:workspaceId/general", async (req) => ({
|
|
113
|
+
workspaceId: req.tenant?.workspaceId ?? null,
|
|
114
|
+
}));
|
|
106
115
|
return app;
|
|
107
116
|
}
|
|
108
117
|
|
|
@@ -120,6 +129,75 @@ afterAll(() => {
|
|
|
120
129
|
process.env = ORIGINAL_ENV;
|
|
121
130
|
});
|
|
122
131
|
|
|
132
|
+
/**
|
|
133
|
+
* #5515 item 1 — `AILK_OPERATOR_EMAIL` is REQUIRED for a workspace-module
|
|
134
|
+
* deployment, and that requirement is what keeps the TOFU operator bind from
|
|
135
|
+
* firing on a settings-route GET.
|
|
136
|
+
*
|
|
137
|
+
* The #5513 review raised this against `apps/api/src/services/tenant-context.ts`
|
|
138
|
+
* before #5677 moved the resolution: registering the `workspace` module's
|
|
139
|
+
* routes under `isModuleEnabled("workspace") || isModuleEnabled("multi-tenant")`
|
|
140
|
+
* put a `tenantPreHandler` route on a build that has no multi-tenant module,
|
|
141
|
+
* so the concern was that the first authenticated account to load the module's
|
|
142
|
+
* Admin pages (`/admin`, renamed from `/workspace` by #5637) would TOFU-bind
|
|
143
|
+
* itself as org owner off a plain settings GET.
|
|
144
|
+
*
|
|
145
|
+
* On a workspace-only build `server.ts`'s `tenantPreHandlerForBuild()` picks
|
|
146
|
+
* THIS guard, which calls `resolveDefaultTenantForOperator` — so the pin is
|
|
147
|
+
* mandatory and unset fails CLOSED. These two cases are the objective record
|
|
148
|
+
* of that, pinned to the settings-route shape rather than the review surface:
|
|
149
|
+
* with the pin unset the bind never runs and the route is 403 to everyone;
|
|
150
|
+
* with it set, only the pinned operator is seated. The documentation half of
|
|
151
|
+
* this item is `.env.example` and `apps/api/CLAUDE.md`. Redesigning #3631's
|
|
152
|
+
* TOFU was explicitly out of scope.
|
|
153
|
+
*/
|
|
154
|
+
describe("workspace-module settings route — the operator pin is required (#5515 item 1)", () => {
|
|
155
|
+
const settingsPath = `/workspaces/${DEFAULT_WORKSPACE_ID}/general`;
|
|
156
|
+
|
|
157
|
+
it("DENIES a settings-route GET when AILK_OPERATOR_EMAIL is UNSET — and the TOFU bind never fires", async () => {
|
|
158
|
+
delete process.env.AILK_OPERATOR_EMAIL;
|
|
159
|
+
mockSingleTenantWorkspace();
|
|
160
|
+
mockBoundOperator();
|
|
161
|
+
|
|
162
|
+
const app = buildApp();
|
|
163
|
+
const res = await app.inject({ method: "GET", url: settingsPath });
|
|
164
|
+
|
|
165
|
+
expect(res.statusCode).toBe(403);
|
|
166
|
+
// The load-bearing assertion: no Membership is minted off a settings GET.
|
|
167
|
+
expect(prisma.membership.create).not.toHaveBeenCalled();
|
|
168
|
+
await app.close();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("GRANTS the same settings-route GET once AILK_OPERATOR_EMAIL pins the caller", async () => {
|
|
172
|
+
process.env.AILK_OPERATOR_EMAIL = OPERATOR_EMAIL;
|
|
173
|
+
mockSingleTenantWorkspace();
|
|
174
|
+
mockBoundOperator();
|
|
175
|
+
|
|
176
|
+
const app = buildApp();
|
|
177
|
+
const res = await app.inject({ method: "GET", url: settingsPath });
|
|
178
|
+
|
|
179
|
+
expect(res.statusCode).toBe(200);
|
|
180
|
+
expect(res.json()).toEqual({ workspaceId: DEFAULT_WORKSPACE_ID });
|
|
181
|
+
await app.close();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("DENIES a settings-route GET from a caller who is NOT the pinned operator, minting nothing", async () => {
|
|
185
|
+
process.env.AILK_OPERATOR_EMAIL = OPERATOR_EMAIL;
|
|
186
|
+
mockSingleTenantWorkspace();
|
|
187
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
|
188
|
+
email: "first-visitor@example.com",
|
|
189
|
+
emailVerified: true,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const app = buildApp();
|
|
193
|
+
const res = await app.inject({ method: "GET", url: settingsPath });
|
|
194
|
+
|
|
195
|
+
expect(res.statusCode).toBe(403);
|
|
196
|
+
expect(prisma.membership.create).not.toHaveBeenCalled();
|
|
197
|
+
await app.close();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
123
201
|
describe("default tenant guard — isolation (#5677)", () => {
|
|
124
202
|
it("DENIES a foreign workspaceId with 403, before any lookup of the target", async () => {
|
|
125
203
|
mockSingleTenantWorkspace();
|
|
@@ -337,3 +415,107 @@ describe("default tenant guard — isolation (#5677)", () => {
|
|
|
337
415
|
await app.close();
|
|
338
416
|
});
|
|
339
417
|
});
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The MULTI-TENANT reach into the same bind (#5701 AC-2). `resolveActiveTenant`
|
|
421
|
+
* (`services/tenant-context.ts:89`) calls `resolveDefaultTenant` directly, so
|
|
422
|
+
* `bindOperatorMembership` is reached WITHOUT the verified-email gate that
|
|
423
|
+
* `resolveDefaultTenantForOperator` — the base-build entry the guard above
|
|
424
|
+
* exercises — applies first. These cases drive that entry point directly,
|
|
425
|
+
* which is the only way to observe the bind's own check; through the plugin
|
|
426
|
+
* the wrapper would refuse first and the assertion would prove nothing.
|
|
427
|
+
*/
|
|
428
|
+
describe("resolveDefaultTenant — the operator bind's own verified-email check (#5701)", () => {
|
|
429
|
+
it("does NOT create the Membership when the pinned address matches but emailVerified is false", async () => {
|
|
430
|
+
mockSingleTenantWorkspace();
|
|
431
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
|
432
|
+
id: USER_ID,
|
|
433
|
+
email: OPERATOR_EMAIL,
|
|
434
|
+
emailVerified: false,
|
|
435
|
+
});
|
|
436
|
+
(prisma.membership.findUnique as jest.Mock).mockResolvedValue(null);
|
|
437
|
+
|
|
438
|
+
const result = await resolveDefaultTenant(USER_ID);
|
|
439
|
+
|
|
440
|
+
expect(result).toBeNull();
|
|
441
|
+
// The bind is the escalation: an unverified address is an unproven claim
|
|
442
|
+
// to the pin, so no `owner` Membership may be minted from it.
|
|
443
|
+
expect(prisma.membership.create).not.toHaveBeenCalled();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("does NOT create the Membership when the pinned caller has no User row", async () => {
|
|
447
|
+
mockSingleTenantWorkspace();
|
|
448
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue(null);
|
|
449
|
+
(prisma.membership.findUnique as jest.Mock).mockResolvedValue(null);
|
|
450
|
+
|
|
451
|
+
const result = await resolveDefaultTenant(USER_ID);
|
|
452
|
+
|
|
453
|
+
expect(result).toBeNull();
|
|
454
|
+
expect(prisma.membership.create).not.toHaveBeenCalled();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("CREATES the Membership when the pinned address matches and emailVerified is true — unchanged behaviour", async () => {
|
|
458
|
+
mockSingleTenantWorkspace();
|
|
459
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
|
460
|
+
id: USER_ID,
|
|
461
|
+
email: OPERATOR_EMAIL,
|
|
462
|
+
emailVerified: true,
|
|
463
|
+
});
|
|
464
|
+
(prisma.membership.create as jest.Mock).mockResolvedValue({});
|
|
465
|
+
(prisma.membership.findUnique as jest.Mock).mockResolvedValue({
|
|
466
|
+
role: "owner",
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const result = await resolveDefaultTenant(USER_ID);
|
|
470
|
+
|
|
471
|
+
expect(prisma.membership.create).toHaveBeenCalledTimes(1);
|
|
472
|
+
expect((prisma.membership.create as jest.Mock).mock.calls[0]?.[0]).toEqual({
|
|
473
|
+
data: {
|
|
474
|
+
id: OPERATOR_MEMBERSHIP_ID,
|
|
475
|
+
organizationId: DEFAULT_ORG_ID,
|
|
476
|
+
userId: USER_ID,
|
|
477
|
+
role: "owner",
|
|
478
|
+
},
|
|
479
|
+
});
|
|
480
|
+
expect(result).toEqual({
|
|
481
|
+
userId: USER_ID,
|
|
482
|
+
organizationId: DEFAULT_ORG_ID,
|
|
483
|
+
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
484
|
+
role: "owner",
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("leaves the UNPINNED branch untouched — no User read, pure TOFU bind (#5701 D4)", async () => {
|
|
489
|
+
delete process.env.AILK_OPERATOR_EMAIL;
|
|
490
|
+
mockSingleTenantWorkspace();
|
|
491
|
+
(prisma.membership.create as jest.Mock).mockResolvedValue({});
|
|
492
|
+
(prisma.membership.findUnique as jest.Mock).mockResolvedValue({
|
|
493
|
+
role: "owner",
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const result = await resolveDefaultTenant(USER_ID);
|
|
497
|
+
|
|
498
|
+
// No pin means no email comparison at all — the bind stays first-caller-wins.
|
|
499
|
+
expect(prisma.user.findUnique).not.toHaveBeenCalled();
|
|
500
|
+
expect(prisma.membership.create).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(result).toEqual({
|
|
502
|
+
userId: USER_ID,
|
|
503
|
+
organizationId: DEFAULT_ORG_ID,
|
|
504
|
+
workspaceId: DEFAULT_WORKSPACE_ID,
|
|
505
|
+
role: "owner",
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("still swallows the OPERATOR_MEMBERSHIP_ID unique violation — the TOFU race is unchanged", async () => {
|
|
510
|
+
mockSingleTenantWorkspace();
|
|
511
|
+
(prisma.user.findUnique as jest.Mock).mockResolvedValue({
|
|
512
|
+
id: USER_ID,
|
|
513
|
+
email: OPERATOR_EMAIL,
|
|
514
|
+
emailVerified: true,
|
|
515
|
+
});
|
|
516
|
+
(prisma.membership.create as jest.Mock).mockRejectedValue({ code: "P2002" });
|
|
517
|
+
(prisma.membership.findUnique as jest.Mock).mockResolvedValue(null);
|
|
518
|
+
|
|
519
|
+
await expect(resolveDefaultTenant(USER_ID)).resolves.toBeNull();
|
|
520
|
+
});
|
|
521
|
+
});
|
|
@@ -485,3 +485,45 @@ describe("cross-isolate limit — does NOT hold by design (#5040, SINGLE-REPLICA
|
|
|
485
485
|
}
|
|
486
486
|
});
|
|
487
487
|
});
|
|
488
|
+
|
|
489
|
+
// #6019 AC-2 — the `pathOnly` split (#5970 security review, finding 1): a
|
|
490
|
+
// route may carry its credential in the query string
|
|
491
|
+
// (`/v1/requests/:id/confirm?token=…` does), and a 429 is a line an attacker
|
|
492
|
+
// can provoke at will by tripping the limiter — logging the raw target would
|
|
493
|
+
// let them farm credentials out of the operator's own log that way.
|
|
494
|
+
describe("rate-limit warn log — no query-string credential (#5970)", () => {
|
|
495
|
+
it("scrubs the query string from the logged path on a 429", async () => {
|
|
496
|
+
__resetBuckets();
|
|
497
|
+
const logLines: Array<Record<string, unknown>> = [];
|
|
498
|
+
const app = Fastify({
|
|
499
|
+
trustProxy: true,
|
|
500
|
+
logger: {
|
|
501
|
+
level: "warn",
|
|
502
|
+
stream: {
|
|
503
|
+
write(msg: string) {
|
|
504
|
+
logLines.push(JSON.parse(msg) as Record<string, unknown>);
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
app.register(rateLimitMiddleware, { perIpPerMinute: 1 });
|
|
510
|
+
app.get("/v1/requests/:id/confirm", async () => ({ ok: true }));
|
|
511
|
+
await app.ready();
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const url = "/v1/requests/callbk_1/confirm?token=SUPERSECRETTOKEN";
|
|
515
|
+
const headers = { "x-forwarded-for": "1.2.3.4" };
|
|
516
|
+
await app.inject({ method: "GET", url, headers });
|
|
517
|
+
const limited = await app.inject({ method: "GET", url, headers });
|
|
518
|
+
expect(limited.statusCode).toBe(429);
|
|
519
|
+
|
|
520
|
+
const logged = logLines.find((line) => line["msg"] === "rate_limited");
|
|
521
|
+
expect(logged?.["path"]).toBe("/v1/requests/callbk_1/confirm");
|
|
522
|
+
const text = JSON.stringify(logLines);
|
|
523
|
+
expect(text).not.toContain("SUPERSECRETTOKEN");
|
|
524
|
+
expect(text).not.toContain("token=");
|
|
525
|
+
} finally {
|
|
526
|
+
await app.close();
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
});
|
|
@@ -187,7 +187,13 @@ export const rateLimitMiddleware: FastifyPluginCallback<RateLimitOptions> = (
|
|
|
187
187
|
// Evict stale buckets on every 429 to cap memory under abuse.
|
|
188
188
|
evictStaleBuckets(now, windowMs);
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
// The path WITHOUT its query string (#5970 security review, finding 1): a
|
|
191
|
+
// route may carry its credential there (`/v1/requests/:id/confirm?token=…`
|
|
192
|
+
// does), and a rate-limit warning is a line an attacker can provoke at
|
|
193
|
+
// will — logging the raw target would let them farm credentials out of
|
|
194
|
+
// the operator's own log by tripping the limiter.
|
|
195
|
+
const pathOnly = req.url.split("?")[0] ?? req.url;
|
|
196
|
+
req.log.warn({ ip, path: pathOnly, count: bucket.count }, RATE_LIMITED);
|
|
191
197
|
|
|
192
198
|
return reply
|
|
193
199
|
.status(429)
|