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,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription routes — instantiated by `scaffold-commerce`'s
|
|
3
|
+
* surface-selection branch (SKILL.md § Step 10h, FEAT-023 U5) into the
|
|
4
|
+
* consumer's own `apps/api`. Lands at, e.g.,
|
|
5
|
+
* `apps/api/src/routes/commerce/subscriptions.ts`. TWO handlers with
|
|
6
|
+
* DELIBERATELY OPPOSITE auth postures — do not register them in the same
|
|
7
|
+
* scope:
|
|
8
|
+
*
|
|
9
|
+
* 1. `subscribeHandler` — a PUBLIC, unauthenticated "subscribe" action.
|
|
10
|
+
* Mirrors `checkout-route.template.ts` exactly (this surface reuses
|
|
11
|
+
* `createCheckoutSession()`, pinned `mode: "subscription"` — spec §3
|
|
12
|
+
* design decision: no parallel subscription client module).
|
|
13
|
+
* 2. `subscriptionRoutes` — an ADMIN + TENANT-GUARDED Fastify plugin
|
|
14
|
+
* exposing the subscription-status READ. Mirrors
|
|
15
|
+
* `invoice-route.template.ts`'s `requireSiteAdmin` +
|
|
16
|
+
* `requireSingleTenantScope` guards verbatim (spec §5.3 escalation
|
|
17
|
+
* boundary: "retrieve-on-view is the v1 default, mirroring U4" — no
|
|
18
|
+
* subscription webhook is scaffolded at v1).
|
|
19
|
+
*
|
|
20
|
+
* Registration (BOTH required, in DIFFERENT scopes):
|
|
21
|
+
*
|
|
22
|
+
* // 1. subscribeHandler — public, rate-limited (mirrors checkout):
|
|
23
|
+
* await fastify.register(async (subscribeScope) => {
|
|
24
|
+
* subscribeScope.register(rateLimitMiddleware); // apps/api/src/middleware/rate-limit.ts
|
|
25
|
+
* subscribeScope.post("/api/subscriptions/subscribe", subscribeHandler);
|
|
26
|
+
* });
|
|
27
|
+
*
|
|
28
|
+
* // 2. subscriptionRoutes — tenant-guarded admin read (mirrors invoices):
|
|
29
|
+
* await tenantScope.register(subscriptionRoutes);
|
|
30
|
+
*
|
|
31
|
+
* ── Auth posture — split by handler, on purpose ─────────────────────────────
|
|
32
|
+
* `subscribeHandler` is a storefront "subscribe" action — the recurring-price
|
|
33
|
+
* analog of `checkout-route.template.ts`'s one-time "buy" action. It is
|
|
34
|
+
* intentionally UNAUTHENTICATED and does exactly ONE thing: create a NEW
|
|
35
|
+
* Stripe Checkout Session (`mode: "subscription"`, pinned — never
|
|
36
|
+
* caller-overridable) for ONE OF THE RECURRING PRICES this surface was
|
|
37
|
+
* scaffolded to sell (`ALLOWED_LOOKUP_KEYS`, enforced server-side before any
|
|
38
|
+
* Stripe call). It has NO list/read of existing subscriptions, no
|
|
39
|
+
* update/cancel — see `subscriptionRoutes` below for that, and see the
|
|
40
|
+
* "point management at the U6 portal" design decision (spec §3): this surface
|
|
41
|
+
* never builds self-service cancel/change, it only shows state + a portal
|
|
42
|
+
* link (`packages/ui`'s `SubscriptionStatus`).
|
|
43
|
+
*
|
|
44
|
+
* `subscriptionRoutes` READS an existing subscription's status — a
|
|
45
|
+
* MANAGEMENT-shaped action, so it is authenticated + site-admin + tenant
|
|
46
|
+
* guarded, the OPPOSITE of `subscribeHandler`'s public posture. Do not copy
|
|
47
|
+
* `subscribeHandler`'s public registration for it.
|
|
48
|
+
*
|
|
49
|
+
* ── Entitlement is NOT authorization (repeats the AILK#3744 lesson) ─────────
|
|
50
|
+
* `assertCommerceEntitled()`/`hasFeature("commerce")` answers "does this
|
|
51
|
+
* DEPLOYMENT have commerce?", never "is this caller allowed to do this?" — it
|
|
52
|
+
* is a SCAFFOLD-TIME check (this file is not emitted without
|
|
53
|
+
* `feature:commerce`); there is deliberately no runtime entitlement call
|
|
54
|
+
* here, and none is claimed for either handler.
|
|
55
|
+
*
|
|
56
|
+
* ── v1 tenancy limitation (mirrors invoice-route.template.ts verbatim) ──────
|
|
57
|
+
* `loadStripeSurfaceConfig()` resolves ONE deployment-wide
|
|
58
|
+
* `STRIPE_SECRET_KEY` and v1 has NO per-organization Stripe customer
|
|
59
|
+
* namespace (see `docs/research/commerce-template-audit/subscriptions.md`).
|
|
60
|
+
* A subscription therefore carries no org attribution, so on a multi-tenant
|
|
61
|
+
* deployment there is no honest way to decide whether a caller-supplied
|
|
62
|
+
* `subscriptionId` belongs to the caller's org. `requireSingleTenantScope`
|
|
63
|
+
* admits ONLY the deployment's own operator org (the fixed single-tenant
|
|
64
|
+
* sentinel `DEFAULT_ORG_ID`) and REFUSES every tenant org's admin outright —
|
|
65
|
+
* subscription-status reading is an OPERATOR surface at v1, not a per-tenant
|
|
66
|
+
* self-service one. Lifting the refusal needs a per-org Stripe Connect /
|
|
67
|
+
* customer-namespace model (tracked follow-up, not silently claimed fixed).
|
|
68
|
+
*
|
|
69
|
+
* TEST-MODE ONLY (v1), and the live-key guard has no remaining gap.
|
|
70
|
+
* `assertSandboxKey` (`packages/stripe-client/src/client.ts`) is an ALLOW-LIST
|
|
71
|
+
* as of AILK#3734: it accepts only `sk_test_`, `pk_test_` and `rk_test_` and
|
|
72
|
+
* refuses everything else, so a restricted live key and any future live prefix
|
|
73
|
+
* are refused by construction. It is a shared file outside this template's
|
|
74
|
+
* scope; both handlers below inherit that coverage via
|
|
75
|
+
* `loadStripeSurfaceConfig()` + `getStripeClient()`, never a second/divergent
|
|
76
|
+
* prefix check.
|
|
77
|
+
*/
|
|
78
|
+
import { DEFAULT_ORG_ID } from "@working-theory/database/tenancy";
|
|
79
|
+
import type {
|
|
80
|
+
FastifyPluginAsync,
|
|
81
|
+
FastifyReply,
|
|
82
|
+
FastifyRequest,
|
|
83
|
+
} from "fastify";
|
|
84
|
+
import { z } from "zod";
|
|
85
|
+
|
|
86
|
+
import {
|
|
87
|
+
createCheckoutSession,
|
|
88
|
+
retrieveSubscriptionStatus,
|
|
89
|
+
StripeConfigError,
|
|
90
|
+
UnknownProductCodeError,
|
|
91
|
+
} from "@working-theory/stripe-client";
|
|
92
|
+
|
|
93
|
+
import { prisma } from "../../lib/prisma.js";
|
|
94
|
+
import { loadStripeSurfaceConfig } from "./stripe-config.js";
|
|
95
|
+
|
|
96
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
97
|
+
// 1. subscribeHandler — PUBLIC "subscribe" action (mirrors checkout-route)
|
|
98
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
99
|
+
|
|
100
|
+
// ── Purchasable-plan allowlist — SCAFFOLD-TIME EDIT ───────────────────────────
|
|
101
|
+
//
|
|
102
|
+
// The ONLY recurring lookup_keys this surface is allowed to sell. Without
|
|
103
|
+
// this allowlist the route resolves a caller-supplied `lookupKey` against
|
|
104
|
+
// the merchant's ENTIRE Stripe account — the exact one-time-checkout hole
|
|
105
|
+
// closed in PR #3733, applied here for the subscription surface. Step 10h
|
|
106
|
+
// replaces the placeholder below with the recurring lookup_key(s) the
|
|
107
|
+
// consumer selected. Until it does, EVERY subscribe request 404s.
|
|
108
|
+
const ALLOWED_LOOKUP_KEYS: ReadonlySet<string> = new Set<string>([
|
|
109
|
+
"REPLACE_WITH_YOUR_RECURRING_STRIPE_LOOKUP_KEY",
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const LOOKUP_KEY_REGEX = /^[A-Za-z0-9._-]{1,200}$/;
|
|
113
|
+
const REDIRECT_MAX_LENGTH = 2048;
|
|
114
|
+
|
|
115
|
+
const SubscribeRequestSchema = z.object({
|
|
116
|
+
lookupKey: z
|
|
117
|
+
.string()
|
|
118
|
+
.regex(LOOKUP_KEY_REGEX, "lookupKey must match /^[A-Za-z0-9._-]{1,200}$/"),
|
|
119
|
+
successUrl: z
|
|
120
|
+
.string()
|
|
121
|
+
.min(1, "successUrl is required")
|
|
122
|
+
.max(REDIRECT_MAX_LENGTH, "successUrl is too long"),
|
|
123
|
+
cancelUrl: z
|
|
124
|
+
.string()
|
|
125
|
+
.min(1, "cancelUrl is required")
|
|
126
|
+
.max(REDIRECT_MAX_LENGTH, "cancelUrl is too long"),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/** Site's own origin — the ONLY origin a checkout redirect may resolve to (mirrors checkout-route.template.ts). */
|
|
130
|
+
function loadSiteOrigin(): URL {
|
|
131
|
+
const raw = process.env.FRONTEND_URL;
|
|
132
|
+
if (!raw) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"FRONTEND_URL is missing. It is the site origin checkout redirects are locked to.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return new URL(raw);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error("FRONTEND_URL is not a valid absolute URL.");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Resolves a caller-supplied redirect against the site's own origin, or null (mirrors checkout-route.template.ts). */
|
|
145
|
+
function toSameOriginRedirect(raw: string, siteOrigin: URL): string | null {
|
|
146
|
+
let resolved: URL;
|
|
147
|
+
try {
|
|
148
|
+
resolved = new URL(raw, siteOrigin);
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (resolved.protocol !== "https:" && resolved.protocol !== "http:") {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (resolved.origin !== siteOrigin.origin) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return resolved.toString();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* POST handler — creates a test-mode SUBSCRIPTION (`mode: "subscription"`,
|
|
163
|
+
* pinned) Checkout Session for `lookupKey` and returns `{ sessionId, url }`.
|
|
164
|
+
* The caller (the scaffolded `PricingTable` section's `onSubscribe`)
|
|
165
|
+
* redirects the browser to `url` on success.
|
|
166
|
+
*
|
|
167
|
+
* @public-by-design — a storefront "subscribe" action, intentionally
|
|
168
|
+
* UNAUTHENTICATED (the recurring-price analog of checkout's public "buy"; see
|
|
169
|
+
* the file-header "Auth posture" note). It creates ONE allowlisted
|
|
170
|
+
* subscription Checkout Session and reads/mutates nothing else. Its ADMIN
|
|
171
|
+
* sibling `subscriptionRoutes` below is identity + tenant guarded — the marker
|
|
172
|
+
* exempts ONLY this handler (route-template-auth-gate, #3751), never the file.
|
|
173
|
+
*/
|
|
174
|
+
export async function subscribeHandler(
|
|
175
|
+
request: FastifyRequest,
|
|
176
|
+
reply: FastifyReply,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
let siteOrigin: URL;
|
|
179
|
+
try {
|
|
180
|
+
if (!process.env.STRIPE_SECRET_KEY || !process.env.STRIPE_PUBLISHABLE_KEY) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"STRIPE_SECRET_KEY / STRIPE_PUBLISHABLE_KEY are missing. Run scaffold-commerce (direct payments path) first.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
siteOrigin = loadSiteOrigin();
|
|
186
|
+
loadStripeSurfaceConfig();
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err instanceof StripeConfigError) {
|
|
189
|
+
request.log.error(
|
|
190
|
+
{ method: request.method, path: request.url, message: err.message },
|
|
191
|
+
"[subscriptions] subscribe: LIVE Stripe key REFUSED — live-mode-forbidden",
|
|
192
|
+
);
|
|
193
|
+
return reply.status(500).send({ error: "live-mode-forbidden" });
|
|
194
|
+
}
|
|
195
|
+
request.log.error(
|
|
196
|
+
{
|
|
197
|
+
method: request.method,
|
|
198
|
+
path: request.url,
|
|
199
|
+
message: err instanceof Error ? err.message : "unknown",
|
|
200
|
+
},
|
|
201
|
+
"[subscriptions] subscribe: config error",
|
|
202
|
+
);
|
|
203
|
+
return reply.status(500).send({ error: "config-error" });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const parsed = SubscribeRequestSchema.safeParse(request.body);
|
|
207
|
+
if (!parsed.success) {
|
|
208
|
+
return reply.status(400).send({
|
|
209
|
+
error: "validation-error",
|
|
210
|
+
details: parsed.error.issues,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const { lookupKey, successUrl, cancelUrl } = parsed.data;
|
|
215
|
+
|
|
216
|
+
if (!ALLOWED_LOOKUP_KEYS.has(lookupKey)) {
|
|
217
|
+
request.log.warn(
|
|
218
|
+
{ method: request.method, path: request.url, status: 404 },
|
|
219
|
+
"[subscriptions] subscribe: lookupKey is not in this surface's allowlist; refused",
|
|
220
|
+
);
|
|
221
|
+
return reply.status(404).send({ error: "unknown-product-code" });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const safeSuccessUrl = toSameOriginRedirect(successUrl, siteOrigin);
|
|
225
|
+
const safeCancelUrl = toSameOriginRedirect(cancelUrl, siteOrigin);
|
|
226
|
+
if (safeSuccessUrl === null || safeCancelUrl === null) {
|
|
227
|
+
request.log.warn(
|
|
228
|
+
{ method: request.method, path: request.url, status: 400 },
|
|
229
|
+
"[subscriptions] subscribe: redirect URL is not same-origin; refused",
|
|
230
|
+
);
|
|
231
|
+
return reply.status(400).send({ error: "invalid-redirect-url" });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// Pinned mode: "subscription" — this endpoint can never open a one-time
|
|
236
|
+
// checkout session, regardless of caller input (spec §3 design decision).
|
|
237
|
+
const { sessionId, url } = await createCheckoutSession({
|
|
238
|
+
productCode: lookupKey,
|
|
239
|
+
successUrl: safeSuccessUrl,
|
|
240
|
+
cancelUrl: safeCancelUrl,
|
|
241
|
+
mode: "subscription",
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
request.log.info(
|
|
245
|
+
{
|
|
246
|
+
method: request.method,
|
|
247
|
+
path: request.url,
|
|
248
|
+
sessionIdPrefix: sessionId.substring(0, 8),
|
|
249
|
+
status: 201,
|
|
250
|
+
},
|
|
251
|
+
"[subscriptions] subscribe: session created",
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
return reply.status(201).send({ sessionId, url });
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (err instanceof UnknownProductCodeError) {
|
|
257
|
+
return reply.status(404).send({ error: "unknown-product-code" });
|
|
258
|
+
}
|
|
259
|
+
if (err instanceof StripeConfigError) {
|
|
260
|
+
request.log.error(
|
|
261
|
+
{
|
|
262
|
+
method: request.method,
|
|
263
|
+
path: request.url,
|
|
264
|
+
errName: err.name,
|
|
265
|
+
message: err.message,
|
|
266
|
+
},
|
|
267
|
+
"[subscriptions] subscribe: config error",
|
|
268
|
+
);
|
|
269
|
+
return reply.status(500).send({ error: "config-error" });
|
|
270
|
+
}
|
|
271
|
+
request.log.error(
|
|
272
|
+
{
|
|
273
|
+
method: request.method,
|
|
274
|
+
path: request.url,
|
|
275
|
+
errName: err instanceof Error ? err.name : "unknown",
|
|
276
|
+
},
|
|
277
|
+
"[subscriptions] subscribe: stripe error; checkout-unavailable",
|
|
278
|
+
);
|
|
279
|
+
return reply.status(502).send({ error: "checkout-unavailable" });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
284
|
+
// 2. subscriptionRoutes — ADMIN + TENANT-GUARDED status read (mirrors
|
|
285
|
+
// invoice-route.template.ts's guards verbatim)
|
|
286
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
287
|
+
|
|
288
|
+
function unauthorized(reply: FastifyReply): void {
|
|
289
|
+
reply.status(401).send({
|
|
290
|
+
ok: false,
|
|
291
|
+
error: { code: "unauthorized", message: "Authentication required." },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function forbidden(reply: FastifyReply): void {
|
|
296
|
+
reply.status(403).send({
|
|
297
|
+
ok: false,
|
|
298
|
+
error: { code: "forbidden", message: "Admin access required." },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Identical for EVERY request — leaks no information about which subscriptions exist (no enumeration oracle). */
|
|
303
|
+
function tenancyUnsupported(reply: FastifyReply): void {
|
|
304
|
+
reply.status(403).send({
|
|
305
|
+
ok: false,
|
|
306
|
+
error: {
|
|
307
|
+
code: "tenancy_unsupported",
|
|
308
|
+
message:
|
|
309
|
+
"Subscription-status reading is unavailable on a multi-tenant deployment: v1 has no per-organization Stripe customer namespace, so a subscription cannot be attributed to an organization.",
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function invalidBody(reply: FastifyReply): void {
|
|
315
|
+
reply.status(400).send({
|
|
316
|
+
ok: false,
|
|
317
|
+
error: { code: "invalid_body", message: "Invalid request." },
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function serverError(reply: FastifyReply): void {
|
|
322
|
+
// Never echo the raw Stripe/internal error — it may embed request params.
|
|
323
|
+
reply.status(500).send({
|
|
324
|
+
ok: false,
|
|
325
|
+
error: { code: "internal_error", message: "Something went wrong." },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The site-admin role set. `User.role` is a per-deployment String
|
|
331
|
+
* ("user" | "admin" | "owner") — DISTINCT from the org-membership `Role` enum
|
|
332
|
+
* the multi-tenant module uses. Named rather than compared inline so the next
|
|
333
|
+
* role change is one greppable edit per template; mirrored from
|
|
334
|
+
* `payment-link-route.template.ts:135` because these are copied-not-imported
|
|
335
|
+
* scaffold sources with nowhere to share a constant (AILK#3765).
|
|
336
|
+
*/
|
|
337
|
+
const SITE_ADMIN_ROLES: ReadonlySet<string> = new Set(["admin", "owner"]);
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Gate 1 — requires an authenticated, site-admin caller. `request.userId` is
|
|
341
|
+
* populated by `authPreHandlerPlugin` (see the Registration note above); this
|
|
342
|
+
* re-checks it defensively. Sends the 401/403 itself and returns `null` on
|
|
343
|
+
* denial; the caller's handler must `return` immediately when this returns
|
|
344
|
+
* `null`.
|
|
345
|
+
*/
|
|
346
|
+
async function requireSiteAdmin(
|
|
347
|
+
request: FastifyRequest,
|
|
348
|
+
reply: FastifyReply,
|
|
349
|
+
): Promise<string | null> {
|
|
350
|
+
const userId = request.userId;
|
|
351
|
+
if (!userId) {
|
|
352
|
+
unauthorized(reply);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let role: string | undefined;
|
|
357
|
+
try {
|
|
358
|
+
const user = await prisma.user.findUnique({
|
|
359
|
+
where: { id: userId },
|
|
360
|
+
select: { role: true },
|
|
361
|
+
});
|
|
362
|
+
role = user?.role;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
// FAIL CLOSED. Never `catch {}` into the happy path.
|
|
365
|
+
//
|
|
366
|
+
// Unwrapped, a rejection here escapes to Fastify's default error handler,
|
|
367
|
+
// which serializes `error.message` back to the caller — contradicting this
|
|
368
|
+
// file's own no-raw-echo contract. It always denied; it denied while
|
|
369
|
+
// leaking.
|
|
370
|
+
request.log.error(
|
|
371
|
+
{
|
|
372
|
+
method: request.method,
|
|
373
|
+
path: request.url,
|
|
374
|
+
message: err instanceof Error ? err.message : "unknown",
|
|
375
|
+
},
|
|
376
|
+
"[subscriptions] role resolution failed; DENYING",
|
|
377
|
+
);
|
|
378
|
+
serverError(reply);
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// A missing user row (deleted account, stale session) has no role -> denied.
|
|
383
|
+
if (!role || !SITE_ADMIN_ROLES.has(role)) {
|
|
384
|
+
forbidden(reply);
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return userId;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Gate 2 — the tenancy guard. v1 resolves ONE deployment-wide
|
|
393
|
+
* `STRIPE_SECRET_KEY` and has no per-org Stripe customer namespace, so a
|
|
394
|
+
* subscription carries no organization attribution — this guard admits ONLY
|
|
395
|
+
* the deployment's own operator org (the fixed single-tenant sentinel
|
|
396
|
+
* `DEFAULT_ORG_ID`) and refuses everyone else, before any Stripe call.
|
|
397
|
+
* Mirrors `invoice-route.template.ts`'s `requireSingleTenantScope` verbatim.
|
|
398
|
+
*/
|
|
399
|
+
async function requireSingleTenantScope(
|
|
400
|
+
request: FastifyRequest,
|
|
401
|
+
reply: FastifyReply,
|
|
402
|
+
): Promise<boolean> {
|
|
403
|
+
const tenant = request.tenant;
|
|
404
|
+
if (!tenant) {
|
|
405
|
+
forbidden(reply);
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (tenant.organizationId !== DEFAULT_ORG_ID) {
|
|
410
|
+
tenancyUnsupported(reply);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const statusQuerySchema = z.object({
|
|
418
|
+
subscriptionId: z.string().min(1).max(255),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* `subscriptionRoutes` — a single GET endpoint: retrieve-on-view by the
|
|
423
|
+
* subscription's own Stripe id (spec §5.3 escalation boundary — no
|
|
424
|
+
* subscription webhook is scaffolded at v1; mirrors
|
|
425
|
+
* `invoice-route.template.ts`'s `?invoiceId=` shape).
|
|
426
|
+
*/
|
|
427
|
+
export const subscriptionRoutes: FastifyPluginAsync = async (fastify) => {
|
|
428
|
+
// GET /v1/commerce/subscriptions?subscriptionId=sub_...
|
|
429
|
+
fastify.get("/v1/commerce/subscriptions", async (request, reply) => {
|
|
430
|
+
if (!(await requireSiteAdmin(request, reply))) return;
|
|
431
|
+
if (!(await requireSingleTenantScope(request, reply))) return;
|
|
432
|
+
|
|
433
|
+
const query = statusQuerySchema.safeParse(request.query);
|
|
434
|
+
if (!query.success) return invalidBody(reply);
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
loadStripeSurfaceConfig();
|
|
438
|
+
const status = await retrieveSubscriptionStatus({
|
|
439
|
+
subscriptionId: query.data.subscriptionId,
|
|
440
|
+
});
|
|
441
|
+
return reply.send({ ok: true, data: status });
|
|
442
|
+
} catch {
|
|
443
|
+
return serverError(reply);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
};
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The 10 business categories `suggest-site-pages` knows about, with the
|
|
4
4
|
ordered page list and primary schema.org type for each page. Schemas
|
|
5
|
-
are drawn from the
|
|
6
|
-
file should reference a type the package can't generate.
|
|
5
|
+
are drawn from the 60 page types `@working-theory/schema` builds; nothing in
|
|
6
|
+
this file should reference a type the package can't generate.
|
|
7
7
|
|
|
8
8
|
## How to read this file
|
|
9
9
|
|
|
@@ -275,7 +275,7 @@ operates as an entity.
|
|
|
275
275
|
|
|
276
276
|
Every `Schema.orgType` referenced above is a member of the
|
|
277
277
|
`SchemaOrgType` union exported by `@working-theory/schema` and has a builder
|
|
278
|
-
function. The v1 catalog uses 21 of the
|
|
278
|
+
function. The v1 catalog uses 21 of the 60 supported types:
|
|
279
279
|
|
|
280
280
|
`HomePage`, `AboutPage`, `ContactPage`, `PrivacyPage`, `TermsPage`,
|
|
281
281
|
`Organization`, `Person`, `ServicesList`, `Service`, `ProductsList`,
|
|
@@ -283,5 +283,5 @@ function. The v1 catalog uses 21 of the 45 supported types:
|
|
|
283
283
|
`EventsList`, `CoursesList`, `Course`, `BlogList`, `BlogPosting`,
|
|
284
284
|
`CaseStudiesList`.
|
|
285
285
|
|
|
286
|
-
The remaining
|
|
286
|
+
The remaining 39 types are available to `scaffold-page` and `add-schema`
|
|
287
287
|
for per-page use but don't drive category recommendations.
|
package/templates/.env.example
CHANGED
|
@@ -50,8 +50,67 @@ CONTENT_BACKEND=mdx
|
|
|
50
50
|
# BETTER_AUTH_URL=http://localhost:3000
|
|
51
51
|
# BETTER_AUTH_SECRET=
|
|
52
52
|
|
|
53
|
+
# ── Single-tenant operator pin (multi-tenant module, apps/api) ───────────────
|
|
54
|
+
# On a single-tenant deployment (the default Workspace exists and is the ONLY
|
|
55
|
+
# Workspace) the FIRST authenticated user to resolve a tenant context is
|
|
56
|
+
# bound as the sole owner (TOFU — trust on first use); every subsequent
|
|
57
|
+
# caller is denied. AILK_OPERATOR_EMAIL is OPTIONAL and pins that binding to
|
|
58
|
+
# one email address instead of "whoever logs in first" — set it when you
|
|
59
|
+
# want to guarantee only a specific operator account can ever become owner.
|
|
60
|
+
# Case-insensitive, trimmed. Leave unset for pure TOFU.
|
|
61
|
+
# AILK_OPERATOR_EMAIL=
|
|
62
|
+
#
|
|
63
|
+
# Belt-and-suspenders: the owner-pin denies any non-operator account at
|
|
64
|
+
# tenant resolution regardless, but for a single-tenant deployment you should
|
|
65
|
+
# also disable open sign-up / configure an OAuth allowlist on your identity
|
|
66
|
+
# provider (or Better Auth's `disableSignUp` option, if you wire it in) so a
|
|
67
|
+
# random account can't even authenticate in the first place.
|
|
68
|
+
|
|
53
69
|
# ── Email provider (Resend default) ──────────────────────────────────────────
|
|
54
70
|
# RESEND_API_KEY=
|
|
71
|
+
#
|
|
72
|
+
# Lead capture's owner-notification email — read by lead-routing.ts. Both
|
|
73
|
+
# must be set for the operator notification to send; either unset skips it.
|
|
74
|
+
# AILK_LEAD_NOTIFICATION_TO=
|
|
75
|
+
# AILK_LEAD_NOTIFICATION_FROM=
|
|
76
|
+
#
|
|
77
|
+
# Abandon-email's lead-facing sender (#3962). Optional — falls back to
|
|
78
|
+
# AILK_LEAD_NOTIFICATION_FROM when unset. Only needed for flows whose config
|
|
79
|
+
# declares an `abandonEmail` block with `mode:'enabled'`; `mode:'dark'` never
|
|
80
|
+
# sends and needs no sender.
|
|
81
|
+
# AILK_ABANDON_EMAIL_FROM=
|
|
82
|
+
#
|
|
83
|
+
# Deliverable-delivery's lead-facing sender (#3961). Optional — falls back to
|
|
84
|
+
# AILK_LEAD_NOTIFICATION_FROM when unset. Only needed for flows whose config
|
|
85
|
+
# declares a `deliverables` block; unset means the grant still mints but the
|
|
86
|
+
# email send is skipped (log-and-skip, never a lost grant).
|
|
87
|
+
# AILK_DELIVERABLE_EMAIL_FROM=
|
|
88
|
+
#
|
|
89
|
+
# Waitlist follow-up sequence's lead-facing sender (#4732). Optional — falls
|
|
90
|
+
# back to AILK_LEAD_NOTIFICATION_FROM when unset. Only needed for flows whose
|
|
91
|
+
# config declares a `followUps` block.
|
|
92
|
+
# AILK_FOLLOWUP_EMAIL_FROM=
|
|
93
|
+
#
|
|
94
|
+
# The Resend Contacts audience the #4732 follow-up sweep checks before
|
|
95
|
+
# sending — a lead found unsubscribed in this audience is suppressed.
|
|
96
|
+
# Optional: unset degrades to "cannot verify — proceed" (never to "block all
|
|
97
|
+
# sends"), since nothing in this repo syncs leads into a Resend audience by
|
|
98
|
+
# default.
|
|
99
|
+
# RESEND_AUDIENCE_ID=
|
|
100
|
+
#
|
|
101
|
+
# The public base URL this API is reachable at — used ONLY to build the
|
|
102
|
+
# deliverable-delivery download link (#3961 D5):
|
|
103
|
+
# `${AILK_PUBLIC_API_URL}/v1/deliverables/:grantId`. Unset means a relative
|
|
104
|
+
# link (base ""), which is not usable from an email client — set this in any
|
|
105
|
+
# deployment that uses the `deliverables` block.
|
|
106
|
+
# AILK_PUBLIC_API_URL=
|
|
107
|
+
#
|
|
108
|
+
# The apps/api origin @working-theory/api-client calls from
|
|
109
|
+
# apps-paid/marketing-web's SERVER (RSC + Server Actions) for the pricing
|
|
110
|
+
# recommender's /v1/flows/* routes (#4884). Unset means /pricing makes no
|
|
111
|
+
# request and serves the manifest content page. Format: https://<host> (no
|
|
112
|
+
# trailing slash). Server-only — never NEXT_PUBLIC_.
|
|
113
|
+
# AILK_API_URL=
|
|
55
114
|
|
|
56
115
|
# ── Analytics ────────────────────────────────────────────────────────────────
|
|
57
116
|
# All providers off by default. Set the env var(s) for any provider(s) you want.
|
|
@@ -188,8 +247,9 @@ CONTENT_BACKEND=mdx
|
|
|
188
247
|
# Allowlist format: comma-separated user IDs (exact-match strings; no regex,
|
|
189
248
|
# no wildcard). Empty entries between commas are trimmed.
|
|
190
249
|
# Run `pnpm --filter @working-theory/database db:seed:test` to seed three test users
|
|
191
|
-
# (
|
|
192
|
-
# Better Auth `role` field).
|
|
250
|
+
# (user_00000000-0000-7000-8000-0000000000e1 / …e2 / …e3 — reserved-range
|
|
251
|
+
# prefixed ids per ADR-0031, distinguished by the Better Auth `role` field).
|
|
252
|
+
# The seed script itself refuses to run unless
|
|
193
253
|
# AILK_TEST_AUTH_ENABLED === "true".
|
|
194
254
|
#
|
|
195
255
|
# AILK_TEST_AUTH_ENABLED=
|
|
@@ -203,3 +263,67 @@ CONTENT_BACKEND=mdx
|
|
|
203
263
|
# LEAD_CAPTURE_FILE_DISABLED=false
|
|
204
264
|
# Set to "true" to include the submitter's IP in metadata (default: off for privacy).
|
|
205
265
|
# LEAD_CAPTURE_INCLUDE_IP=false
|
|
266
|
+
|
|
267
|
+
# ── Instant AEO score service (#4666) ─────────────────────────────────────────
|
|
268
|
+
# Total crawl wall-clock budget (ms) for POST /v1/aeo/score. Must leave
|
|
269
|
+
# cold-start headroom inside the deployment's function timeout (assumed
|
|
270
|
+
# >= 10s) and stay below the handler's 25s request guard — a value above
|
|
271
|
+
# 15000 is rejected on each request (logged once per request, falls back
|
|
272
|
+
# to the default below; there is no persistent startup phase to check
|
|
273
|
+
# this once in a serverless deployment).
|
|
274
|
+
# AEO_SCORE_BUDGET_MS=6000
|
|
275
|
+
|
|
276
|
+
# ── Scheduling (the `booker` section, #4416) ──────────────────────────────────
|
|
277
|
+
# Server-side only — read inside apps/api, never exposed to the browser.
|
|
278
|
+
# Unset is a supported state: the booker degrades to the provider's own
|
|
279
|
+
# booking page. Full notes in apps/api/.env.example.
|
|
280
|
+
# CAL_API_KEY=cal_live_xxx
|
|
281
|
+
# CAL_API_BASE_URL=https://api.cal.com
|
|
282
|
+
# SCHEDULING_PUBLIC_BASE_URL=https://cal.com
|
|
283
|
+
# SCHEDULING_PROVIDER=cal
|
|
284
|
+
#
|
|
285
|
+
# Which event the /schedule page books, as "<owner>/<event-slug>" (the path of
|
|
286
|
+
# the provider's own public booking page). Read server-side by apps/web; unset
|
|
287
|
+
# means the /schedule route 404s, exactly as if the scheduling module were not
|
|
288
|
+
# installed. Deliberately NOT a NEXT_PUBLIC_ variable — the whole scheduling
|
|
289
|
+
# config surface is server-read.
|
|
290
|
+
# SCHEDULING_CAL_LINK=owner/event-slug
|
|
291
|
+
|
|
292
|
+
# ── Recommender mode — the purchase hand-off (REL-013, #4762 / #4879) ────────
|
|
293
|
+
# A `recommending` flow's email + checkout moment spans two deployments:
|
|
294
|
+
# apps/api serves POST /v1/flows/:slug/save + /checkout and the Stripe
|
|
295
|
+
# webhook; apps/web runs the Server Action that calls them
|
|
296
|
+
# (app/[locale]/flows/[slug]/checkout.actions.ts). Who sets what, and what
|
|
297
|
+
# happens unset, is tabulated in docs/runbooks/site-configuration.md
|
|
298
|
+
# § Recommender mode. apps/api's three reads are declared above:
|
|
299
|
+
# DATABASE_URL (required — /save + /checkout answer 503 flows_not_configured
|
|
300
|
+
# without it), STRIPE_SECRET_KEY (sk_test_ only — the checkout route refuses
|
|
301
|
+
# any other prefix before its first side effect) and STRIPE_WEBHOOK_SECRET
|
|
302
|
+
# (whsec_ — the completed/expired stamp on the FlowCheckout buyer row).
|
|
303
|
+
#
|
|
304
|
+
# The public origin of THIS site (apps/web). The Stripe success/cancel return
|
|
305
|
+
# URLs are built from it (resolveBaseUrl(), apps/web/src/lib/routes.ts) as
|
|
306
|
+
# `${NEXT_PUBLIC_BASE_URL}/<locale>/flows/<slug>/results/<payload>?checkout=…`;
|
|
307
|
+
# it also drives the llms.txt / sitemap.xml / robots.txt canonical URLs. Unset
|
|
308
|
+
# falls back to https://ailk.example.com — a placeholder host a paying buyer
|
|
309
|
+
# would be returned to after payment. Set on every apps/web deployment that
|
|
310
|
+
# serves a recommending flow. Scheme + host only, no trailing slash.
|
|
311
|
+
# NEXT_PUBLIC_BASE_URL=https://www.example.com
|
|
312
|
+
#
|
|
313
|
+
# apps/api's origin as seen from apps/web's SERVER process — the api-client's
|
|
314
|
+
# base URL (getApiBase(), packages/api-client/src/http.ts) for every flow
|
|
315
|
+
# Server Action call (flows.get / submitStep / save / checkout). AILK_API_URL
|
|
316
|
+
# wins; NEXT_PUBLIC_API_URL is the fallback; both unset resolves to
|
|
317
|
+
# http://localhost:3001 (local dev only — off-localhost every save / checkout
|
|
318
|
+
# call fails as `network`). Distinct from NEXT_PUBLIC_AILK_API_URL
|
|
319
|
+
# (apps/web/.env.example), which BROWSER-side blocks post to directly. Set on
|
|
320
|
+
# every apps/web deployment whose API is not on localhost.
|
|
321
|
+
# AILK_API_URL=http://localhost:3001
|
|
322
|
+
#
|
|
323
|
+
# NEXT_PUBLIC_ENABLED_LOCALES — do NOT set by hand. Baked at build time by
|
|
324
|
+
# apps/web/next.config.mjs from project.yaml `product.locales` (falls back to
|
|
325
|
+
# "en" when project.yaml is absent) and read by middleware.ts, the sitemap
|
|
326
|
+
# route and the flow Server Actions / clients to validate the locale segment
|
|
327
|
+
# of every URL they build. Declared here so the read is not an undocumented
|
|
328
|
+
# dependency; a value set in the platform env is overridden by the build.
|
|
329
|
+
# NEXT_PUBLIC_ENABLED_LOCALES=en
|
package/templates/CLAUDE.md
CHANGED
|
@@ -14,7 +14,7 @@ docs/ Project notes (ADRs, architecture, planning)
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
You consume `@working-theory/*` packages from npm. Source for those packages is at
|
|
17
|
-
<https://github.com/
|
|
17
|
+
<https://github.com/working-theory-labs/ai-launch-kit>; you don't have it locally.
|
|
18
18
|
Treat each `@working-theory/*` package as a third-party dependency — read its README
|
|
19
19
|
or its types, don't go looking for the source.
|
|
20
20
|
|
|
@@ -108,9 +108,15 @@ checking what `@working-theory/schema` exports.
|
|
|
108
108
|
These are non-negotiable in this repo. Don't ask, don't try to route
|
|
109
109
|
around them.
|
|
110
110
|
|
|
111
|
-
- **No live Stripe keys in code.**
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
- **No live Stripe keys in code.** The runtime guard is an ALLOW-LIST, not
|
|
112
|
+
a list of banned prefixes: it accepts only recognized test-mode keys
|
|
113
|
+
(`sk_test_`, `pk_test_`, `rk_test_`) and refuses everything else. Every
|
|
114
|
+
live key is therefore refused — a restricted key (`rk_live_`) and any
|
|
115
|
+
prefix Stripe introduces later included — because it is absent from the
|
|
116
|
+
allow-list, not because someone remembered to ban it. Live keys belong
|
|
117
|
+
in the deployment platform's secrets store, never in `.env`, source
|
|
118
|
+
files, or commit messages. Use Stripe sandbox keys for everything in
|
|
119
|
+
source.
|
|
114
120
|
- **No secrets in commits.** API keys, license keys, database URLs — all
|
|
115
121
|
go through `.env.local` (gitignored) for local dev and the deployment
|
|
116
122
|
platform's secrets store for prod.
|
|
@@ -127,7 +133,7 @@ when:
|
|
|
127
133
|
|
|
128
134
|
- An architecture invariant would have to bend.
|
|
129
135
|
- A change to a shared `@working-theory/*` package would be needed (file an issue
|
|
130
|
-
upstream at <https://github.com/
|
|
136
|
+
upstream at <https://github.com/working-theory-labs/ai-launch-kit>; don't fork).
|
|
131
137
|
- You're about to delete or move >5 files at once.
|
|
132
138
|
- The acceptance criteria contradict each other or the existing code.
|
|
133
139
|
|
package/templates/README.md
CHANGED
|
@@ -105,6 +105,6 @@ If you need to override a section component, swap its registration in the sectio
|
|
|
105
105
|
|
|
106
106
|
Your project — `{{PROJECT_NAME}}`, owned by {{COMPANY_NAME}} ({{COMPANY_DOMAIN}}) — is yours. Pick whatever license suits you.
|
|
107
107
|
|
|
108
|
-
The AI Launch Kit framework upstream is **Apache 2.0** — see <https://github.com/
|
|
108
|
+
The AI Launch Kit framework upstream is **Apache 2.0** — see <https://github.com/working-theory-labs/ai-launch-kit/blob/main/LICENSE>. Contributions are welcome at the upstream repo; start with the [contributing guide](https://github.com/working-theory-labs/ai-launch-kit/blob/main/CONTRIBUTING.md).
|
|
109
109
|
|
|
110
110
|
For the full handbook, examples, and Pro features: <https://ailaunchkit.ai>.
|