create-ailk 0.1.0 → 0.2.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 +124 -3
- package/component-catalog.md +456 -0
- package/dist/cli.js +92 -16
- package/dist/component-catalog.d.ts +11 -0
- package/dist/component-catalog.js +39 -0
- package/dist/copy.d.ts +7 -0
- package/dist/copy.js +24 -0
- package/dist/derive-architecture.d.ts +13 -0
- package/dist/derive-architecture.js +238 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +19 -2
- package/dist/lib/extract-module-bundle.d.ts +16 -0
- package/dist/lib/extract-module-bundle.js +184 -0
- package/dist/lib/fetch-module.d.ts +55 -0
- package/dist/lib/fetch-module.js +125 -0
- package/dist/lib/module-license-gate.d.ts +48 -0
- package/dist/lib/module-license-gate.js +67 -0
- package/dist/lib/module-tier.d.ts +35 -0
- package/dist/lib/module-tier.js +37 -0
- package/dist/module-architecture.d.ts +175 -0
- package/dist/module-architecture.js +663 -0
- package/dist/parse-args.d.ts +47 -1
- package/dist/parse-args.js +143 -2
- package/dist/programmatic.d.ts +160 -3
- package/dist/programmatic.js +196 -7
- package/dist/surfaces.d.ts +211 -0
- package/dist/surfaces.js +532 -0
- package/dist/sync-routes.d.ts +92 -0
- package/dist/sync-routes.js +350 -0
- package/package.json +12 -5
- package/templates/.claude/rules/architecture.md +6 -2
- package/templates/.claude/skills/README.md +9 -9
- package/templates/.claude/skills/add-schema/SKILL.md +14 -7
- package/templates/.claude/skills/add-schema/references/schema-types.md +109 -6
- package/templates/.claude/skills/provision-config/SKILL.md +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +498 -1
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +370 -0
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +376 -0
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +471 -0
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +365 -0
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +60 -0
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +446 -0
- package/templates/.claude/skills/suggest-site-pages/references/categories.md +4 -4
- package/templates/.env.example +126 -2
- package/templates/CLAUDE.md +11 -5
- package/templates/README.md +1 -1
- package/templates/apps/api/.env.example +38 -0
- package/templates/apps/api/CLAUDE.md +78 -1
- package/templates/apps/api/LICENSE +201 -0
- package/templates/apps/api/api/index.ts +6 -141
- package/templates/apps/api/jest.config.cjs +16 -0
- package/templates/apps/api/package.json +22 -6
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +141 -0
- package/templates/apps/api/src/__tests__/server.test.ts +46 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +211 -0
- package/templates/apps/api/src/bin/abandon-sweep.ts +42 -0
- package/templates/apps/api/src/bin/deliverable-resend.ts +144 -0
- package/templates/apps/api/src/bin/deliverable-upload.ts +114 -0
- package/templates/apps/api/src/bin/followup-sweep.ts +44 -0
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +46 -0
- package/templates/apps/api/src/bin/seed-presets.ts +58 -0
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +104 -0
- package/templates/apps/api/src/bin/session-retention-sweep.ts +46 -0
- package/templates/apps/api/src/cli.ts +5 -11
- package/templates/apps/api/src/config/index.ts +35 -0
- package/templates/apps/api/src/config/modules.ts +43 -0
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +11 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +241 -0
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +2 -2
- package/templates/apps/api/src/lib/site.ts +16 -0
- package/templates/apps/api/src/lib/stripe.ts +22 -0
- package/templates/apps/api/src/lib/tenant-db.ts +225 -0
- package/templates/apps/api/src/lib/ws-token.ts +91 -0
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +7 -1
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +91 -4
- package/templates/apps/api/src/middleware/auth.ts +43 -8
- package/templates/apps/api/src/middleware/rate-limit.ts +78 -3
- package/templates/apps/api/src/middleware/tenant.ts +99 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +92 -0
- package/templates/apps/api/src/openapi/spec.ts +312 -3
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +175 -0
- package/templates/apps/api/src/routes/aeo/index.ts +107 -0
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +5 -2
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +22 -3
- package/templates/apps/api/src/routes/auth/tokens.ts +13 -1
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +450 -0
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +262 -0
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +436 -0
- package/templates/apps/api/src/routes/billing/index.ts +36 -0
- package/templates/apps/api/src/routes/billing/portal.ts +182 -0
- package/templates/apps/api/src/routes/billing/read.ts +75 -0
- package/templates/apps/api/src/routes/billing/usage.ts +153 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +99 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +20 -8
- package/templates/apps/api/src/routes/content/create.ts +1 -0
- package/templates/apps/api/src/routes/content/delete.ts +1 -0
- package/templates/apps/api/src/routes/content/index.ts +42 -6
- package/templates/apps/api/src/routes/content/update.ts +1 -0
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +393 -0
- package/templates/apps/api/src/routes/deliverables/index.ts +200 -0
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +443 -0
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +82 -0
- package/templates/apps/api/src/routes/flows/README.md +147 -0
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +752 -0
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +671 -0
- package/templates/apps/api/src/routes/flows/index.ts +202 -0
- package/templates/apps/api/src/routes/flows/recommender.ts +443 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +438 -31
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +10 -10
- package/templates/apps/api/src/routes/leads/index.ts +43 -9
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +558 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +815 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +869 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +221 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +468 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +424 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +707 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +375 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +467 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +566 -0
- package/templates/apps/api/src/routes/project-listings/get.ts +70 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +120 -0
- package/templates/apps/api/src/routes/project-listings/list.ts +47 -0
- package/templates/apps/api/src/routes/project-listings/me.ts +76 -0
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +222 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +97 -0
- package/templates/apps/api/src/routes/project-listings/patch.ts +136 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +52 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +37 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +22 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +106 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +122 -0
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +490 -0
- package/templates/apps/api/src/routes/schedule/index.ts +249 -0
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +385 -0
- package/templates/apps/api/src/routes/slack/actions.ts +177 -0
- package/templates/apps/api/src/routes/slack/index.ts +38 -0
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +16 -6
- package/templates/apps/api/src/routes/test/auth/session.ts +8 -6
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +577 -0
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +78 -0
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +41 -0
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +64 -0
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +42 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +84 -0
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +119 -0
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +238 -0
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +101 -0
- package/templates/apps/api/src/routes/webhooks/README.md +35 -12
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +315 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +270 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +248 -20
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +356 -0
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +671 -0
- package/templates/apps/api/src/routes/workspaces/config.ts +125 -0
- package/templates/apps/api/src/routes/workspaces/index.ts +194 -0
- package/templates/apps/api/src/routes/workspaces/sessions.ts +218 -0
- package/templates/apps/api/src/server.ts +307 -7
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +489 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +132 -0
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +523 -0
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +359 -0
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +142 -0
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +425 -0
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +2840 -0
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +393 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +8 -8
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +560 -0
- package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +684 -0
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +406 -0
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +290 -0
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +736 -0
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +475 -0
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +340 -0
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +983 -0
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +278 -0
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +71 -0
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +379 -0
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +941 -0
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +588 -0
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +768 -0
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +1314 -0
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +341 -0
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +567 -0
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +474 -0
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +354 -0
- package/templates/apps/api/src/services/abandon-email.ts +307 -0
- package/templates/apps/api/src/services/aeo-score.ts +288 -0
- package/templates/apps/api/src/services/answer-distributions.ts +138 -0
- package/templates/apps/api/src/services/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +523 -0
- package/templates/apps/api/src/services/entitlements.ts +102 -0
- package/templates/apps/api/src/services/flow-aggregate.ts +232 -0
- package/templates/apps/api/src/services/flow-checkouts.ts +123 -0
- package/templates/apps/api/src/services/flow-engine.ts +1296 -0
- package/templates/apps/api/src/services/lead-promotion.ts +176 -0
- package/templates/apps/api/src/services/lead-routing.ts +3 -3
- package/templates/apps/api/src/services/listing-config.ts +102 -0
- package/templates/apps/api/src/services/listing-csv-sweep.ts +455 -0
- package/templates/apps/api/src/services/listing-promotion.ts +342 -0
- package/templates/apps/api/src/services/playbook-compile.ts +398 -0
- package/templates/apps/api/src/services/playbook-render.ts +263 -0
- package/templates/apps/api/src/services/project-listing-decision.ts +510 -0
- package/templates/apps/api/src/services/project-listing-flow.ts +426 -0
- package/templates/apps/api/src/services/project-listing-issue.ts +251 -0
- package/templates/apps/api/src/services/project-listings.ts +1472 -0
- package/templates/apps/api/src/services/recommender-capture.ts +835 -0
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +392 -0
- package/templates/apps/api/src/services/scheduling/index.ts +63 -0
- package/templates/apps/api/src/services/scheduling/types.ts +88 -0
- package/templates/apps/api/src/services/slack-notify.ts +240 -0
- package/templates/apps/api/src/services/tenant-context.ts +451 -0
- package/templates/apps/api/src/services/usage-aggregation.ts +516 -0
- package/templates/apps/api/src/services/usage-credits.ts +340 -0
- package/templates/apps/api/src/services/usage-metering.ts +699 -0
- package/templates/apps/api/src/services/waitlist-dashboard.ts +947 -0
- package/templates/apps/api/src/services/waitlist-experiments.ts +213 -0
- package/templates/apps/api/src/services/waitlist-followup.ts +486 -0
- package/templates/apps/api/src/services/waitlist-scoring.ts +166 -0
- package/templates/apps/api/src/services/waitlist-signups.ts +165 -0
- package/templates/apps/api/src/vercel-handler.ts +165 -0
- package/templates/apps/api/tsconfig.json +0 -12
- package/templates/apps/mcp/CLAUDE.md +19 -9
- package/templates/apps/mcp/LICENSE +201 -0
- package/templates/apps/mcp/__tests__/create_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/delete_page.test.ts +1 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +105 -0
- package/templates/apps/mcp/__tests__/parity.test.ts +70 -6
- package/templates/apps/mcp/__tests__/request_callback.test.ts +4 -4
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +242 -0
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +4 -4
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +4 -4
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +47 -0
- package/templates/apps/mcp/__tests__/update_page.test.ts +1 -0
- package/templates/apps/mcp/package.json +6 -4
- package/templates/apps/mcp/src/config/modules.ts +37 -0
- package/templates/apps/mcp/src/server.ts +109 -18
- package/templates/apps/mcp/src/tools/capture_lead.ts +1 -1
- package/templates/apps/mcp/src/tools/create_booking.ts +58 -0
- package/templates/apps/mcp/src/tools/create_page.ts +1 -0
- package/templates/apps/mcp/src/tools/delete_page.ts +1 -0
- package/templates/apps/mcp/src/tools/get_event_meta.ts +55 -0
- package/templates/apps/mcp/src/tools/get_flow.ts +61 -0
- package/templates/apps/mcp/src/tools/index.ts +25 -0
- package/templates/apps/mcp/src/tools/list_availability.ts +67 -0
- package/templates/apps/mcp/src/tools/purchase.ts +3 -0
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +83 -0
- package/templates/apps/mcp/src/tools/update_page.ts +1 -0
- package/templates/apps/mcp/tsconfig.json +1 -9
- package/templates/apps/web/.env.example +44 -0
- package/templates/apps/web/CLAUDE.md +28 -0
- package/templates/apps/web/LICENSE +201 -0
- package/templates/apps/web/__tests__/__mocks__/code-highlight.jest-stub.ts +24 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl-server.ts +33 -0
- package/templates/apps/web/__tests__/__mocks__/next-intl.ts +18 -0
- package/templates/apps/web/__tests__/__mocks__/server-only.ts +13 -0
- package/templates/apps/web/__tests__/__mocks__/style-mock.js +4 -0
- package/templates/apps/web/__tests__/__mocks__/vercel-analytics-server.ts +9 -0
- package/templates/apps/web/__tests__/jest-support/import-meta-transformer.cjs +90 -0
- package/templates/apps/web/__tests__/setup.ts +22 -32
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +6 -3
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +8 -2
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +260 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +263 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-out-button.tsx +39 -0
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +54 -0
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +96 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +309 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +149 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +273 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/gate.test.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +171 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +179 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +116 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ForbiddenState.tsx +24 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +66 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +61 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +114 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +85 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +67 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/format.ts +111 -0
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +179 -0
- package/templates/apps/web/app/[locale]/about/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +6 -1
- package/templates/apps/web/app/[locale]/careers/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/careers/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/careers/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/case-studies/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/case-studies/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/case-studies/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/clients/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/clients/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/contact/ContactForm.tsx +21 -1
- package/templates/apps/web/app/[locale]/contact/ConversationalIntakeWrapper.tsx +188 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/ConversationalIntakeWrapper.test.tsx +242 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/contact.actions.test.ts +29 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/intake-analytics.test.ts +51 -0
- package/templates/apps/web/app/[locale]/contact/__tests__/page-renderer-migration.test.tsx +39 -15
- package/templates/apps/web/app/[locale]/contact/contact.actions.ts +21 -3
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +49 -0
- package/templates/apps/web/app/[locale]/contact/intake-script.ts +66 -0
- package/templates/apps/web/app/[locale]/contact/page.tsx +14 -4
- package/templates/apps/web/app/[locale]/courses/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/courses/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/courses/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/downloads/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/downloads/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/events/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/events/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/events/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/faq/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +265 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +445 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +341 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +287 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +302 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +128 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +254 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +144 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +108 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +532 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +121 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +457 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +396 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +114 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +52 -0
- package/templates/apps/web/app/[locale]/how-to/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/how-to/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/integrations/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/integrations/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/integrations/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/launch-service/page.tsx +14 -1
- package/templates/apps/web/app/[locale]/layout.tsx +127 -16
- package/templates/apps/web/app/[locale]/page.tsx +10 -1
- package/templates/apps/web/app/[locale]/partners/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/partners/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/partners/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/products/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/products/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/products/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/projects/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/projects/__tests__/page-renderer-migration.test.tsx +10 -0
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +110 -0
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/projects/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/resources/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/resources/__tests__/page-renderer-migration.test.tsx +12 -2
- package/templates/apps/web/app/[locale]/resources/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +117 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +96 -0
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +92 -0
- package/templates/apps/web/app/[locale]/schedule/page.tsx +82 -0
- package/templates/apps/web/app/[locale]/services/[slug]/page.tsx +9 -0
- package/templates/apps/web/app/[locale]/services/__tests__/page-renderer-migration.test.tsx +3 -3
- package/templates/apps/web/app/[locale]/services/page.tsx +9 -1
- package/templates/apps/web/app/[locale]/sign-in/__tests__/next-path.test.ts +31 -0
- package/templates/apps/web/app/[locale]/sign-in/__tests__/page.test.tsx +152 -0
- package/templates/apps/web/app/[locale]/sign-in/next-path.ts +0 -0
- package/templates/apps/web/app/[locale]/sign-in/page.tsx +83 -0
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +123 -0
- package/templates/apps/web/app/[locale]/software/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/software/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/tech-articles/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/tech-articles/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/[locale]/testimonials/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/testimonials/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/thanks/[slug]/__tests__/page.test.tsx +93 -0
- package/templates/apps/web/app/[locale]/thanks/[slug]/page.tsx +48 -0
- package/templates/apps/web/app/[locale]/use-cases/__tests__/page-renderer-migration.test.tsx +11 -1
- package/templates/apps/web/app/[locale]/use-cases/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/[slug]/page.tsx +13 -1
- package/templates/apps/web/app/[locale]/whitepapers/__tests__/page-renderer-migration.test.tsx +1 -1
- package/templates/apps/web/app/globals.css +35 -0
- package/templates/apps/web/app/llms.txt/route.ts +2 -1
- package/templates/apps/web/app/og/route.tsx +10 -2
- package/templates/apps/web/auth.ts +8 -0
- package/templates/apps/web/components/sign-out-trigger.tsx +42 -0
- package/templates/apps/web/content/en/blog/README.txt +25 -0
- package/templates/apps/web/content/en/docs/index.mdx +28 -0
- package/templates/apps/web/jest.config.cjs +90 -27
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +119 -0
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +499 -0
- package/templates/apps/web/lib/account-config.ts +13 -0
- package/templates/apps/web/lib/server-api.ts +85 -0
- package/templates/apps/web/lib/site-brand.tsx +121 -0
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/messages/en.json +275 -7
- package/templates/apps/web/middleware.ts +11 -3
- package/templates/apps/web/next.config.mjs +8 -2
- package/templates/apps/web/package.json +17 -7
- package/templates/apps/web/postcss.config.mjs +23 -0
- package/templates/apps/web/public/.well-known/security.txt +3 -3
- package/templates/apps/web/src/lib/routes.ts +15 -2
- package/templates/apps/web/tsconfig.json +1 -16
- package/templates/content/_site.mdx +36 -25
- package/templates/content/en/AboutPage/about.mdx +24 -14
- package/templates/content/en/ContactPage/contact.mdx +23 -14
- package/templates/content/en/FAQPage/faq.mdx +10 -392
- package/templates/content/en/HomePage/home.mdx +14 -285
- package/templates/database/CHANGELOG.md +443 -0
- package/templates/database/LICENSE +201 -0
- package/templates/database/README.md +3 -1
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +392 -0
- package/templates/database/__tests__/flows-presets.test.ts +173 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +146 -0
- package/templates/database/__tests__/seed.test.ts +88 -0
- package/templates/database/__tests__/user-cascade.test.ts +19 -4
- package/templates/database/auth/schema.prisma +521 -0
- package/templates/database/config/schema.prisma +234 -0
- package/templates/database/content/schema.prisma +263 -0
- package/templates/database/flows/listing-diagnostic.ts +182 -0
- package/templates/database/flows/presets.ts +315 -0
- package/templates/database/inbox/__tests__/schema.test.ts +4 -0
- package/templates/database/inbox/schema.prisma +574 -3
- package/templates/database/jest.config.cjs +8 -0
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +201 -0
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +67 -0
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +46 -0
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +25 -0
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +49 -0
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +197 -0
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +32 -0
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +22 -0
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +151 -0
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +223 -0
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +22 -0
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +32 -0
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +40 -0
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +4 -0
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +2 -0
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +2 -0
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +29 -0
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +6 -0
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +39 -0
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +24 -0
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +3 -0
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +33 -0
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +30 -0
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +47 -0
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +16 -0
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +47 -0
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +10 -0
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +26 -0
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +90 -0
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +174 -0
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +14 -0
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +25 -0
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
- package/templates/database/ops/schema.prisma +27 -0
- package/templates/database/package.json +42 -4
- package/templates/database/prisma.config.ts +1 -1
- package/templates/database/schema.prisma +19 -435
- package/templates/database/seed-test.ts +14 -4
- package/templates/database/seed.ts +148 -0
- package/templates/database/tenancy.ts +45 -0
- package/templates/database/tsconfig.flows.json +15 -0
- package/templates/database/tsconfig.json +7 -1
- package/templates/database/tsconfig.tenancy.json +14 -0
- package/templates/package.json +30 -55
- package/templates/pnpm-workspace.yaml +0 -3
- package/templates/project.yaml +1 -1
- package/templates/tsconfig.json +1 -37
- package/templates/apps/web/__tests__/auth/adversarial/cookie-attributes.test.ts +0 -94
- package/templates/apps/web/__tests__/auth/adversarial/oauth-state-parameter.test.ts +0 -75
- package/templates/apps/web/__tests__/auth/adversarial/redirect-uri-allowlist.test.ts +0 -82
- package/templates/apps/web/__tests__/auth/env-helpers.test.ts +0 -168
- package/templates/apps/web/__tests__/authority-list-routes.test.tsx +0 -208
- package/templates/apps/web/__tests__/better-stack.test.ts +0 -66
- package/templates/apps/web/__tests__/blog-dir-resolution.test.ts +0 -87
- package/templates/apps/web/__tests__/blog-source-populated.test.ts +0 -130
- package/templates/apps/web/__tests__/data-testid-coverage.test.tsx +0 -60
- package/templates/apps/web/__tests__/google-analytics.test.tsx +0 -104
- package/templates/apps/web/__tests__/i18n/middleware.test.ts +0 -119
- package/templates/apps/web/__tests__/i18n/routing.test.ts +0 -114
- package/templates/apps/web/__tests__/i18n/translations.test.ts +0 -191
- package/templates/apps/web/__tests__/json-ld.test.ts +0 -98
- package/templates/apps/web/__tests__/knowledge-events-routes.test.tsx +0 -580
- package/templates/apps/web/__tests__/layout-stealth.test.ts +0 -39
- package/templates/apps/web/__tests__/lighthouse-fixtures/improvement-image-shrink.tsx +0 -103
- package/templates/apps/web/__tests__/lighthouse-fixtures/override-fixture.md +0 -114
- package/templates/apps/web/__tests__/lighthouse-fixtures/regression-bloat.tsx +0 -94
- package/templates/apps/web/__tests__/page.test.tsx +0 -230
- package/templates/apps/web/__tests__/products-services-phase1.test.tsx +0 -321
- package/templates/apps/web/__tests__/projects-routes.test.tsx +0 -392
- package/templates/apps/web/__tests__/resources-downloads-integrations-phase2.test.tsx +0 -475
- package/templates/apps/web/__tests__/routes/ai-plugin-json.test.ts +0 -53
- package/templates/apps/web/__tests__/routes/blog-route.test.tsx +0 -191
- package/templates/apps/web/__tests__/routes/llms-txt.test.ts +0 -123
- package/templates/apps/web/__tests__/routes/robots-txt.test.ts +0 -52
- package/templates/apps/web/__tests__/routes/sitemap-xml.test.ts +0 -78
- package/templates/apps/web/__tests__/routes/well-known-mcp.test.ts +0 -203
- package/templates/apps/web/__tests__/section-routes.test.tsx +0 -693
- package/templates/apps/web/__tests__/security-headers.test.ts +0 -201
- package/templates/apps/web/__tests__/sentry-config.test.ts +0 -116
- package/templates/apps/web/__tests__/sitemap-locales.test.ts +0 -415
- package/templates/apps/web/__tests__/standalone-informational-routes.test.tsx +0 -844
- package/templates/apps/web/app/[locale]/__tests__/layout.test.tsx +0 -120
- package/templates/apps/web/app/[locale]/__tests__/page-renderer-migration.test.tsx +0 -202
- package/templates/apps/web/app/__tests__/error-boundaries.test.tsx +0 -152
- package/templates/apps/web/content/en/blog/index.mdx +0 -15
- package/templates/apps/web/content/en/blog/launching-ailk.mdx +0 -20
- package/templates/apps/web/content/en/docs/getting-started.mdx +0 -30
- package/templates/content/.gitkeep +0 -0
- package/templates/content/CLAUDE.md +0 -85
- package/templates/content/ar/HomePage/home.mdx +0 -123
- package/templates/content/ar/_site.mdx +0 -35
- package/templates/content/de/_site.mdx +0 -29
- package/templates/content/en/.gitkeep +0 -0
- package/templates/content/en/AboutPage/about.schema.json +0 -82
- package/templates/content/en/BookCall/book-call.mdx +0 -19
- package/templates/content/en/BookCall/book-call.schema.json +0 -121
- package/templates/content/en/BreadcrumbList/site-breadcrumbs.mdx +0 -20
- package/templates/content/en/CareersList/careers.mdx +0 -24
- package/templates/content/en/CaseStudiesList/case-studies.mdx +0 -22
- package/templates/content/en/CaseStudiesList/showcase.mdx +0 -8
- package/templates/content/en/CaseStudy/ailk-aeo-rollout.mdx +0 -32
- package/templates/content/en/CaseStudy/schema-builder-migration.mdx +0 -41
- package/templates/content/en/ClientsList/clients.mdx +0 -38
- package/templates/content/en/ContactPage/contact.schema.json +0 -100
- package/templates/content/en/ContactPoint/primary.mdx +0 -14
- package/templates/content/en/Course/aeo-fundamentals.mdx +0 -37
- package/templates/content/en/Course/schema-builder-workshop.mdx +0 -62
- package/templates/content/en/CoursesList/courses.mdx +0 -22
- package/templates/content/en/DownloadsList/downloads.mdx +0 -22
- package/templates/content/en/Event/aeo-office-hours-may-2026.mdx +0 -39
- package/templates/content/en/Event/ailk-launch-day-2026.mdx +0 -35
- package/templates/content/en/EventsList/events.mdx +0 -26
- package/templates/content/en/FAQPage/faq.schema.json +0 -231
- package/templates/content/en/GlossaryList/glossary.mdx +0 -19
- package/templates/content/en/GlossaryList/glossary.schema.json +0 -300
- package/templates/content/en/HomePage/home.schema.json +0 -88
- package/templates/content/en/HomePage/launch-service.mdx +0 -475
- package/templates/content/en/HomePage/launch-service.schema.json +0 -138
- package/templates/content/en/HowTo/add-mdx-content.mdx +0 -38
- package/templates/content/en/HowTo/configure-aeo-scoring.mdx +0 -35
- package/templates/content/en/HowTo/set-up-json-ld.mdx +0 -36
- package/templates/content/en/Integration/github-actions.mdx +0 -31
- package/templates/content/en/Integration/openai-api.mdx +0 -26
- package/templates/content/en/Integration/vercel.mdx +0 -32
- package/templates/content/en/IntegrationsList/integrations.mdx +0 -22
- package/templates/content/en/JobPosting/content-strategist.mdx +0 -46
- package/templates/content/en/JobPosting/developer-advocate.mdx +0 -47
- package/templates/content/en/JobPosting/senior-fullstack-engineer.mdx +0 -57
- package/templates/content/en/LandingPage/aeo-audit.mdx +0 -19
- package/templates/content/en/LandingPage/aeo-audit.schema.json +0 -92
- package/templates/content/en/LandingPage/agency-waitlist.mdx +0 -340
- package/templates/content/en/LandingPage/agency-waitlist.schema.json +0 -152
- package/templates/content/en/LandingPage/developers.mdx +0 -19
- package/templates/content/en/LandingPage/developers.schema.json +0 -109
- package/templates/content/en/LandingPage/for-agencies.mdx +0 -390
- package/templates/content/en/LandingPage/for-agencies.schema.json +0 -107
- package/templates/content/en/LandingPage/for-ai-search-agencies.mdx +0 -417
- package/templates/content/en/LandingPage/for-ai-search-agencies.schema.json +0 -105
- package/templates/content/en/LandingPage/for-businesses.mdx +0 -19
- package/templates/content/en/LandingPage/for-businesses.schema.json +0 -96
- package/templates/content/en/LandingPage/platform.mdx +0 -369
- package/templates/content/en/LandingPage/platform.schema.json +0 -166
- package/templates/content/en/LandingPage/vs-ai-website-builders.mdx +0 -19
- package/templates/content/en/LandingPage/vs-ai-website-builders.schema.json +0 -102
- package/templates/content/en/LandingPage/vs-wordpress.mdx +0 -19
- package/templates/content/en/LandingPage/vs-wordpress.schema.json +0 -53
- package/templates/content/en/NewsletterSignup/newsletter.mdx +0 -236
- package/templates/content/en/NewsletterSignup/newsletter.schema.json +0 -91
- package/templates/content/en/Organization/site-owner.mdx +0 -16
- package/templates/content/en/Partner/prisma.mdx +0 -31
- package/templates/content/en/Partner/supabase.mdx +0 -30
- package/templates/content/en/Partner/vercel.mdx +0 -28
- package/templates/content/en/PartnersList/partners.mdx +0 -22
- package/templates/content/en/Person/founder.mdx +0 -18
- package/templates/content/en/Pricing/pricing.mdx +0 -538
- package/templates/content/en/Pricing/pricing.schema.json +0 -209
- package/templates/content/en/PrivacyPage/privacy.mdx +0 -165
- package/templates/content/en/PrivacyPage/privacy.schema.json +0 -64
- package/templates/content/en/Product/ailk.mdx +0 -19
- package/templates/content/en/Product/ailk.schema.json +0 -94
- package/templates/content/en/Product/api-launch-kit.mdx +0 -26
- package/templates/content/en/Product/launch-kit.mdx +0 -28
- package/templates/content/en/ProductsList/products.mdx +0 -26
- package/templates/content/en/Project/ailk-phase-2.mdx +0 -54
- package/templates/content/en/ProjectsList/projects.mdx +0 -25
- package/templates/content/en/ResourceDetail/aeo-scoring-guide.mdx +0 -27
- package/templates/content/en/ResourceDetail/content-model-whitepaper.mdx +0 -28
- package/templates/content/en/ResourceDetail/schema-org-quick-reference.mdx +0 -39
- package/templates/content/en/ResourcesHub/resources.mdx +0 -22
- package/templates/content/en/Service/consulting.mdx +0 -22
- package/templates/content/en/Service/implementation.mdx +0 -30
- package/templates/content/en/Service/launch-service.mdx +0 -232
- package/templates/content/en/Service/launch.mdx +0 -228
- package/templates/content/en/ServicesList/services.mdx +0 -84
- package/templates/content/en/SoftwareProduct/ailk-aeo.mdx +0 -44
- package/templates/content/en/SoftwareProduct/ailk-content-adapters.mdx +0 -47
- package/templates/content/en/SoftwareProduct/ailk-schema.mdx +0 -49
- package/templates/content/en/TeamList/team.mdx +0 -63
- package/templates/content/en/TeamMember/founder.mdx +0 -72
- package/templates/content/en/TermsPage/terms.mdx +0 -19
- package/templates/content/en/TermsPage/terms.schema.json +0 -99
- package/templates/content/en/TestimonialsList/testimonials.mdx +0 -44
- package/templates/content/en/UseCasesList/use-cases.mdx +0 -30
- package/templates/content/en/Whitepaper/ax-first-product-development.mdx +0 -27
- package/templates/content/en/Whitepaper/open-core-commercial-strategy.mdx +0 -27
- package/templates/content/en/Whitepaper/structured-data-for-answer-engines.mdx +0 -31
- package/templates/content/en-XA/AboutPage/about.mdx +0 -8
- package/templates/content/en-XA/FAQPage/faq.mdx +0 -8
- package/templates/content/en-XA/HomePage/home.mdx +0 -8
- package/templates/content/es/_site.mdx +0 -30
- package/templates/content/fr/_site.mdx +0 -30
- package/templates/content/ja/_site.mdx +0 -30
- package/templates/content/pt-BR/AboutPage/about.mdx +0 -8
- package/templates/content/pt-BR/FAQPage/faq.mdx +0 -8
- package/templates/content/pt-BR/HomePage/home.mdx +0 -23
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* Org-billing route group (#3701 — the apps/api predecessor to the
|
|
4
|
+
* org-billing UI, #3697).
|
|
5
|
+
*
|
|
6
|
+
* Routes:
|
|
7
|
+
* GET /v1/billing — org-scoped billing read (tier, seat
|
|
8
|
+
* count, billing-customer presence,
|
|
9
|
+
* entitlements)
|
|
10
|
+
* GET /v1/billing/usage — org-scoped usage read (#3717): the
|
|
11
|
+
* current-period report summary + credit
|
|
12
|
+
* status, client-safe (no Stripe id)
|
|
13
|
+
* POST /v1/billing/portal-session — create a Stripe customer-portal
|
|
14
|
+
* session for the caller's own org
|
|
15
|
+
*
|
|
16
|
+
* Registered INSIDE server.ts's `tenantScope` (spec D4 / AC-5) so
|
|
17
|
+
* `request.tenant` is populated by `tenantPreHandlerPlugin` before either
|
|
18
|
+
* handler runs — NOT at the public/checkout registration site, and NOT
|
|
19
|
+
* behind the `X-Internal-Auth` gate (spec 2.1.5).
|
|
20
|
+
*
|
|
21
|
+
* API↔MCP parity: the read is admin/settings-shaped (a human managing their
|
|
22
|
+
* own org's billing), not an agent tool — no MCP twin is warranted (spec §5,
|
|
23
|
+
* AC-6). The tenant-scoped authed MCP surface is tracked separately (#3591),
|
|
24
|
+
* mirroring the sibling /v1/workspaces/* routes' parity note.
|
|
25
|
+
*/
|
|
26
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
27
|
+
|
|
28
|
+
import { billingPortalSessionHandler } from "./portal.js";
|
|
29
|
+
import { billingReadHandler } from "./read.js";
|
|
30
|
+
import { billingUsageHandler } from "./usage.js";
|
|
31
|
+
|
|
32
|
+
export const billingRoutes: FastifyPluginAsync = async (fastify) => {
|
|
33
|
+
fastify.get("/v1/billing", billingReadHandler);
|
|
34
|
+
fastify.get("/v1/billing/usage", billingUsageHandler);
|
|
35
|
+
fastify.post("/v1/billing/portal-session", billingPortalSessionHandler);
|
|
36
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* POST /v1/billing/portal-session
|
|
4
|
+
*
|
|
5
|
+
* Creates a Stripe billing-portal session for the caller's OWN org (#3701 —
|
|
6
|
+
* the apps/api predecessor to the org-billing UI, #3697). Org scope is
|
|
7
|
+
* ALWAYS `request.tenant.organizationId` (spec D1) — never a client-supplied
|
|
8
|
+
* id, so this route can never act on a foreign org. Reuses the existing
|
|
9
|
+
* `createBillingPortalSession` wrapper (spec D3) and mirrors
|
|
10
|
+
* `checkout/sessions.ts`'s 404/500/502 error mapping exactly:
|
|
11
|
+
*
|
|
12
|
+
* 404 no-billing-customer — the org has no Stripe customer yet (no Stripe call)
|
|
13
|
+
* 500 stripe-not-configured — STRIPE_SECRET_KEY is missing/empty (no Stripe call)
|
|
14
|
+
* 500 live-mode-forbidden — STRIPE_SECRET_KEY is a live key (assertSandboxKey guard)
|
|
15
|
+
* 500 config-error — STRIPE_SECRET_KEY invalid configuration (from the SDK wrapper)
|
|
16
|
+
* 502 portal-unavailable — Stripe SDK threw an unexpected error
|
|
17
|
+
*
|
|
18
|
+
* Registered inside `server.ts`'s `tenantScope` (spec D4) behind
|
|
19
|
+
* `tenantPreHandlerPlugin` — cookie/session auth via the tenant scope, NOT
|
|
20
|
+
* the `X-Internal-Auth` internal gate `checkout/sessions.ts` uses (spec
|
|
21
|
+
* 2.1.5): the caller here is apps/workspace forwarding the browser cookie,
|
|
22
|
+
* not a server-to-server internal call.
|
|
23
|
+
*
|
|
24
|
+
* AUTHORIZATION (#3766): the tenant preHandler establishes session + org
|
|
25
|
+
* MEMBERSHIP only — it does not discriminate role. The billing portal is a
|
|
26
|
+
* privileged, MUTATING capability (Stripe's portal lets the holder change the
|
|
27
|
+
* payment method, download invoices, and cancel the subscription), so
|
|
28
|
+
* membership alone is not sufficient: this handler additionally requires an
|
|
29
|
+
* `owner` or `admin` role and answers 403 for a `member`. The check runs
|
|
30
|
+
* BEFORE the `getOrgBillingIdentity` lookup and before any Stripe call,
|
|
31
|
+
* mirroring `middleware/tenant.ts`'s default-deny ordering (deny before any
|
|
32
|
+
* lookup of the target, so an unauthorized caller learns nothing — not even
|
|
33
|
+
* whether the org has a billing customer).
|
|
34
|
+
*
|
|
35
|
+
* PII/secret-min: the raw `billingCustomerId` is used only as an input to
|
|
36
|
+
* `createBillingPortalSession` — it never appears in a response body or log
|
|
37
|
+
* line (spec 5.2).
|
|
38
|
+
*/
|
|
39
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
40
|
+
|
|
41
|
+
import { config } from "../../config/index.js";
|
|
42
|
+
import {
|
|
43
|
+
assertSandboxKey,
|
|
44
|
+
createBillingPortalSession,
|
|
45
|
+
StripeConfigError,
|
|
46
|
+
} from "../../lib/stripe.js";
|
|
47
|
+
import { getOrgBillingIdentity } from "../../services/entitlements.js";
|
|
48
|
+
import type { MembershipRole } from "../../services/tenant-context.js";
|
|
49
|
+
|
|
50
|
+
function unauthorized(reply: FastifyReply) {
|
|
51
|
+
return reply.status(401).send({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: { code: "unauthorized", message: "Authentication required" },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function forbidden(reply: FastifyReply) {
|
|
58
|
+
return reply.status(403).send({
|
|
59
|
+
ok: false,
|
|
60
|
+
error: { code: "forbidden", message: "Forbidden" },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Roles permitted to open the org's Stripe billing portal (#3766).
|
|
66
|
+
*
|
|
67
|
+
* Mirrors the owner/admin floor already applied to workspace creation in
|
|
68
|
+
* `services/tenant-context.ts` (`createWorkspaceForOrg` — "Creating a
|
|
69
|
+
* workspace requires the admin role"): same policy, same two roles. Expressed
|
|
70
|
+
* here as an allow-list rather than a `!==` chain so it is default-deny — a
|
|
71
|
+
* role later added to `MembershipRole` is refused until it is named here,
|
|
72
|
+
* instead of silently falling through into a privileged path.
|
|
73
|
+
*/
|
|
74
|
+
const BILLING_PORTAL_ROLES: ReadonlySet<MembershipRole> = new Set<MembershipRole>([
|
|
75
|
+
"owner",
|
|
76
|
+
"admin",
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
// Provisional return path (spec OQ-2 — resolved at implementation): the
|
|
80
|
+
// org-billing UI (#3697) has not landed a page path yet. Points at the
|
|
81
|
+
// workspace app's future billing-settings page under the configured frontend
|
|
82
|
+
// origin; finalize this constant when #3697 sets the real path.
|
|
83
|
+
const PORTAL_RETURN_PATH = "/settings/billing";
|
|
84
|
+
|
|
85
|
+
export async function billingPortalSessionHandler(
|
|
86
|
+
request: FastifyRequest,
|
|
87
|
+
reply: FastifyReply,
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
const tenant = request.tenant;
|
|
90
|
+
if (!tenant) return unauthorized(reply);
|
|
91
|
+
|
|
92
|
+
// Role gate (#3766) — BEFORE any lookup or Stripe call. The tenant
|
|
93
|
+
// preHandler proved membership; opening the billing portal additionally
|
|
94
|
+
// requires owner/admin.
|
|
95
|
+
if (!BILLING_PORTAL_ROLES.has(tenant.role)) {
|
|
96
|
+
request.log.warn(
|
|
97
|
+
{ method: request.method, path: request.url, status: 403 },
|
|
98
|
+
"[billing] portal session denied; insufficient role",
|
|
99
|
+
);
|
|
100
|
+
return forbidden(reply);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const identity = await getOrgBillingIdentity(tenant.organizationId);
|
|
104
|
+
if (!identity.billingCustomerId) {
|
|
105
|
+
// No Stripe call — the org has no billing customer to open a portal for.
|
|
106
|
+
return reply.status(404).send({
|
|
107
|
+
ok: false,
|
|
108
|
+
error: {
|
|
109
|
+
code: "no-billing-customer",
|
|
110
|
+
message: "No billing customer for this organization",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const stripeSecretKey = process.env["STRIPE_SECRET_KEY"];
|
|
116
|
+
if (!stripeSecretKey) {
|
|
117
|
+
return reply.status(500).send({
|
|
118
|
+
ok: false,
|
|
119
|
+
error: {
|
|
120
|
+
code: "stripe-not-configured",
|
|
121
|
+
message: "Stripe is not configured",
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
assertSandboxKey(stripeSecretKey);
|
|
127
|
+
} catch {
|
|
128
|
+
return reply.status(500).send({
|
|
129
|
+
ok: false,
|
|
130
|
+
error: {
|
|
131
|
+
code: "live-mode-forbidden",
|
|
132
|
+
message: "Live Stripe keys are not permitted",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const { url } = await createBillingPortalSession({
|
|
139
|
+
stripeCustomerId: identity.billingCustomerId,
|
|
140
|
+
returnUrl: `${config.FRONTEND_URL}${PORTAL_RETURN_PATH}`,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
request.log.info(
|
|
144
|
+
{ method: request.method, path: request.url, status: 200 },
|
|
145
|
+
"[billing] portal session created",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
return reply.status(200).send({ ok: true, data: { url } });
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err instanceof StripeConfigError) {
|
|
151
|
+
// 500 — misconfiguration; log message (no PII) but never echo in response.
|
|
152
|
+
request.log.error(
|
|
153
|
+
{
|
|
154
|
+
method: request.method,
|
|
155
|
+
path: request.url,
|
|
156
|
+
errName: err.name,
|
|
157
|
+
message: err.message,
|
|
158
|
+
},
|
|
159
|
+
"[billing] config error",
|
|
160
|
+
);
|
|
161
|
+
return reply.status(500).send({
|
|
162
|
+
ok: false,
|
|
163
|
+
error: { code: "config-error", message: "Billing portal is misconfigured" },
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
request.log.error(
|
|
167
|
+
{
|
|
168
|
+
method: request.method,
|
|
169
|
+
path: request.url,
|
|
170
|
+
errName: err instanceof Error ? err.name : "unknown",
|
|
171
|
+
},
|
|
172
|
+
"[billing] stripe error; portal-unavailable",
|
|
173
|
+
);
|
|
174
|
+
return reply.status(502).send({
|
|
175
|
+
ok: false,
|
|
176
|
+
error: {
|
|
177
|
+
code: "portal-unavailable",
|
|
178
|
+
message: "Billing portal is temporarily unavailable",
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* GET /v1/billing
|
|
4
|
+
*
|
|
5
|
+
* Org-scoped billing read (#3701 — the apps/api predecessor to the
|
|
6
|
+
* org-billing UI, #3697). The org is ALWAYS `request.tenant.organizationId`
|
|
7
|
+
* — there is no client-supplied org id, so cross-tenant enumeration is
|
|
8
|
+
* structurally impossible (spec D1). Returns a client-safe projection:
|
|
9
|
+
* `hasBillingCustomer` is a presence flag only — the raw Stripe customer id
|
|
10
|
+
* never crosses the API boundary (spec D2).
|
|
11
|
+
*
|
|
12
|
+
* Registered inside `server.ts`'s `tenantScope` (spec D4) — `request.tenant`
|
|
13
|
+
* is populated by `tenantPreHandlerPlugin` before this handler ever runs; a
|
|
14
|
+
* caller with no resolvable org membership is denied by the preHandler
|
|
15
|
+
* itself (403), never reaching this code.
|
|
16
|
+
*
|
|
17
|
+
* AUTHORIZATION (#3766) — membership is deliberately sufficient here; this
|
|
18
|
+
* route intentionally carries NO owner/admin role gate, unlike its sibling
|
|
19
|
+
* `portal.ts`. The distinction is capability, not merely read-vs-write:
|
|
20
|
+
*
|
|
21
|
+
* - It exposes no financial detail. The projection is plan metadata only —
|
|
22
|
+
* `tier`, `seatCount`, and the `hasBillingCustomer` PRESENCE FLAG. No
|
|
23
|
+
* invoices, no amounts, no payment methods, no subscription dates, and
|
|
24
|
+
* (per spec D2) never the raw Stripe customer id.
|
|
25
|
+
* - It confers no capability. There is nothing here to mutate; contrast the
|
|
26
|
+
* portal, whose session lets its holder change the payment method and
|
|
27
|
+
* cancel the subscription — which is why that route gates on role.
|
|
28
|
+
* - Its data is operationally load-bearing for ordinary members. `tier` and
|
|
29
|
+
* `entitlements` are what the app reads to decide which features to
|
|
30
|
+
* render; gating them on owner/admin would break the product for members
|
|
31
|
+
* without protecting anything sensitive.
|
|
32
|
+
*
|
|
33
|
+
* If this projection is ever widened to carry invoices, amounts, payment
|
|
34
|
+
* methods, or subscription lifecycle state, that change MUST also add the
|
|
35
|
+
* `portal.ts` role gate — the reason for its absence would no longer hold.
|
|
36
|
+
*/
|
|
37
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
getOrgBillingIdentity,
|
|
41
|
+
resolveEntitlements,
|
|
42
|
+
} from "../../services/entitlements.js";
|
|
43
|
+
|
|
44
|
+
function unauthorized(reply: FastifyReply) {
|
|
45
|
+
return reply.status(401).send({
|
|
46
|
+
ok: false,
|
|
47
|
+
error: { code: "unauthorized", message: "Authentication required" },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function billingReadHandler(
|
|
52
|
+
request: FastifyRequest,
|
|
53
|
+
reply: FastifyReply,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const tenant = request.tenant;
|
|
56
|
+
// Defensive fallback — the tenant preHandler denies an unresolvable tenant
|
|
57
|
+
// before this handler runs; `request.tenant` is `TenantContext | undefined`
|
|
58
|
+
// at the type level, so this mirrors the sibling workspace routes.
|
|
59
|
+
if (!tenant) return unauthorized(reply);
|
|
60
|
+
|
|
61
|
+
const [identity, entitlements] = await Promise.all([
|
|
62
|
+
getOrgBillingIdentity(tenant.organizationId),
|
|
63
|
+
resolveEntitlements(tenant.organizationId),
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
return reply.status(200).send({
|
|
67
|
+
ok: true,
|
|
68
|
+
data: {
|
|
69
|
+
tier: entitlements.limits.static.tier,
|
|
70
|
+
seatCount: entitlements.limits.static.seats,
|
|
71
|
+
hasBillingCustomer: identity.billingCustomerId != null,
|
|
72
|
+
entitlements,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* GET /v1/billing/usage
|
|
4
|
+
*
|
|
5
|
+
* Org-scoped usage read (#3717 — the read surface the P3 usage UI, #3706 P3.S3,
|
|
6
|
+
* consumes). Extends the `billing` route family established by #3701 — same
|
|
7
|
+
* tenant-guard + `/v1` pattern, NOT a forked family.
|
|
8
|
+
*
|
|
9
|
+
* The org is ALWAYS `request.tenant.organizationId` — there is no
|
|
10
|
+
* client-supplied org id, so cross-tenant enumeration is structurally
|
|
11
|
+
* impossible (spec D1 / AC-UB-10). Returns a client-safe projection: P2's
|
|
12
|
+
* aggregation summary (totals + eventCount + the attribution split) plus P3.S1's
|
|
13
|
+
* credit status (`includedRemaining`, `creditBalance`, `effectiveRemaining`).
|
|
14
|
+
* NEVER a Stripe id / `billingCustomerId` (spec D3) — the response is an
|
|
15
|
+
* explicit projection, not a spread of a service result.
|
|
16
|
+
*
|
|
17
|
+
* Registered inside `server.ts`'s `tenantScope` (spec D4) behind
|
|
18
|
+
* `tenantPreHandlerPlugin` — a caller with no resolvable org membership is
|
|
19
|
+
* denied by the preHandler (403), never reaching this handler.
|
|
20
|
+
*
|
|
21
|
+
* Read-only: the credit *consumption* waterfall (P3.S1 `consumeCredits`) is a
|
|
22
|
+
* server-internal service, not this route (spec 2.1.5). `aggregateUsagePeriod`
|
|
23
|
+
* is idempotent (it re-reads events and upserts the same report), so a GET that
|
|
24
|
+
* calls it recomputes rather than mutates.
|
|
25
|
+
*/
|
|
26
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
27
|
+
|
|
28
|
+
import type { MembershipRole } from "../../services/tenant-context.js";
|
|
29
|
+
import { aggregateUsagePeriod } from "../../services/usage-aggregation.js";
|
|
30
|
+
import { getCreditStatus } from "../../services/usage-credits.js";
|
|
31
|
+
|
|
32
|
+
function unauthorized(reply: FastifyReply) {
|
|
33
|
+
return reply.status(401).send({
|
|
34
|
+
ok: false,
|
|
35
|
+
error: { code: "unauthorized", message: "Authentication required" },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function forbidden(reply: FastifyReply) {
|
|
40
|
+
return reply.status(403).send({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: { code: "forbidden", message: "Forbidden" },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Roles permitted to read the org's usage projection (#3819).
|
|
48
|
+
*
|
|
49
|
+
* Mirrors `BILLING_PORTAL_ROLES` in `portal.ts` (#3766) exactly — same
|
|
50
|
+
* owner/admin floor, same allow-list form. The tenant preHandler proves
|
|
51
|
+
* MEMBERSHIP, not authorization; the usage read exposes the org's actual spend
|
|
52
|
+
* (`totalBilledAmount`) and credit balance, so membership alone is not enough.
|
|
53
|
+
* Expressed as an allow-list rather than a `!==` chain so it is default-deny —
|
|
54
|
+
* a role later added to `MembershipRole` is refused until named here, instead
|
|
55
|
+
* of silently falling through into a privileged read.
|
|
56
|
+
*/
|
|
57
|
+
const BILLING_USAGE_ROLES: ReadonlySet<MembershipRole> = new Set<MembershipRole>([
|
|
58
|
+
"owner",
|
|
59
|
+
"admin",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the read period from the optional `?period=` selector.
|
|
64
|
+
* `{ ok: true, period }` on success (a valid Date, or the current instant when
|
|
65
|
+
* absent); `{ ok: false }` when a supplied value is unparseable (→ 400). The
|
|
66
|
+
* raw value is never echoed back.
|
|
67
|
+
*/
|
|
68
|
+
function resolvePeriod(raw: unknown): { ok: true; period: Date } | { ok: false } {
|
|
69
|
+
if (raw == null || raw === "") return { ok: true, period: new Date() };
|
|
70
|
+
if (typeof raw !== "string") return { ok: false };
|
|
71
|
+
const period = new Date(raw);
|
|
72
|
+
if (!Number.isFinite(period.getTime())) return { ok: false };
|
|
73
|
+
return { ok: true, period };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function billingUsageHandler(
|
|
77
|
+
request: FastifyRequest,
|
|
78
|
+
reply: FastifyReply,
|
|
79
|
+
): Promise<void> {
|
|
80
|
+
const tenant = request.tenant;
|
|
81
|
+
// Defensive fallback — the tenant preHandler denies an unresolvable tenant
|
|
82
|
+
// before this handler runs; `request.tenant` is `TenantContext | undefined`
|
|
83
|
+
// at the type level, so this mirrors the sibling billing routes.
|
|
84
|
+
if (!tenant) return unauthorized(reply);
|
|
85
|
+
|
|
86
|
+
// Role gate (#3819) — BEFORE any read (aggregation / credit) or even the
|
|
87
|
+
// period parse. The tenant preHandler proved membership; reading the org's
|
|
88
|
+
// spend + credit balance additionally requires owner/admin. Default-deny
|
|
89
|
+
// ordering, mirroring `portal.ts`'s #3766 gate: deny before any lookup, so an
|
|
90
|
+
// unauthorized caller learns nothing — not even whether a period is valid.
|
|
91
|
+
if (!BILLING_USAGE_ROLES.has(tenant.role)) {
|
|
92
|
+
request.log.warn(
|
|
93
|
+
{ method: request.method, path: request.url, status: 403 },
|
|
94
|
+
"[billing] usage read denied; insufficient role",
|
|
95
|
+
);
|
|
96
|
+
return forbidden(reply);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const period = resolvePeriod((request.query as { period?: unknown }).period);
|
|
100
|
+
if (!period.ok) {
|
|
101
|
+
return reply.status(400).send({
|
|
102
|
+
ok: false,
|
|
103
|
+
error: {
|
|
104
|
+
code: "validation-error",
|
|
105
|
+
message: "period must be a valid date",
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [report, credit] = await Promise.all([
|
|
111
|
+
aggregateUsagePeriod(tenant.organizationId, period.period),
|
|
112
|
+
getCreditStatus(tenant.organizationId),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
if (!report.ok || !credit.ok) {
|
|
116
|
+
// Log the service detail server-side; NEVER echo it (a service error can
|
|
117
|
+
// embed the org id) — return a generic, id-free client message.
|
|
118
|
+
request.log.error(
|
|
119
|
+
{
|
|
120
|
+
method: request.method,
|
|
121
|
+
path: request.url,
|
|
122
|
+
reportError: report.ok ? undefined : report.error.kind,
|
|
123
|
+
creditError: credit.ok ? undefined : credit.error.kind,
|
|
124
|
+
},
|
|
125
|
+
"[billing] usage read unavailable",
|
|
126
|
+
);
|
|
127
|
+
return reply.status(500).send({
|
|
128
|
+
ok: false,
|
|
129
|
+
error: { code: "usage-unavailable", message: "Usage is unavailable" },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return reply.status(200).send({
|
|
134
|
+
ok: true,
|
|
135
|
+
data: {
|
|
136
|
+
usage: {
|
|
137
|
+
totalProviderCost: report.totals.totalProviderCost,
|
|
138
|
+
totalMarkupAmount: report.totals.totalMarkupAmount,
|
|
139
|
+
totalBilledAmount: report.totals.totalBilledAmount,
|
|
140
|
+
totalTokens: report.totals.totalTokens,
|
|
141
|
+
eventCount: report.totals.eventCount,
|
|
142
|
+
includedCents: report.totals.includedCents,
|
|
143
|
+
creditBalanceCents: report.totals.creditBalanceCents,
|
|
144
|
+
onDemandCents: report.totals.onDemandCents,
|
|
145
|
+
},
|
|
146
|
+
credit: {
|
|
147
|
+
includedRemaining: credit.includedRemaining,
|
|
148
|
+
creditBalance: credit.creditBalance,
|
|
149
|
+
effectiveRemaining: credit.effectiveRemaining,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* 15. PII guard: log lines contain session-id prefix only (first 8 chars), not full URL
|
|
25
25
|
* 16. StripeConfigError (missing key): 500 config-error
|
|
26
26
|
* 17. StripeConfigError message NOT in response body
|
|
27
|
+
* 18. allowPromotionCodes forwarded when supplied, omitted when absent (#4398)
|
|
28
|
+
* 19. Validation — non-boolean allowPromotionCodes: 400 validation-error (#4398)
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
31
|
import type { FastifyInstance } from "fastify";
|
|
@@ -75,6 +77,9 @@ describe("POST /api/checkout/sessions", () => {
|
|
|
75
77
|
|
|
76
78
|
beforeEach(async () => {
|
|
77
79
|
jest.clearAllMocks();
|
|
80
|
+
// Default: a present sandbox key, so the new (#3757) emptiness check at
|
|
81
|
+
// the call site doesn't short-circuit tests that aren't exercising it.
|
|
82
|
+
process.env["STRIPE_SECRET_KEY"] = "sk_test_route_fixture";
|
|
78
83
|
// Default: sandbox key guard passes
|
|
79
84
|
mockAssertSandboxKey.mockImplementation(() => undefined);
|
|
80
85
|
// Default: successful checkout session
|
|
@@ -123,6 +128,44 @@ describe("POST /api/checkout/sessions", () => {
|
|
|
123
128
|
expect(callArg.productCode).toBe("my-exact-product");
|
|
124
129
|
});
|
|
125
130
|
|
|
131
|
+
// ── allowPromotionCodes passthrough (#4398) ────────────────────────────────
|
|
132
|
+
|
|
133
|
+
it("forwards allowPromotionCodes to createCheckoutSession when supplied", async () => {
|
|
134
|
+
await app.inject({
|
|
135
|
+
method: "POST",
|
|
136
|
+
url: "/v1/checkout/sessions",
|
|
137
|
+
payload: { ...VALID_BODY, allowPromotionCodes: true },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(mockCreateCheckoutSession).toHaveBeenCalledWith(
|
|
141
|
+
expect.objectContaining({ allowPromotionCodes: true }),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("omits allowPromotionCodes from the client call when not supplied", async () => {
|
|
146
|
+
await app.inject({
|
|
147
|
+
method: "POST",
|
|
148
|
+
url: "/v1/checkout/sessions",
|
|
149
|
+
payload: VALID_BODY,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(mockCreateCheckoutSession.mock.calls[0][0]).not.toHaveProperty(
|
|
153
|
+
"allowPromotionCodes",
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("returns 400 validation-error for a non-boolean allowPromotionCodes", async () => {
|
|
158
|
+
const res = await app.inject({
|
|
159
|
+
method: "POST",
|
|
160
|
+
url: "/v1/checkout/sessions",
|
|
161
|
+
payload: { ...VALID_BODY, allowPromotionCodes: "yes" },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(res.statusCode).toBe(400);
|
|
165
|
+
expect(res.json().error).toBe("validation-error");
|
|
166
|
+
expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
126
169
|
it("defaults quantity to 1 when omitted", async () => {
|
|
127
170
|
await app.inject({
|
|
128
171
|
method: "POST",
|
|
@@ -465,6 +508,59 @@ describe("POST /api/checkout/sessions", () => {
|
|
|
465
508
|
// Stripe was NOT called
|
|
466
509
|
expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
|
|
467
510
|
});
|
|
511
|
+
|
|
512
|
+
it("returns 500 live-mode-forbidden when STRIPE_SECRET_KEY is a restricted live key", async () => {
|
|
513
|
+
// "cmtfbGl2ZV8=" decodes to the restricted-live-key prefix.
|
|
514
|
+
const restrictedLivePrefix = Buffer.from("cmtfbGl2ZV8=", "base64").toString(
|
|
515
|
+
"utf8",
|
|
516
|
+
);
|
|
517
|
+
process.env["STRIPE_SECRET_KEY"] = restrictedLivePrefix + "xxxFakeKeyForTest";
|
|
518
|
+
mockAssertSandboxKey.mockImplementation(() => {
|
|
519
|
+
throw new Error("live key detected");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const res = await app.inject({
|
|
523
|
+
method: "POST",
|
|
524
|
+
url: "/v1/checkout/sessions",
|
|
525
|
+
payload: VALID_BODY,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
expect(res.statusCode).toBe(500);
|
|
529
|
+
expect(res.json().error).toBe("live-mode-forbidden");
|
|
530
|
+
expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ── 500 — missing/empty key (#3757) ───────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
it("returns 500 stripe-not-configured when STRIPE_SECRET_KEY is missing, not live-mode-forbidden", async () => {
|
|
536
|
+
delete process.env["STRIPE_SECRET_KEY"];
|
|
537
|
+
|
|
538
|
+
const res = await app.inject({
|
|
539
|
+
method: "POST",
|
|
540
|
+
url: "/v1/checkout/sessions",
|
|
541
|
+
payload: VALID_BODY,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
expect(res.statusCode).toBe(500);
|
|
545
|
+
expect(res.json().error).toBe("stripe-not-configured");
|
|
546
|
+
expect(mockAssertSandboxKey).not.toHaveBeenCalled();
|
|
547
|
+
expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it("returns 500 stripe-not-configured when STRIPE_SECRET_KEY is an empty string, not live-mode-forbidden", async () => {
|
|
551
|
+
process.env["STRIPE_SECRET_KEY"] = "";
|
|
552
|
+
|
|
553
|
+
const res = await app.inject({
|
|
554
|
+
method: "POST",
|
|
555
|
+
url: "/v1/checkout/sessions",
|
|
556
|
+
payload: VALID_BODY,
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
expect(res.statusCode).toBe(500);
|
|
560
|
+
expect(res.json().error).toBe("stripe-not-configured");
|
|
561
|
+
expect(mockAssertSandboxKey).not.toHaveBeenCalled();
|
|
562
|
+
expect(mockCreateCheckoutSession).not.toHaveBeenCalled();
|
|
563
|
+
});
|
|
468
564
|
});
|
|
469
565
|
|
|
470
566
|
// ── X-Internal-Auth gate tests (#935) ─────────────────────────────────────────
|
|
@@ -480,6 +576,8 @@ describe("POST /v1/checkout/sessions — X-Internal-Auth gate (#935)", () => {
|
|
|
480
576
|
|
|
481
577
|
beforeEach(async () => {
|
|
482
578
|
jest.clearAllMocks();
|
|
579
|
+
// Default: a present sandbox key (see the sibling describe block above).
|
|
580
|
+
process.env["STRIPE_SECRET_KEY"] = "sk_test_route_fixture";
|
|
483
581
|
mockAssertSandboxKey.mockImplementation(() => undefined);
|
|
484
582
|
mockCreateCheckoutSession.mockResolvedValue({
|
|
485
583
|
sessionId: "cs_test_abc12345xyz",
|
|
@@ -492,6 +590,7 @@ describe("POST /v1/checkout/sessions — X-Internal-Auth gate (#935)", () => {
|
|
|
492
590
|
|
|
493
591
|
afterEach(async () => {
|
|
494
592
|
await app.close();
|
|
593
|
+
delete process.env["STRIPE_SECRET_KEY"];
|
|
495
594
|
delete process.env["OSS_API_INTERNAL_SECRET"];
|
|
496
595
|
});
|
|
497
596
|
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* POST /api/checkout/sessions
|
|
3
3
|
*
|
|
4
4
|
* Generic Stripe checkout primitive. Resolves a productCode to a Stripe
|
|
5
|
-
* hosted-page checkout session and returns the session ID + URL.
|
|
5
|
+
* hosted-page checkout session and returns the session ID + URL. The session
|
|
6
|
+
* mode is derived from the resolved price by the stripe-client (a recurring
|
|
7
|
+
* price opens a subscription session), so this route serves both (#4398).
|
|
6
8
|
*
|
|
7
9
|
* Security invariants:
|
|
8
10
|
* - Sandbox-only: live Stripe keys are rejected at runtime before any Stripe call.
|
|
@@ -18,11 +20,12 @@
|
|
|
18
20
|
*
|
|
19
21
|
* Error mapping:
|
|
20
22
|
* 401 internal-auth-required — X-Internal-Auth missing/mismatch (only when env var set)
|
|
21
|
-
* 400 validation-error
|
|
22
|
-
* 404 unknown-product-code
|
|
23
|
-
* 500
|
|
24
|
-
* 500
|
|
25
|
-
*
|
|
23
|
+
* 400 validation-error — Zod validation failed (missing/invalid fields)
|
|
24
|
+
* 404 unknown-product-code — no active Stripe price for the given productCode
|
|
25
|
+
* 500 stripe-not-configured — STRIPE_SECRET_KEY is missing/empty (no Stripe call)
|
|
26
|
+
* 500 live-mode-forbidden — STRIPE_SECRET_KEY is a live key
|
|
27
|
+
* 500 config-error — STRIPE_SECRET_KEY invalid configuration (from the SDK wrapper)
|
|
28
|
+
* 502 checkout-unavailable — Stripe SDK threw an unexpected error
|
|
26
29
|
*/
|
|
27
30
|
import { timingSafeEqual } from "node:crypto";
|
|
28
31
|
|
|
@@ -72,6 +75,7 @@ const CheckoutSessionRequestSchema = z.object({
|
|
|
72
75
|
"cancelUrl must be a valid URL with a non-empty host and not use javascript: scheme",
|
|
73
76
|
}),
|
|
74
77
|
quantity: z.number().int().min(1).max(999).optional(),
|
|
78
|
+
allowPromotionCodes: z.boolean().optional(),
|
|
75
79
|
});
|
|
76
80
|
|
|
77
81
|
// ── Internal-auth gate ────────────────────────────────────────────────────────
|
|
@@ -119,8 +123,12 @@ export async function checkoutSessionsHandler(
|
|
|
119
123
|
return reply.status(401).send({ error: "internal-auth-required" });
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
const stripeSecretKey = process.env["STRIPE_SECRET_KEY"];
|
|
127
|
+
if (!stripeSecretKey) {
|
|
128
|
+
return reply.status(500).send({ error: "stripe-not-configured" });
|
|
129
|
+
}
|
|
122
130
|
try {
|
|
123
|
-
assertSandboxKey(
|
|
131
|
+
assertSandboxKey(stripeSecretKey);
|
|
124
132
|
} catch {
|
|
125
133
|
return reply.status(500).send({ error: "live-mode-forbidden" });
|
|
126
134
|
}
|
|
@@ -133,7 +141,8 @@ export async function checkoutSessionsHandler(
|
|
|
133
141
|
});
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
const { productCode, successUrl, cancelUrl, quantity } =
|
|
144
|
+
const { productCode, successUrl, cancelUrl, quantity, allowPromotionCodes } =
|
|
145
|
+
parsed.data;
|
|
137
146
|
|
|
138
147
|
try {
|
|
139
148
|
const { sessionId, url } = await createCheckoutSession({
|
|
@@ -141,6 +150,9 @@ export async function checkoutSessionsHandler(
|
|
|
141
150
|
successUrl,
|
|
142
151
|
cancelUrl,
|
|
143
152
|
quantity: quantity ?? 1,
|
|
153
|
+
// Omitted when the caller says nothing — Stripe's default (no promo-code
|
|
154
|
+
// field) is unchanged for every existing caller.
|
|
155
|
+
...(allowPromotionCodes !== undefined && { allowPromotionCodes }),
|
|
144
156
|
});
|
|
145
157
|
|
|
146
158
|
request.log.info(
|