create-ailk 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -8
- package/component-catalog.json +12248 -0
- package/component-catalog.md +193 -68
- package/dist/cli.js +29 -1
- package/dist/derive-architecture.js +5 -2
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/extract-module-bundle.d.ts +50 -2
- package/dist/lib/extract-module-bundle.js +223 -15
- package/dist/lib/fetch-guard.d.ts +28 -0
- package/dist/lib/fetch-guard.js +101 -0
- package/dist/lib/fetch-module.js +4 -0
- package/dist/lib/fetch-template.js +4 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +60 -8
- package/dist/module-architecture.js +587 -242
- package/dist/parse-args.js +6 -2
- package/dist/programmatic.d.ts +10 -0
- package/dist/programmatic.js +134 -19
- package/dist/site-chrome.d.ts +80 -0
- package/dist/site-chrome.js +135 -0
- package/dist/surfaces.d.ts +21 -6
- package/dist/surfaces.js +209 -36
- package/dist/template-manifest.d.ts +88 -3
- package/dist/template-manifest.js +281 -29
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +2 -0
- package/package.json +10 -2
- package/site-templates/_base/ailk-template.json +6 -0
- package/site-templates/_base/content/en/Pricing/pricing.mdx +169 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +351 -0
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +400 -0
- package/site-templates/marketing-site/ailk-template.json +11 -0
- package/site-templates/marketing-site/content/en/blog/05-this-vc-morning-routine-will-make-you-super-annoying.mdx +36 -0
- package/site-templates/marketing-site/provenance.json +4 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +288 -0
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +286 -0
- package/site-templates/marketing-site-free/ailk-template.json +2 -1
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +279 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +336 -0
- package/site-templates/minimalism-site/ailk-template.json +22 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-1.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-2.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-3.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-4.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-5.svg +7 -0
- package/site-templates/minimalism-site/assets/avatar-placeholder-6.svg +7 -0
- package/site-templates/minimalism-site/assets/hero-backdrop.svg +17 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-1.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-2.svg +6 -0
- package/site-templates/minimalism-site/assets/logo-placeholder-3.svg +6 -0
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/AboutPage/about.mdx +110 -0
- package/site-templates/minimalism-site/content/en/ContactPage/contact.mdx +68 -0
- package/site-templates/minimalism-site/content/en/FAQPage/faq.mdx +57 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +244 -0
- package/site-templates/minimalism-site/content/en/LandingPage/referral.mdx +77 -0
- package/site-templates/minimalism-site/content/en/LandingPage/reports.mdx +73 -0
- package/site-templates/minimalism-site/content/en/ReportPage/annual-letter-2024.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/field-notes-2023.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/platform-shift-2025.mdx +52 -0
- package/site-templates/minimalism-site/content/en/ReportPage/state-of-the-market-2026.mdx +52 -0
- package/site-templates/minimalism-site/content/en/Service/first-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/second-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/Service/third-engagement.mdx +38 -0
- package/site-templates/minimalism-site/content/en/ServicesList/services.mdx +73 -0
- package/site-templates/minimalism-site/flows/referral.flow.json +71 -0
- package/site-templates/minimalism-site/provenance.json +4 -0
- package/site-templates/saas-product/TEMPLATE-MANIFEST.md +61 -0
- package/site-templates/saas-product/ailk-template.json +16 -0
- package/site-templates/saas-product/provenance.json +10 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.claude/skills/README.md +16 -25
- package/templates/.env.example +102 -12
- package/templates/.github/workflows/ci.yml +48 -0
- package/templates/README.md +3 -1
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +29 -2
- package/templates/apps/api/eslint.config.mjs +69 -0
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +7 -2
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +89 -9
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +167 -0
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +10 -1
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +521 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +150 -0
- package/templates/apps/api/src/middleware/default-tenant.ts +136 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +171 -0
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/leads/list.ts +104 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +521 -55
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +408 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +329 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +300 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +160 -0
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +284 -0
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/owner-workspaces.ts +132 -0
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +58 -25
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +12 -9
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/{scaffold.test.ts → create_project.test.ts} +23 -23
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/eslint.config.mjs +69 -0
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +8 -2
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +61 -37
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/{scaffold.ts → create_project.ts} +3 -3
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +9 -7
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +74 -8
- package/templates/apps/web/CLAUDE.md +46 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +67 -14
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +19 -2
- package/templates/apps/web/app/[locale]/(authed)/leads/__tests__/page.test.tsx +181 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/components/LeadsTable.tsx +137 -0
- package/templates/apps/web/app/[locale]/(authed)/leads/page.tsx +111 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +1 -1
- package/templates/apps/web/app/[locale]/[slug]/__tests__/page.test.tsx +111 -0
- package/templates/apps/web/app/[locale]/[slug]/page.tsx +65 -0
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +6 -5
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +1 -1
- package/templates/apps/web/app/[locale]/layout.tsx +308 -65
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/components/google-analytics.tsx +18 -1
- package/templates/apps/web/eslint.config.mjs +69 -0
- package/templates/apps/web/jest.config.cjs +15 -19
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +50 -6
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/nav-config.ts +27 -7
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/site-chrome.ts +29 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +50 -0
- package/templates/apps/web/messages/en.json +216 -2
- package/templates/apps/web/next.config.mjs +20 -0
- package/templates/apps/web/package.json +8 -3
- package/templates/apps/web/sentry.client.config.ts +131 -0
- package/templates/apps/web/sentry.edge.config.ts +72 -0
- package/templates/apps/web/sentry.server.config.ts +72 -0
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/apps/web/src/lib/routes.ts +13 -18
- package/templates/brand_assets/README.md +24 -0
- package/templates/content/en/AboutPage/about.mdx +160 -15
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ContactPage/contact.mdx +90 -13
- package/templates/content/en/FAQPage/faq.mdx +62 -22
- package/templates/content/en/HomePage/home.mdx +244 -20
- package/templates/content/en/LandingPage/blog.mdx +83 -0
- package/templates/content/en/LandingPage/features.mdx +103 -0
- package/templates/content/en/LandingPage/how-it-works.mdx +143 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +149 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/seed.test.ts +1 -1
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/eslint.config.mjs +69 -0
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +7 -3
- package/templates/database/seed.ts +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/templates/package.json +1 -14
- package/versions.json +52 -0
- package/site-templates/marketing-site-free/content/en/Pricing/pricing.mdx +0 -103
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/site-templates/{marketing-site-free → _base}/content/en/PrivacyPage/privacy.mdx +0 -0
- /package/site-templates/{marketing-site-free → _base}/content/en/TermsPage/terms.mdx +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
- /package/templates/apps/web/{src/components → components}/SiteNav.tsx +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file site.test.ts
|
|
3
|
+
* @description Coverage for the site-alias -> workspace resolution (#6155
|
|
4
|
+
* spec D2/D5/D6, AC-5): `resolveSiteWorkspaceId()`'s two-and-only-two
|
|
5
|
+
* resolution rule and `assertSiteAliasResolves()`'s startup refusal.
|
|
6
|
+
*
|
|
7
|
+
* Mocks the module-level `prisma` singleton via the repo's manual-mock
|
|
8
|
+
* convention (`apps/api/src/lib/__mocks__/prisma.ts`), the same shape
|
|
9
|
+
* `middleware/__tests__/default-tenant.test.ts` uses — this suite exercises
|
|
10
|
+
* the REAL `resolveSentinelWorkspaceRow` (`@working-theory/database/tenancy`,
|
|
11
|
+
* mapped straight to source by `jest.config.cjs`) against the mocked
|
|
12
|
+
* `prisma.workspace` delegate, not a re-implementation of it.
|
|
13
|
+
*
|
|
14
|
+
* `DATABASE_URL` is set globally by `src/__tests__/setup.ts`, so every case
|
|
15
|
+
* here runs past the D6 database-less carve-out and exercises the real
|
|
16
|
+
* resolution path. `CONTENT_ADAPTER` is pinned to `database` per-case below
|
|
17
|
+
* (#6228): the assertion's first arm now skips on any OTHER adapter, so a
|
|
18
|
+
* case that means to exercise the REAL resolution must say which adapter it
|
|
19
|
+
* is booting under rather than inherit whatever the ambient env holds.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line no-restricted-syntax -- lib/prisma.ts wraps the external DB boundary (PrismaClient requires a live connection); __mocks__/prisma.ts is the repo-standard test double for this boundary.
|
|
23
|
+
jest.mock("../prisma.js");
|
|
24
|
+
|
|
25
|
+
import { prisma } from "../prisma.js";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
__resetSiteWorkspaceCacheForTests,
|
|
29
|
+
assertSiteAliasResolves,
|
|
30
|
+
currentSiteId,
|
|
31
|
+
resolveSiteWorkspaceId,
|
|
32
|
+
} from "../site.js";
|
|
33
|
+
|
|
34
|
+
const DEFAULT_ORG_ID = "org_00000000-0000-7000-8000-000000000001";
|
|
35
|
+
const DEFAULT_WORKSPACE_ID = "wksp_00000000-0000-7000-8000-000000000001";
|
|
36
|
+
const PINNED_WORKSPACE_ID = "wksp_01930000-0000-7000-8000-000000000042";
|
|
37
|
+
const NON_SENTINEL_ORG_ID = "org_01930000-0000-7bcd-8000-000000000002";
|
|
38
|
+
|
|
39
|
+
const ORIGINAL_AILK_SITE_ID = process.env.AILK_SITE_ID;
|
|
40
|
+
const ORIGINAL_CONTENT_ADAPTER = process.env.CONTENT_ADAPTER;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
// #6228 arm 0 — every case in this suite is about the alias->workspace
|
|
44
|
+
// resolution itself, which only the database adapter reaches.
|
|
45
|
+
process.env.CONTENT_ADAPTER = "database";
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
if (ORIGINAL_CONTENT_ADAPTER === undefined) {
|
|
50
|
+
delete process.env.CONTENT_ADAPTER;
|
|
51
|
+
} else {
|
|
52
|
+
process.env.CONTENT_ADAPTER = ORIGINAL_CONTENT_ADAPTER;
|
|
53
|
+
}
|
|
54
|
+
__resetSiteWorkspaceCacheForTests();
|
|
55
|
+
jest.clearAllMocks();
|
|
56
|
+
if (ORIGINAL_AILK_SITE_ID === undefined) {
|
|
57
|
+
delete process.env.AILK_SITE_ID;
|
|
58
|
+
} else {
|
|
59
|
+
process.env.AILK_SITE_ID = ORIGINAL_AILK_SITE_ID;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("resolveSiteWorkspaceId / assertSiteAliasResolves (#6155 D2/D5/D6)", () => {
|
|
64
|
+
it("case 1 — resolves a pinned alias to the matching workspace, and the assertion returns without throwing", async () => {
|
|
65
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
66
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
|
|
67
|
+
id: PINNED_WORKSPACE_ID,
|
|
68
|
+
organizationId: NON_SENTINEL_ORG_ID,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const resolved = await resolveSiteWorkspaceId();
|
|
72
|
+
|
|
73
|
+
expect(resolved).toBe(PINNED_WORKSPACE_ID);
|
|
74
|
+
expect(prisma.workspace.findUnique).toHaveBeenCalledWith({
|
|
75
|
+
where: { id: PINNED_WORKSPACE_ID },
|
|
76
|
+
});
|
|
77
|
+
await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("case 2 — the unset alias (the scaffold default) resolves to the seeded sentinel workspace (D2 rule 2)", async () => {
|
|
81
|
+
delete process.env.AILK_SITE_ID;
|
|
82
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
|
|
83
|
+
id: DEFAULT_WORKSPACE_ID,
|
|
84
|
+
organizationId: DEFAULT_ORG_ID,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const resolved = await resolveSiteWorkspaceId();
|
|
88
|
+
|
|
89
|
+
expect(resolved).toBe(DEFAULT_WORKSPACE_ID);
|
|
90
|
+
expect(prisma.workspace.findUnique).toHaveBeenCalledWith({
|
|
91
|
+
where: { id: DEFAULT_WORKSPACE_ID },
|
|
92
|
+
});
|
|
93
|
+
// resolveSentinelWorkspaceRow carries no workspace.count() gate (D2).
|
|
94
|
+
expect(prisma.workspace.count).not.toHaveBeenCalled();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("case 3 — a pinned alias naming no workspace does not resolve, and the assertion rejects naming both the alias and AILK_SITE_ID", async () => {
|
|
98
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
99
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
|
|
100
|
+
|
|
101
|
+
const resolved = await resolveSiteWorkspaceId();
|
|
102
|
+
expect(resolved).toBeNull();
|
|
103
|
+
|
|
104
|
+
await expect(assertSiteAliasResolves()).rejects.toThrow(
|
|
105
|
+
new RegExp(PINNED_WORKSPACE_ID.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
|
|
106
|
+
);
|
|
107
|
+
await expect(assertSiteAliasResolves()).rejects.toThrow(/AILK_SITE_ID/);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("case 4 — memoized: a second resolveSiteWorkspaceId() call issues zero further Prisma queries", async () => {
|
|
111
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
112
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
|
|
113
|
+
id: PINNED_WORKSPACE_ID,
|
|
114
|
+
organizationId: NON_SENTINEL_ORG_ID,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const first = await resolveSiteWorkspaceId();
|
|
118
|
+
const second = await resolveSiteWorkspaceId();
|
|
119
|
+
|
|
120
|
+
expect(first).toBe(PINNED_WORKSPACE_ID);
|
|
121
|
+
expect(second).toBe(PINNED_WORKSPACE_ID);
|
|
122
|
+
expect(prisma.workspace.findUnique).toHaveBeenCalledTimes(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("case 5 — the unset alias resolves to the sentinel on a MULTI-workspace deployment (the D2 rule-2 regression case)", async () => {
|
|
126
|
+
// The exact case resolveDefaultWorkspaceRow's workspace.count() gate
|
|
127
|
+
// would have refused — three workspaces exist, one of them the seeded
|
|
128
|
+
// sentinel, and AILK_SITE_ID is unpinned. resolveSentinelWorkspaceRow
|
|
129
|
+
// carries no count() gate, so this must still resolve.
|
|
130
|
+
delete process.env.AILK_SITE_ID;
|
|
131
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue({
|
|
132
|
+
id: DEFAULT_WORKSPACE_ID,
|
|
133
|
+
organizationId: DEFAULT_ORG_ID,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const resolved = await resolveSiteWorkspaceId();
|
|
137
|
+
|
|
138
|
+
expect(resolved).toBe(DEFAULT_WORKSPACE_ID);
|
|
139
|
+
expect(prisma.workspace.count).not.toHaveBeenCalled();
|
|
140
|
+
await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("case 6 — the unset alias does not resolve when the sentinel workspace is absent — the misconfiguration AC-5 asks to fail loudly", async () => {
|
|
144
|
+
delete process.env.AILK_SITE_ID;
|
|
145
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
|
|
146
|
+
|
|
147
|
+
const resolved = await resolveSiteWorkspaceId();
|
|
148
|
+
expect(resolved).toBeNull();
|
|
149
|
+
|
|
150
|
+
await expect(assertSiteAliasResolves()).rejects.toThrow(/AILK_SITE_ID/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("case 7 — a genuine thrown resolution error PROPAGATES and crashes the boot — it is NOT swallowed as a database-less carve-out", async () => {
|
|
154
|
+
// The code-review finding this case pins (#6155): the D6 carve-out is a
|
|
155
|
+
// presence check made BEFORE any query, never a catch around one that
|
|
156
|
+
// already ran. A reachable database that errors mid-query (a real
|
|
157
|
+
// connection failure, a permissions error) is exactly the
|
|
158
|
+
// misconfiguration class AC-5 exists to surface, so it must reject —
|
|
159
|
+
// not resolve silently the way the "cannot be attempted" cases do.
|
|
160
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
161
|
+
const connectionError = new Error("P1001: Can't reach database server");
|
|
162
|
+
(prisma.workspace.findUnique as jest.Mock).mockRejectedValue(
|
|
163
|
+
connectionError,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
await expect(resolveSiteWorkspaceId()).rejects.toThrow(connectionError);
|
|
167
|
+
await expect(assertSiteAliasResolves()).rejects.toThrow(connectionError);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("case 8 — prisma.workspace.findUnique missing entirely (a prisma double that never touches tenancy) is the ONLY case that skips without throwing", async () => {
|
|
171
|
+
// The exact shape that surfaced the D6 carve-out: a shared/global prisma
|
|
172
|
+
// test double (lib/__mocks__/prisma-client.js, and the many hand-rolled
|
|
173
|
+
// hoisted factories elsewhere) that declares no workspace delegate at
|
|
174
|
+
// all — the resolution could not even be ATTEMPTED. Distinguishes this
|
|
175
|
+
// from case 7's genuine mid-query failure: here findUnique is not a
|
|
176
|
+
// function at all, never called, never thrown from.
|
|
177
|
+
delete process.env.AILK_SITE_ID;
|
|
178
|
+
const original = prisma.workspace.findUnique;
|
|
179
|
+
// eslint-disable-next-line no-restricted-syntax -- deliberately simulating a prisma double that declares no `workspace.findUnique` delegate at all (the shape assertSiteAliasResolves()'s presence check exists to catch), restored in the finally block below.
|
|
180
|
+
(prisma.workspace as unknown as Record<string, unknown>).findUnique =
|
|
181
|
+
undefined;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
|
|
185
|
+
} finally {
|
|
186
|
+
prisma.workspace.findUnique = original;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("currentSiteId() stays synchronous and byte-identical for the env-set case (NFR1)", () => {
|
|
191
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
192
|
+
expect(currentSiteId()).toBe(PINNED_WORKSPACE_ID);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("currentSiteId() stays synchronous and byte-identical for the env-unset case (NFR1)", () => {
|
|
196
|
+
delete process.env.AILK_SITE_ID;
|
|
197
|
+
expect(currentSiteId()).toBe("default");
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("assertSiteAliasResolves — the #6228 content-adapter carve-out (arm 0)", () => {
|
|
202
|
+
it("skips the assertion on a non-database adapter, even with DATABASE_URL set and unreachable", async () => {
|
|
203
|
+
// The scaffold-boot shape: create-ailk's verify-scaffold.mjs sets a
|
|
204
|
+
// PLACEHOLDER DATABASE_URL (validateConfig checks presence only, Prisma
|
|
205
|
+
// connects lazily), so arm 1 does not fire. Before #6228 the generated
|
|
206
|
+
// MDX site crashed at boot on `Can't reach database server`.
|
|
207
|
+
process.env.CONTENT_ADAPTER = "mdx";
|
|
208
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
209
|
+
(prisma.workspace.findUnique as jest.Mock).mockRejectedValue(
|
|
210
|
+
new Error("Can't reach database server at 127.0.0.1:5432"),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
|
|
214
|
+
// The arm is a PRESENCE check made BEFORE any query — not a catch around
|
|
215
|
+
// one that already ran (the #6155 code-review finding this file's
|
|
216
|
+
// docblock records).
|
|
217
|
+
expect(prisma.workspace.findUnique).not.toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("skips on the projection adapter too — the arm is `not database`, not `is mdx`", async () => {
|
|
221
|
+
process.env.CONTENT_ADAPTER = "projection";
|
|
222
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
223
|
+
|
|
224
|
+
await expect(assertSiteAliasResolves()).resolves.toBeUndefined();
|
|
225
|
+
expect(prisma.workspace.findUnique).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("still throws under the database adapter when the alias names no workspace", async () => {
|
|
229
|
+
// AC-2: the arm narrows WHICH boots are asserted, never WHAT the
|
|
230
|
+
// assertion decides once it runs.
|
|
231
|
+
process.env.CONTENT_ADAPTER = "database";
|
|
232
|
+
process.env.AILK_SITE_ID = PINNED_WORKSPACE_ID;
|
|
233
|
+
(prisma.workspace.findUnique as jest.Mock).mockResolvedValue(null);
|
|
234
|
+
|
|
235
|
+
await expect(assertSiteAliasResolves()).rejects.toThrow(/AILK_SITE_ID/);
|
|
236
|
+
expect(prisma.workspace.findUnique).toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* apps/api stripe shim — surface-drift smoke tests.
|
|
4
|
+
*
|
|
5
|
+
* Ensures apps/api/src/lib/stripe.ts re-exports exactly what
|
|
6
|
+
* @working-theory/stripe-client exports, with no surface drift.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
jest.mock("@working-theory/stripe-client", () => ({
|
|
10
|
+
getStripeClient: jest.fn(),
|
|
11
|
+
assertSandboxKey: jest.fn(),
|
|
12
|
+
verifyWebhookSignature: jest.fn(),
|
|
13
|
+
createCheckoutSession: jest.fn(),
|
|
14
|
+
STRIPE_API_VERSION: "2023-10-16",
|
|
15
|
+
WEBHOOK_TOLERANCE_SECONDS: 300,
|
|
16
|
+
StripeConfigError: class StripeConfigError extends Error {},
|
|
17
|
+
StripeWebhookSignatureError: class StripeWebhookSignatureError extends Error {},
|
|
18
|
+
UnknownProductCodeError: class UnknownProductCodeError extends Error {},
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// Import the shim (not the wrapper directly)
|
|
22
|
+
import * as stripeShim from "../stripe.js";
|
|
23
|
+
import * as stripeClient from "@working-theory/stripe-client";
|
|
24
|
+
|
|
25
|
+
describe("apps/api/src/lib/stripe — surface re-exports", () => {
|
|
26
|
+
it("exports getStripeClient as a function", () => {
|
|
27
|
+
expect(typeof stripeShim.getStripeClient).toBe("function");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("exports assertSandboxKey as a function", () => {
|
|
31
|
+
expect(typeof stripeShim.assertSandboxKey).toBe("function");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("exports verifyWebhookSignature as a function", () => {
|
|
35
|
+
expect(typeof stripeShim.verifyWebhookSignature).toBe("function");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("exports createCheckoutSession as a function", () => {
|
|
39
|
+
expect(typeof stripeShim.createCheckoutSession).toBe("function");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("exports STRIPE_API_VERSION", () => {
|
|
43
|
+
expect(stripeShim.STRIPE_API_VERSION).toBe("2023-10-16");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("exports WEBHOOK_TOLERANCE_SECONDS", () => {
|
|
47
|
+
expect(stripeShim.WEBHOOK_TOLERANCE_SECONDS).toBe(300);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("exports StripeConfigError as a constructor producing Error instances", () => {
|
|
51
|
+
expect(typeof stripeShim.StripeConfigError).toBe("function");
|
|
52
|
+
expect(new stripeShim.StripeConfigError()).toBeInstanceOf(Error);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("exports StripeWebhookSignatureError as a constructor producing Error instances", () => {
|
|
56
|
+
expect(typeof stripeShim.StripeWebhookSignatureError).toBe("function");
|
|
57
|
+
expect(new stripeShim.StripeWebhookSignatureError()).toBeInstanceOf(Error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("exports UnknownProductCodeError as a constructor producing Error instances", () => {
|
|
61
|
+
expect(typeof stripeShim.UnknownProductCodeError).toBe("function");
|
|
62
|
+
expect(new stripeShim.UnknownProductCodeError()).toBeInstanceOf(Error);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("shim functions are the same references as @working-theory/stripe-client exports", () => {
|
|
66
|
+
expect(stripeShim.getStripeClient).toBe(stripeClient.getStripeClient);
|
|
67
|
+
expect(stripeShim.assertSandboxKey).toBe(stripeClient.assertSandboxKey);
|
|
68
|
+
expect(stripeShim.verifyWebhookSignature).toBe(
|
|
69
|
+
stripeClient.verifyWebhookSignature,
|
|
70
|
+
);
|
|
71
|
+
expect(stripeShim.createCheckoutSession).toBe(
|
|
72
|
+
stripeClient.createCheckoutSession,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-recipient hourly send budget for the auth transactional-email relay
|
|
3
|
+
* (#5962 § D8c).
|
|
4
|
+
*
|
|
5
|
+
* WHY IT IS PER RECIPIENT AND NOT PER SITE. The first draft of #5962's spec
|
|
6
|
+
* carried a second, site-wide cap. It was removed, and its removal is the
|
|
7
|
+
* point of this module's shape: Better Auth writes the `Verification` row
|
|
8
|
+
* BEFORE it invokes `sendResetPassword`, so every attacker request both feeds
|
|
9
|
+
* a site-wide counter and refreshes its window — roughly one request per 72
|
|
10
|
+
* seconds would hold a site permanently at its cap and silence reset mail for
|
|
11
|
+
* EVERY user, indefinitely. A site-wide cap an attacker can hold open is a
|
|
12
|
+
* denial of the control it implements. `siteVolumeAlert()` below replaces it
|
|
13
|
+
* with an operator SIGNAL that never refuses a send.
|
|
14
|
+
*
|
|
15
|
+
* The per-recipient budget IS a lockout weapon, narrowly: an attacker who
|
|
16
|
+
* requests three resets for one address in an hour denies that person a reset
|
|
17
|
+
* for the rest of it. Accepted for the reason
|
|
18
|
+
* `services/project-listing-resume-email.ts` gives and this module can
|
|
19
|
+
* honestly claim — the window is an hour rather than forever, it clears
|
|
20
|
+
* itself, and it is bounded to ONE victim rather than to the whole site.
|
|
21
|
+
*
|
|
22
|
+
* SINGLE-REPLICA CAVEAT, stated rather than hidden: the store is an in-process
|
|
23
|
+
* Map, so on an N-instance deployment the effective ceiling is N × the cap.
|
|
24
|
+
* Identical to the caveat `middleware/rate-limit.ts` documents for itself. The
|
|
25
|
+
* durable alternative needs a table `database/auth/schema.prisma` does not
|
|
26
|
+
* have, and #5962 adds no migration (Q16).
|
|
27
|
+
*
|
|
28
|
+
* PII: the key is `sha256(lowercased recipient + ":" + kind)`. This module is
|
|
29
|
+
* memory an operator can dump, and a plaintext address in it would be a
|
|
30
|
+
* recipient list.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { createHash } from "node:crypto";
|
|
34
|
+
|
|
35
|
+
import { getClock } from "@working-theory/common";
|
|
36
|
+
|
|
37
|
+
/** The three fixed message kinds the relay will send. */
|
|
38
|
+
export type AuthEmailKind =
|
|
39
|
+
| "password-reset"
|
|
40
|
+
| "email-verification"
|
|
41
|
+
| "password-changed";
|
|
42
|
+
|
|
43
|
+
/** Sends allowed per (recipient, kind) per hour. */
|
|
44
|
+
export const RECIPIENT_HOURLY_MAX = 3;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sends per hour, site-wide, past which ONE operator warning is logged. Never
|
|
48
|
+
* a refusal — see the module comment.
|
|
49
|
+
*/
|
|
50
|
+
export const SITE_HOURLY_ALERT_THRESHOLD = 50;
|
|
51
|
+
|
|
52
|
+
const WINDOW_MS = 60 * 60 * 1000;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Cap on distinct entries, independent of the time-based window. The key is
|
|
56
|
+
* derived from a caller-supplied address, so a caller minting a fresh address
|
|
57
|
+
* per request would otherwise grow the map without bound for the lifetime of
|
|
58
|
+
* one isolate. Same reasoning, same shape, as `rate-limit.ts`'s MAX_BUCKETS.
|
|
59
|
+
*/
|
|
60
|
+
export const MAX_BUCKETS = 10_000;
|
|
61
|
+
|
|
62
|
+
type Bucket = { count: number; windowStartMs: number };
|
|
63
|
+
|
|
64
|
+
const buckets = new Map<string, Bucket>();
|
|
65
|
+
|
|
66
|
+
let siteWindowStartMs = 0;
|
|
67
|
+
let siteCount = 0;
|
|
68
|
+
let siteAlertedThisWindow = false;
|
|
69
|
+
|
|
70
|
+
export interface BudgetOptions {
|
|
71
|
+
/** Explicit clock reading. Defaults to `getClock().now()`. Tests inject it. */
|
|
72
|
+
nowMs?: number;
|
|
73
|
+
/** Entry cap. Exposed for tests only — no production caller overrides it. */
|
|
74
|
+
maxBuckets?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function bucketKey(recipient: string, kind: AuthEmailKind): string {
|
|
78
|
+
const normalized = recipient.trim().toLowerCase();
|
|
79
|
+
return createHash("sha256").update(`${normalized}:${kind}`).digest("hex");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Counts one send against `(recipient, kind)` and reports whether it may
|
|
84
|
+
* proceed.
|
|
85
|
+
*
|
|
86
|
+
* `false` means SUPPRESS THE SEND — it never means "tell the caller". The
|
|
87
|
+
* relay answers 202 either way, because an observable suppression is the same
|
|
88
|
+
* account-enumeration oracle the rest of this flow is built to close.
|
|
89
|
+
*/
|
|
90
|
+
export function consumeRecipientBudget(
|
|
91
|
+
recipient: string,
|
|
92
|
+
kind: AuthEmailKind,
|
|
93
|
+
options: BudgetOptions = {},
|
|
94
|
+
): boolean {
|
|
95
|
+
const now = options.nowMs ?? getClock().now();
|
|
96
|
+
const maxBuckets = options.maxBuckets ?? MAX_BUCKETS;
|
|
97
|
+
const key = bucketKey(recipient, kind);
|
|
98
|
+
|
|
99
|
+
let bucket = buckets.get(key);
|
|
100
|
+
|
|
101
|
+
if (!bucket || now - bucket.windowStartMs >= WINDOW_MS) {
|
|
102
|
+
// A genuinely new identity at capacity evicts the single oldest-inserted
|
|
103
|
+
// entry (`Map` iteration order is insertion order) before inserting
|
|
104
|
+
// itself — O(1) amortized, and no behaviour change under the cap.
|
|
105
|
+
if (!buckets.has(key) && buckets.size >= maxBuckets) {
|
|
106
|
+
const oldestKey = buckets.keys().next().value;
|
|
107
|
+
if (oldestKey !== undefined) buckets.delete(oldestKey);
|
|
108
|
+
}
|
|
109
|
+
bucket = { count: 0, windowStartMs: now };
|
|
110
|
+
buckets.set(key, bucket);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (bucket.count >= RECIPIENT_HOURLY_MAX) return false;
|
|
114
|
+
|
|
115
|
+
bucket.count += 1;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Counts one send site-wide and reports whether THIS call is the one that
|
|
121
|
+
* should emit the hourly operator warning.
|
|
122
|
+
*
|
|
123
|
+
* It is a signal, not a gate: nothing in this module consults its return value
|
|
124
|
+
* to deny a send, and nothing may be added that does.
|
|
125
|
+
*/
|
|
126
|
+
export function siteVolumeAlert(options: BudgetOptions = {}): boolean {
|
|
127
|
+
const now = options.nowMs ?? getClock().now();
|
|
128
|
+
|
|
129
|
+
if (now - siteWindowStartMs >= WINDOW_MS) {
|
|
130
|
+
siteWindowStartMs = now;
|
|
131
|
+
siteCount = 0;
|
|
132
|
+
siteAlertedThisWindow = false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
siteCount += 1;
|
|
136
|
+
|
|
137
|
+
if (siteCount >= SITE_HOURLY_ALERT_THRESHOLD && !siteAlertedThisWindow) {
|
|
138
|
+
siteAlertedThisWindow = true;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Test-only reset. Not intended for production use. */
|
|
145
|
+
export function __resetAuthEmailBudget(): void {
|
|
146
|
+
buckets.clear();
|
|
147
|
+
siteWindowStartMs = 0;
|
|
148
|
+
siteCount = 0;
|
|
149
|
+
siteAlertedThisWindow = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Test-only accessor for the distinct-bucket count. */
|
|
153
|
+
export function __authEmailBucketCount(): number {
|
|
154
|
+
return buckets.size;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Test-only accessor proving the stored keys carry no plaintext address. */
|
|
158
|
+
export function __authEmailBudgetKeys(): string[] {
|
|
159
|
+
return [...buckets.keys()];
|
|
160
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared content adapter singleton for apps/api.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Adapter selection is NOT decided here — it is `resolveContentAdapter()`'s job
|
|
5
|
+
* (#5804, roadmap D3: one resolver, one env var). This module supplies the
|
|
6
|
+
* app's own inputs (content root, default locale, manifest path, the projection
|
|
7
|
+
* factory) and memoises the result for the process.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - "projection" → projection adapter (requires compiled manifest)
|
|
9
|
+
* `CONTENT_ADAPTER` selects: `mdx` (default) | `projection` | `database`.
|
|
10
|
+
* `CONTENT_BACKEND` is read for one more release by the resolver's shim.
|
|
10
11
|
*
|
|
11
12
|
* Content-root resolution order:
|
|
12
13
|
* 1. `CONTENT_ROOT` env var (explicit override — recommended for production)
|
|
@@ -15,13 +16,27 @@
|
|
|
15
16
|
*
|
|
16
17
|
* Manifest-path resolution (projection only):
|
|
17
18
|
* 1. `MANIFEST_PATH` env var (explicit override)
|
|
18
|
-
* 2. `<content-root>/.compiled/manifest.json` (default)
|
|
19
|
+
* 2. `<content-root>/.compiled/manifest.json` (the resolver's default)
|
|
20
|
+
*
|
|
21
|
+
* `workspaceId` is supplied by the CALLER, never read here (#5852). A public
|
|
22
|
+
* read resolves its workspace through `services/public-site-resolver.ts` and
|
|
23
|
+
* passes the result in; a caller that passes none still raises the resolver's
|
|
24
|
+
* "workspaceId required" error under `CONTENT_ADAPTER=database`, rather than
|
|
25
|
+
* silently reading some other workspace's pages. There is no local ADAPTER memo
|
|
26
|
+
* slot: `resolveContentAdapter()` already memoises on the full key
|
|
27
|
+
* (kind + workspaceId + root + locale + manifest), and a second cache keyed on
|
|
28
|
+
* less than that is how one workspace's adapter reaches the next request. This
|
|
29
|
+
* module's own INPUTS — the content root and the default locale, both
|
|
30
|
+
* workspace-independent and both fixed for the process — stay memoised, so a
|
|
31
|
+
* public read costs no more filesystem work than it did before the seam widened.
|
|
19
32
|
*/
|
|
20
33
|
import { existsSync } from "node:fs";
|
|
21
34
|
import { resolve } from "node:path";
|
|
22
35
|
|
|
23
36
|
import {
|
|
24
|
-
|
|
37
|
+
resetResolvedContentAdapter,
|
|
38
|
+
resolveContentAdapter,
|
|
39
|
+
type ArtifactPrismaClient,
|
|
25
40
|
type ContentAdapter,
|
|
26
41
|
type MdxContentAdapter,
|
|
27
42
|
} from "@working-theory/content-adapters";
|
|
@@ -30,7 +45,17 @@ import { loadManifestFromDisk } from "@working-theory/content-projection/node";
|
|
|
30
45
|
|
|
31
46
|
import { readDefaultLocale } from "./locales.js";
|
|
32
47
|
|
|
33
|
-
|
|
48
|
+
/** Process-fixed inputs, resolved once: neither depends on the workspace. */
|
|
49
|
+
let memoisedContentRoot: string | undefined;
|
|
50
|
+
let memoisedDefaultLocale: string | undefined;
|
|
51
|
+
|
|
52
|
+
/** Caller-supplied inputs; everything else is this module's own (#5852 D5). */
|
|
53
|
+
export interface GetContentAdapterOptions {
|
|
54
|
+
/** The resolved tenant. Required by `CONTENT_ADAPTER=database`, ignored otherwise. */
|
|
55
|
+
workspaceId?: string | undefined;
|
|
56
|
+
/** apps/api's client. The database adapter never constructs one. */
|
|
57
|
+
prisma?: ArtifactPrismaClient | undefined;
|
|
58
|
+
}
|
|
34
59
|
|
|
35
60
|
/**
|
|
36
61
|
* Resolve the content root. Honours `CONTENT_ROOT` env first, then probes the
|
|
@@ -50,37 +75,38 @@ export function getContentRoot(): string {
|
|
|
50
75
|
}
|
|
51
76
|
|
|
52
77
|
/**
|
|
53
|
-
* Returns the
|
|
54
|
-
* Safe to call repeatedly. Adapter type is determined by `CONTENT_ADAPTER` env
|
|
78
|
+
* Returns the content adapter for the given tenant, memoised by the resolver.
|
|
79
|
+
* Safe to call repeatedly. Adapter type is determined by `CONTENT_ADAPTER` env,
|
|
80
|
+
* read by the resolver.
|
|
55
81
|
*/
|
|
56
|
-
export function getContentAdapter(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function buildAdapter(): ContentAdapter {
|
|
64
|
-
const contentRoot = getContentRoot();
|
|
65
|
-
|
|
66
|
-
if (process.env.CONTENT_ADAPTER === "projection") {
|
|
67
|
-
const manifestPath =
|
|
68
|
-
process.env.MANIFEST_PATH ??
|
|
69
|
-
resolve(contentRoot, ".compiled", "manifest.json");
|
|
70
|
-
|
|
71
|
-
const manifest = loadManifestFromDisk(manifestPath);
|
|
72
|
-
return createProjectionAdapter({ manifest });
|
|
73
|
-
}
|
|
82
|
+
export function getContentAdapter(
|
|
83
|
+
options: GetContentAdapterOptions = {},
|
|
84
|
+
): ContentAdapter {
|
|
85
|
+
memoisedContentRoot ??= getContentRoot();
|
|
86
|
+
memoisedDefaultLocale ??= readDefaultLocale();
|
|
74
87
|
|
|
75
|
-
return
|
|
88
|
+
return resolveContentAdapter({
|
|
89
|
+
workspaceId: options.workspaceId,
|
|
90
|
+
prisma: options.prisma,
|
|
91
|
+
contentRoot: memoisedContentRoot,
|
|
92
|
+
defaultLocale: memoisedDefaultLocale,
|
|
93
|
+
manifestPath: process.env.MANIFEST_PATH,
|
|
94
|
+
// The projection adapter lives in a package that depends on
|
|
95
|
+
// content-adapters, so the resolver cannot import it without closing a
|
|
96
|
+
// cycle. apps/api depends on both, and injects it.
|
|
97
|
+
createProjectionAdapter: (manifestPath) =>
|
|
98
|
+
createProjectionAdapter({ manifest: loadManifestFromDisk(manifestPath) }),
|
|
99
|
+
});
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
/**
|
|
79
|
-
* Test/utility helper — drop the
|
|
103
|
+
* Test/utility helper — drop the memoised adapter so the next call rebuilds
|
|
80
104
|
* with current env. Not used in production.
|
|
81
105
|
*/
|
|
82
106
|
export function resetContentAdapter(): void {
|
|
83
|
-
|
|
107
|
+
memoisedContentRoot = undefined;
|
|
108
|
+
memoisedDefaultLocale = undefined;
|
|
109
|
+
resetResolvedContentAdapter();
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
export type { MdxContentAdapter };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* The magnet-deliverable Asset store's data-access boundary (#6310 D16).
|
|
4
|
+
*
|
|
5
|
+
* `deliverable-fulfillment.ts` resolves `deliverables.assets[].assetKey`
|
|
6
|
+
* against the workspace-scoped `Asset` model before falling back to the
|
|
7
|
+
* legacy `DeliverableAsset` store (D1/D2), and the download route
|
|
8
|
+
* (`routes/deliverables/index.ts`) serves an `asset_`-class grant's bytes
|
|
9
|
+
* from the same model (D6). Neither caller may run a raw `prisma.asset`
|
|
10
|
+
* query directly — that access is banned by the tenant-scope
|
|
11
|
+
* `forbidden_pattern` in architecture.yaml — and neither is `lib/asset-store.ts`
|
|
12
|
+
* a fit: it is `workspace`/`multi-tenant`-owned (`MODULE_SLICES`), so a
|
|
13
|
+
* `signal-collectors` file (the route) cannot import it, and
|
|
14
|
+
* `deliverable-fulfillment.ts` ships in every scaffold regardless of module
|
|
15
|
+
* selection. This file is its own named boundary instead — modelled
|
|
16
|
+
* line-for-line on `lib/asset-store.ts` — exporting exactly two scoped
|
|
17
|
+
* reads and nothing else:
|
|
18
|
+
*
|
|
19
|
+
* - `findDeliverableAssetIdByKey(workspaceId, key)` — the mint-time
|
|
20
|
+
* resolution (D2): does an `Asset` exist at this workspace's `key`?
|
|
21
|
+
* - `readDeliverableAsset(assetId, workspaceId)` — the serve-time read
|
|
22
|
+
* (D6): the bytes, scoped by the `workspaceId` the grant carries
|
|
23
|
+
* (D15), so a rewritten or mis-set grant can never serve another
|
|
24
|
+
* workspace's bytes.
|
|
25
|
+
*
|
|
26
|
+
* No write, no `findMany`, no list, no unscoped read — both functions take
|
|
27
|
+
* a `workspaceId` the caller resolved server-side, never a client-supplied
|
|
28
|
+
* value, so neither can enumerate or cross a tenant boundary (NFR3).
|
|
29
|
+
*/
|
|
30
|
+
import { prisma } from "./prisma.js";
|
|
31
|
+
|
|
32
|
+
/** What the mint-time resolution needs, and nothing more. */
|
|
33
|
+
export async function findDeliverableAssetIdByKey(
|
|
34
|
+
workspaceId: string,
|
|
35
|
+
key: string,
|
|
36
|
+
): Promise<string | null> {
|
|
37
|
+
const row = await prisma.asset.findFirst({
|
|
38
|
+
where: { workspaceId, key },
|
|
39
|
+
select: { id: true },
|
|
40
|
+
});
|
|
41
|
+
return row?.id ?? null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What the serve path needs, and deliberately nothing more (no key, no workspace). */
|
|
45
|
+
export type DeliverableAssetBytes = {
|
|
46
|
+
filename: string;
|
|
47
|
+
contentType: string;
|
|
48
|
+
bytes: Buffer;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolve one asset's bytes by id, scoped by `workspaceId` (D15). A foreign-
|
|
53
|
+
* workspace id — or any other miss — returns `null`; the caller turns that
|
|
54
|
+
* into the same uniform 404 an unknown grant gets (D6/FR5).
|
|
55
|
+
*/
|
|
56
|
+
export async function readDeliverableAsset(
|
|
57
|
+
assetId: string,
|
|
58
|
+
workspaceId: string | null,
|
|
59
|
+
): Promise<DeliverableAssetBytes | null> {
|
|
60
|
+
if (workspaceId === null) return null;
|
|
61
|
+
|
|
62
|
+
const row = await prisma.asset.findFirst({
|
|
63
|
+
where: { id: assetId, workspaceId },
|
|
64
|
+
select: { filename: true, contentType: true, bytes: true },
|
|
65
|
+
});
|
|
66
|
+
if (!row) return null;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...row,
|
|
70
|
+
// Prisma maps a `Bytes` column to `Uint8Array`, and Fastify treats only
|
|
71
|
+
// a `Buffer` as a raw payload — this wraps rather than copies
|
|
72
|
+
// (`Buffer.from(buffer, byteOffset, length)` is a view), mirroring
|
|
73
|
+
// `asset-store.ts`'s own identical wrap.
|
|
74
|
+
bytes: Buffer.from(row.bytes.buffer, row.bytes.byteOffset, row.bytes.byteLength),
|
|
75
|
+
};
|
|
76
|
+
}
|