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,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file module-exclusion.test.ts
|
|
3
|
+
* @description Module-exclusion regression test (S1, #5088).
|
|
4
|
+
*
|
|
5
|
+
* Proves the mechanical claim card S1 exists to make true: when a paid
|
|
6
|
+
* module is excluded (`AILK_ENABLED_MODULES` omits it), none of the HTTP
|
|
7
|
+
* routes that module contributes register — Fastify's default 404 is the
|
|
8
|
+
* signal, since a request to an unregistered route always 404s regardless
|
|
9
|
+
* of auth/validation state, while a registered-but-rejected request 4xxs
|
|
10
|
+
* with a DIFFERENT code (401/403/400/...). This is the runtime half of "a
|
|
11
|
+
* route and its MCP tool appear or vanish together" (API↔MCP parity) — see
|
|
12
|
+
* the sibling `apps/mcp/__tests__/module-exclusion.test.ts`.
|
|
13
|
+
*
|
|
14
|
+
* Reverting one of `server.ts`'s `isModuleEnabled(...)` conditionals makes
|
|
15
|
+
* the matching case below fail RED — see #5088 S1 verification notes.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors `src/__tests__/server.test.ts`'s external-boundary mocks (CORS,
|
|
18
|
+
* rate-limit, email) — see that file's header for why each is mocked.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
jest.mock('@fastify/cors', () => {
|
|
22
|
+
return jest.fn().mockResolvedValue(undefined);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
jest.mock('@fastify/rate-limit', () => {
|
|
26
|
+
return jest.fn().mockResolvedValue(undefined);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
jest.mock('@working-theory/email', () => ({
|
|
30
|
+
sendLeadNotification: jest.fn().mockResolvedValue({ ok: true }),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
import { build } from '../server';
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- server is typed via Fastify generics; explicit any avoids pulling in the full FastifyInstance type here
|
|
36
|
+
type App = any;
|
|
37
|
+
|
|
38
|
+
async function withEnabledModules<T>(
|
|
39
|
+
enabled: string,
|
|
40
|
+
fn: () => Promise<T>,
|
|
41
|
+
): Promise<T> {
|
|
42
|
+
const prior = process.env.AILK_ENABLED_MODULES;
|
|
43
|
+
process.env.AILK_ENABLED_MODULES = enabled;
|
|
44
|
+
try {
|
|
45
|
+
return await fn();
|
|
46
|
+
} finally {
|
|
47
|
+
if (prior === undefined) {
|
|
48
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
49
|
+
} else {
|
|
50
|
+
process.env.AILK_ENABLED_MODULES = prior;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// One representative route per module-conditional registration block in
|
|
56
|
+
// server.ts. `always404WhenExcluded` routes are expected to 404 ONLY when
|
|
57
|
+
// their module is absent; every other module stays enabled for each case so
|
|
58
|
+
// a bug that over-excludes (drops an unrelated module's route too) is also
|
|
59
|
+
// caught by the "still registered" assertions below.
|
|
60
|
+
const MODULE_ROUTES: Record<string, { method: 'GET' | 'POST'; url: string }[]> =
|
|
61
|
+
{
|
|
62
|
+
'multi-tenant': [
|
|
63
|
+
{ method: 'GET', url: '/v1/workspaces' },
|
|
64
|
+
{ method: 'GET', url: '/v1/billing' },
|
|
65
|
+
{ method: 'GET', url: '/v1/waitlist-experiments' },
|
|
66
|
+
{ method: 'GET', url: '/v1/project-listings' },
|
|
67
|
+
],
|
|
68
|
+
scheduling: [{ method: 'GET', url: '/v1/schedule/availability' }],
|
|
69
|
+
'content-write': [{ method: 'POST', url: '/v1/content/BlogPosting' }],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const ALL_MODULES = Object.keys(MODULE_ROUTES);
|
|
73
|
+
|
|
74
|
+
describe('Module exclusion — registration vanishes with the module (S1, #5088)', () => {
|
|
75
|
+
let app: App;
|
|
76
|
+
|
|
77
|
+
afterEach(async () => {
|
|
78
|
+
if (app) {
|
|
79
|
+
await app.close();
|
|
80
|
+
app = undefined;
|
|
81
|
+
}
|
|
82
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it.each(ALL_MODULES)(
|
|
86
|
+
"excluding '%s' 404s every route it owns, while every OTHER module's routes stay registered",
|
|
87
|
+
async (excluded) => {
|
|
88
|
+
const enabled = ALL_MODULES.filter((m) => m !== excluded).join(',');
|
|
89
|
+
app = await withEnabledModules(enabled, () => build());
|
|
90
|
+
|
|
91
|
+
for (const { method, url } of MODULE_ROUTES[excluded]!) {
|
|
92
|
+
const res = await app.inject({ method, url });
|
|
93
|
+
expect(res.statusCode).toBe(404);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const other of ALL_MODULES) {
|
|
97
|
+
if (other === excluded) continue;
|
|
98
|
+
for (const { method, url } of MODULE_ROUTES[other]!) {
|
|
99
|
+
const res = await app.inject({ method, url });
|
|
100
|
+
expect(res.statusCode).not.toBe(404);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
it('with every module enabled (the default), every module route registers — no over-exclusion', async () => {
|
|
107
|
+
app = await build();
|
|
108
|
+
for (const routes of Object.values(MODULE_ROUTES)) {
|
|
109
|
+
for (const { method, url } of routes) {
|
|
110
|
+
const res = await app.inject({ method, url });
|
|
111
|
+
expect(res.statusCode).not.toBe(404);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Free/never-gate surfaces are unaffected by any module exclusion.
|
|
115
|
+
const health = await app.inject({ method: 'GET', url: '/health' });
|
|
116
|
+
expect(health.statusCode).toBe(200);
|
|
117
|
+
const contentTypes = await app.inject({
|
|
118
|
+
method: 'GET',
|
|
119
|
+
url: '/v1/content/types',
|
|
120
|
+
});
|
|
121
|
+
expect(contentTypes.statusCode).not.toBe(404);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('excluding every paid module at once still serves the free surface', async () => {
|
|
125
|
+
app = await withEnabledModules('', () => build());
|
|
126
|
+
const health = await app.inject({ method: 'GET', url: '/health' });
|
|
127
|
+
expect(health.statusCode).toBe(200);
|
|
128
|
+
const contentTypes = await app.inject({
|
|
129
|
+
method: 'GET',
|
|
130
|
+
url: '/v1/content/types',
|
|
131
|
+
});
|
|
132
|
+
expect(contentTypes.statusCode).not.toBe(404);
|
|
133
|
+
|
|
134
|
+
for (const routes of Object.values(MODULE_ROUTES)) {
|
|
135
|
+
for (const { method, url } of routes) {
|
|
136
|
+
const res = await app.inject({ method, url });
|
|
137
|
+
expect(res.statusCode).toBe(404);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -204,4 +204,50 @@ describe('Parser scope — webhooks raw-body parser does not leak', () => {
|
|
|
204
204
|
await server.close();
|
|
205
205
|
}
|
|
206
206
|
});
|
|
207
|
+
|
|
208
|
+
describe('per-scope rate-limit defaults (#5111, DEF-004)', () => {
|
|
209
|
+
// Real end-to-end behavior against the real in-house rate-limit
|
|
210
|
+
// middleware (NOT mocked — this repo's convention is to mock only real
|
|
211
|
+
// external boundaries, never an internal module, see the file header).
|
|
212
|
+
// aeoScope's `/v1/aeo/score/example` needs no DB/network (a bundled
|
|
213
|
+
// example, `loadExampleScore()`); flowsScope's `/v1/flows/:slug` with a
|
|
214
|
+
// schema-invalid slug returns 400 before any DB access — both let the
|
|
215
|
+
// onRequest rate-limit hook be the only thing under test, regardless of
|
|
216
|
+
// the eventual handler status.
|
|
217
|
+
it('caps aeoScope at the strict pre-#4730 default (10/min) — the 11th request 429s', async () => {
|
|
218
|
+
app = await build();
|
|
219
|
+
const ip = '198.51.100.11';
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < 10; i++) {
|
|
222
|
+
const res = await app.inject({
|
|
223
|
+
method: 'GET',
|
|
224
|
+
url: '/v1/aeo/score/example',
|
|
225
|
+
headers: { 'x-forwarded-for': ip },
|
|
226
|
+
});
|
|
227
|
+
expect(res.statusCode).not.toBe(429);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const eleventh = await app.inject({
|
|
231
|
+
method: 'GET',
|
|
232
|
+
url: '/v1/aeo/score/example',
|
|
233
|
+
headers: { 'x-forwarded-for': ip },
|
|
234
|
+
});
|
|
235
|
+
expect(eleventh.statusCode).toBe(429);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('leaves flowsScope above the strict default — 11 requests never 429', async () => {
|
|
239
|
+
app = await build();
|
|
240
|
+
const ip = '198.51.100.12';
|
|
241
|
+
|
|
242
|
+
for (let i = 0; i < 11; i++) {
|
|
243
|
+
const res = await app.inject({
|
|
244
|
+
method: 'GET',
|
|
245
|
+
url: '/v1/flows/not-a-real-slug!!!',
|
|
246
|
+
headers: { 'x-forwarded-for': ip },
|
|
247
|
+
});
|
|
248
|
+
// Schema-invalid slug → 400, well before any rate limit or DB call.
|
|
249
|
+
expect(res.statusCode).toBe(400);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
});
|
|
207
253
|
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file vercel-handler.test.ts
|
|
3
|
+
* @description Regression coverage for the two independent defects in the
|
|
4
|
+
* Vercel serverless adapter (#5168):
|
|
5
|
+
*
|
|
6
|
+
* - request side: the adapter forwards the client's original content-length
|
|
7
|
+
* verbatim, but the body it hands to inject() is a JS object that
|
|
8
|
+
* light-my-request re-serializes itself. light-my-request only computes a
|
|
9
|
+
* fresh content-length when the header is ABSENT (it does
|
|
10
|
+
* `req.headers['content-length'] = req.headers['content-length'] || ...`)
|
|
11
|
+
* — a stale header therefore survives and Fastify's content-length check
|
|
12
|
+
* rejects any body whose re-serialized length differs from what the client
|
|
13
|
+
* declared for the original bytes (FST_ERR_CTP_INVALID_CONTENT_LENGTH).
|
|
14
|
+
* - response side: sending response.body (light-my-request's UTF-8-decoded
|
|
15
|
+
* string) instead of response.rawPayload corrupts any non-UTF-8 response
|
|
16
|
+
* by substituting U+FFFD for every invalid byte.
|
|
17
|
+
*
|
|
18
|
+
* The two request-side tests below drive a REAL minimal Fastify instance
|
|
19
|
+
* through the handler — the defect lives inside Fastify's own
|
|
20
|
+
* content-length check, so a mocked inject() cannot fail the way production
|
|
21
|
+
* fails. The response-side test mocks inject() because the assertion is
|
|
22
|
+
* about what the adapter forwards to res.send, not about Fastify's own
|
|
23
|
+
* behavior.
|
|
24
|
+
*
|
|
25
|
+
* Each test resets the module registry and re-mocks '../server' itself
|
|
26
|
+
* (rather than sharing one top-level jest.mock) because the handler
|
|
27
|
+
* memoizes its built server instance at module scope (getServer()) —
|
|
28
|
+
* without a fresh module per test, the first test's server would be reused
|
|
29
|
+
* by every later test regardless of what that test wants build() to return.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
33
|
+
import Fastify from 'fastify';
|
|
34
|
+
|
|
35
|
+
// ── Typed request/response stubs ────────────────────────────────────────────
|
|
36
|
+
//
|
|
37
|
+
// Minimal stubs that satisfy only the VercelRequest/VercelResponse surface
|
|
38
|
+
// the handler actually reads/calls, each cast to the real type ONCE at a
|
|
39
|
+
// dedicated boundary function — avoids the `as unknown as <T>` double
|
|
40
|
+
// assertion pattern the rest of the call sites would otherwise repeat
|
|
41
|
+
// (mirrors apps/api/src/lib/__tests__/require-paid-tier.test.ts's
|
|
42
|
+
// `asReply`/`StubReply` pattern).
|
|
43
|
+
|
|
44
|
+
interface MockVercelRequest {
|
|
45
|
+
method: string;
|
|
46
|
+
url: string;
|
|
47
|
+
headers: Record<string, string>;
|
|
48
|
+
body: unknown;
|
|
49
|
+
query: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asRequest(stub: MockVercelRequest): VercelRequest {
|
|
53
|
+
// @ts-expect-error: stub satisfies the subset of VercelRequest the handler
|
|
54
|
+
// reads (method/url/headers/body/query); a full VercelRequest (extends
|
|
55
|
+
// http.IncomingMessage) cannot be constructed without a live connection.
|
|
56
|
+
return stub;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface MockRes {
|
|
60
|
+
headersSent: boolean;
|
|
61
|
+
_status?: number;
|
|
62
|
+
_headers: Record<string, string>;
|
|
63
|
+
_sent?: unknown;
|
|
64
|
+
status(code: number): MockRes;
|
|
65
|
+
setHeader(key: string, value: string): MockRes;
|
|
66
|
+
getHeader(key: string): string | undefined;
|
|
67
|
+
send(body: unknown): MockRes;
|
|
68
|
+
json(body: unknown): MockRes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function asResponse(stub: MockRes): VercelResponse {
|
|
72
|
+
// @ts-expect-error: stub satisfies the subset of VercelResponse the handler
|
|
73
|
+
// calls (status/setHeader/getHeader/send/json); a full VercelResponse
|
|
74
|
+
// (extends http.ServerResponse) cannot be constructed without a live
|
|
75
|
+
// connection.
|
|
76
|
+
return stub;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makeRes(): MockRes {
|
|
80
|
+
const headers: Record<string, string> = {};
|
|
81
|
+
const res: MockRes = {
|
|
82
|
+
headersSent: false,
|
|
83
|
+
_headers: headers,
|
|
84
|
+
status(code: number) {
|
|
85
|
+
res._status = code;
|
|
86
|
+
return res;
|
|
87
|
+
},
|
|
88
|
+
setHeader(key: string, value: string) {
|
|
89
|
+
headers[key] = value;
|
|
90
|
+
return res;
|
|
91
|
+
},
|
|
92
|
+
getHeader(key: string) {
|
|
93
|
+
return headers[key];
|
|
94
|
+
},
|
|
95
|
+
send(body: unknown) {
|
|
96
|
+
res._sent = body;
|
|
97
|
+
return res;
|
|
98
|
+
},
|
|
99
|
+
json(body: unknown) {
|
|
100
|
+
res._sent = body;
|
|
101
|
+
return res;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
return res;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Re-required per test (after jest.resetModules()) rather than statically
|
|
108
|
+
// imported — a static import can't reach a freshly reset module.
|
|
109
|
+
function loadHandler(): (req: VercelRequest, res: VercelResponse) => Promise<void> {
|
|
110
|
+
return require('../vercel-handler').default;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe('vercel-handler — request side: stale content-length (#5168)', () => {
|
|
114
|
+
let app: ReturnType<typeof Fastify>;
|
|
115
|
+
|
|
116
|
+
beforeEach(async () => {
|
|
117
|
+
jest.resetModules();
|
|
118
|
+
app = Fastify();
|
|
119
|
+
app.post('/echo', async (req) => req.body);
|
|
120
|
+
await app.ready();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
afterEach(async () => {
|
|
124
|
+
await app.close();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('accepts a JSON body serialized with indent=2', async () => {
|
|
128
|
+
jest.doMock('../server', () => ({ build: jest.fn().mockResolvedValue(app) }));
|
|
129
|
+
const handler = loadHandler();
|
|
130
|
+
|
|
131
|
+
const body = { hello: 'world', nested: { a: 1 } };
|
|
132
|
+
const pretty = JSON.stringify(body, null, 2);
|
|
133
|
+
const req: MockVercelRequest = {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
url: '/echo',
|
|
136
|
+
headers: {
|
|
137
|
+
'content-type': 'application/json',
|
|
138
|
+
'content-length': String(Buffer.byteLength(pretty)),
|
|
139
|
+
},
|
|
140
|
+
body,
|
|
141
|
+
query: {},
|
|
142
|
+
};
|
|
143
|
+
const res = makeRes();
|
|
144
|
+
|
|
145
|
+
await handler(asRequest(req), asResponse(res));
|
|
146
|
+
|
|
147
|
+
expect(res._status).toBe(200);
|
|
148
|
+
expect(JSON.parse(res._sent as string)).toEqual(body);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('accepts a compact JSON body plus a single trailing newline', async () => {
|
|
152
|
+
jest.doMock('../server', () => ({ build: jest.fn().mockResolvedValue(app) }));
|
|
153
|
+
const handler = loadHandler();
|
|
154
|
+
|
|
155
|
+
const body = { hello: 'world' };
|
|
156
|
+
const compactPlusNewline = JSON.stringify(body) + '\n';
|
|
157
|
+
const req: MockVercelRequest = {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
url: '/echo',
|
|
160
|
+
headers: {
|
|
161
|
+
'content-type': 'application/json',
|
|
162
|
+
'content-length': String(Buffer.byteLength(compactPlusNewline)),
|
|
163
|
+
},
|
|
164
|
+
body,
|
|
165
|
+
query: {},
|
|
166
|
+
};
|
|
167
|
+
const res = makeRes();
|
|
168
|
+
|
|
169
|
+
await handler(asRequest(req), asResponse(res));
|
|
170
|
+
|
|
171
|
+
expect(res._status).toBe(200);
|
|
172
|
+
expect(JSON.parse(res._sent as string)).toEqual(body);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('vercel-handler — response side: binary round-trip (#5168)', () => {
|
|
177
|
+
beforeEach(() => {
|
|
178
|
+
jest.resetModules();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('forwards rawPayload byte-identically for a non-UTF-8 response', async () => {
|
|
182
|
+
// '%PDF-' header followed by bytes that are invalid UTF-8 on their own
|
|
183
|
+
// (0x80, 0xFF are continuation/invalid bytes outside any valid sequence).
|
|
184
|
+
const rawPayload = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2d, 0x80, 0xff, 0xfe]);
|
|
185
|
+
// What light-my-request's UTF-8-decoded `.body` would mangle those bytes
|
|
186
|
+
// into — every invalid byte replaced with U+FFFD.
|
|
187
|
+
const mangledBody = rawPayload.toString('utf8');
|
|
188
|
+
const inject = jest.fn().mockResolvedValue({
|
|
189
|
+
statusCode: 200,
|
|
190
|
+
headers: { 'content-type': 'application/pdf' },
|
|
191
|
+
body: mangledBody,
|
|
192
|
+
rawPayload,
|
|
193
|
+
});
|
|
194
|
+
jest.doMock('../server', () => ({ build: jest.fn().mockResolvedValue({ inject }) }));
|
|
195
|
+
const handler = loadHandler();
|
|
196
|
+
|
|
197
|
+
const req: MockVercelRequest = {
|
|
198
|
+
method: 'GET',
|
|
199
|
+
url: '/pdf',
|
|
200
|
+
headers: {},
|
|
201
|
+
body: undefined,
|
|
202
|
+
query: {},
|
|
203
|
+
};
|
|
204
|
+
const res = makeRes();
|
|
205
|
+
|
|
206
|
+
await handler(asRequest(req), asResponse(res));
|
|
207
|
+
|
|
208
|
+
expect(Buffer.isBuffer(res._sent)).toBe(true);
|
|
209
|
+
expect(Buffer.compare(res._sent as Buffer, rawPayload)).toBe(0);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* istanbul ignore file -- CLI bootstrap; the underlying runAbandonEmailSweep
|
|
3
|
+
logic is unit-tested in services/__tests__/abandon-email.test.ts. Mirrors
|
|
4
|
+
cli.ts's coverage-ignore rationale — real entry-point semantics run the
|
|
5
|
+
compiled dist directly, on the operator's own cron/scheduler cadence (D1:
|
|
6
|
+
no queue/timer infrastructure in this repo — an external scheduler runs
|
|
7
|
+
this command on whatever cadence the deployment chooses). */
|
|
8
|
+
/**
|
|
9
|
+
* Abandon-email sweep CLI entry (#3962 D1).
|
|
10
|
+
*
|
|
11
|
+
* Dev: `pnpm --filter @working-theory/api abandon:sweep` (tsx, this file).
|
|
12
|
+
* Production: `node dist/bin/abandon-sweep.js` (the compiled entry, run by
|
|
13
|
+
* any external scheduler — Vercel Cron, GitHub Actions cron, plain crontab).
|
|
14
|
+
* The sweep is stateless and idempotent (D5), so overlap or a missed tick
|
|
15
|
+
* is harmless — the delay window is data, not a timer.
|
|
16
|
+
*/
|
|
17
|
+
import { runAbandonEmailSweep } from "../services/abandon-email.js";
|
|
18
|
+
|
|
19
|
+
async function main(): Promise<void> {
|
|
20
|
+
const result = await runAbandonEmailSweep();
|
|
21
|
+
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
console.error(
|
|
24
|
+
`[abandon-sweep] ${result.error.kind}: ${result.error.message}`,
|
|
25
|
+
);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`[abandon-sweep] ${JSON.stringify(result.summary)}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
main().catch((err: unknown) => {
|
|
34
|
+
// Belt-and-suspenders (code-review finding on #3962's acceptance PR):
|
|
35
|
+
// runAbandonEmailSweep already catches internally and returns a
|
|
36
|
+
// structured `unexpected_error` result, so this path is unreachable in
|
|
37
|
+
// practice — it exists only to prevent a bare unhandled-rejection crash
|
|
38
|
+
// if `main` itself ever throws before/around that call.
|
|
39
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
40
|
+
console.error(`[abandon-sweep] unexpected_error: ${message}`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* istanbul ignore file -- CLI bootstrap; mirrors abandon-sweep.ts's coverage-ignore
|
|
3
|
+
rationale — the underlying list/resend logic is unit-tested at the service
|
|
4
|
+
layer (services/__tests__/deliverable-resend.test.ts), through the same
|
|
5
|
+
mocked-prisma double the fulfillment service tests already use. This file
|
|
6
|
+
is a thin entry: parse args, dispatch to list or resend, print, set exit
|
|
7
|
+
code. */
|
|
8
|
+
/**
|
|
9
|
+
* Deliverable-grant resend CLI (#3998 D2/D4).
|
|
10
|
+
*
|
|
11
|
+
* Dev — list mode (read-only, default):
|
|
12
|
+
* `pnpm --filter @working-theory/api deliverable:resend -- --site <siteId>
|
|
13
|
+
* [--since <iso>] [--limit <n>]`
|
|
14
|
+
* Dev — resend mode (one grant):
|
|
15
|
+
* `pnpm --filter @working-theory/api deliverable:resend -- --grant <grantId>`
|
|
16
|
+
* Production: `node dist/bin/deliverable-resend.js <same flags>`.
|
|
17
|
+
*
|
|
18
|
+
* No HTTP resend route exists (spec §2.2/D2) — this CLI is the sole recovery
|
|
19
|
+
* path for a grant whose post-mint send never completed, run by the operator
|
|
20
|
+
* on their own machine/CI. List mode is the read-only default and can never
|
|
21
|
+
* send anything. `--grant` is always a deliberate, named, single-grant act
|
|
22
|
+
* (D4) — there is no bulk/`--all` mode; the operator loops over the list
|
|
23
|
+
* output if several need resending.
|
|
24
|
+
*/
|
|
25
|
+
import {
|
|
26
|
+
listSendlessGrants,
|
|
27
|
+
resendDeliverableGrant,
|
|
28
|
+
} from "../services/deliverable-fulfillment.js";
|
|
29
|
+
|
|
30
|
+
type ParsedArgs = {
|
|
31
|
+
site?: string | undefined;
|
|
32
|
+
since?: string | undefined;
|
|
33
|
+
limit?: string | undefined;
|
|
34
|
+
grant?: string | undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Minimal `--flag value` parser — no external dependency for a 4-flag CLI. */
|
|
38
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
39
|
+
const out: ParsedArgs = {};
|
|
40
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
41
|
+
const flag = argv[i];
|
|
42
|
+
const value = argv[i + 1];
|
|
43
|
+
if (flag === "--site") out.site = value;
|
|
44
|
+
else if (flag === "--since") out.since = value;
|
|
45
|
+
else if (flag === "--limit") out.limit = value;
|
|
46
|
+
else if (flag === "--grant") out.grant = value;
|
|
47
|
+
else continue;
|
|
48
|
+
i += 1;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function runResend(grantId: string): Promise<void> {
|
|
54
|
+
const result = await resendDeliverableGrant(grantId);
|
|
55
|
+
|
|
56
|
+
if (!result.ok) {
|
|
57
|
+
console.error(
|
|
58
|
+
`[deliverable-resend] ${result.error.kind}: ${result.error.message}`,
|
|
59
|
+
);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(
|
|
65
|
+
`[deliverable-resend] grant="${grantId}" outcome="${result.outcome}"`,
|
|
66
|
+
);
|
|
67
|
+
if (result.outcome !== "sent") {
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runList(args: ParsedArgs): Promise<void> {
|
|
73
|
+
if (!args.site) {
|
|
74
|
+
console.error(
|
|
75
|
+
"[deliverable-resend] usage: --site <siteId> [--since <iso>] [--limit <n>]",
|
|
76
|
+
);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let since: Date | undefined;
|
|
82
|
+
if (args.since !== undefined) {
|
|
83
|
+
since = new Date(args.since);
|
|
84
|
+
if (Number.isNaN(since.getTime())) {
|
|
85
|
+
console.error(
|
|
86
|
+
`[deliverable-resend] invalid --since value: "${args.since}"`,
|
|
87
|
+
);
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// D4: every historical (pre-migration) grant reads sendStatus IS NULL,
|
|
93
|
+
// so an unfiltered first run on an established site lists every grant
|
|
94
|
+
// ever minted — most of which were, in fact, delivered via the prior
|
|
95
|
+
// log-only path. The caveat is printed, not silently assumed away.
|
|
96
|
+
console.log(
|
|
97
|
+
"[deliverable-resend] no --since given — listing EVERY sendless grant " +
|
|
98
|
+
"for this site, including pre-migration rows that predate this " +
|
|
99
|
+
"column and may already have been delivered.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let limit: number | undefined;
|
|
104
|
+
if (args.limit !== undefined) {
|
|
105
|
+
limit = Number.parseInt(args.limit, 10);
|
|
106
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
107
|
+
console.error(
|
|
108
|
+
`[deliverable-resend] invalid --limit value: "${args.limit}"`,
|
|
109
|
+
);
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rows = await listSendlessGrants({
|
|
116
|
+
siteId: args.site,
|
|
117
|
+
...(since !== undefined && { since }),
|
|
118
|
+
...(limit !== undefined && { limit }),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
console.log(JSON.stringify(row));
|
|
123
|
+
}
|
|
124
|
+
console.log(`[deliverable-resend] ${rows.length} sendless grant(s)`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function main(): Promise<void> {
|
|
128
|
+
const args = parseArgs(process.argv.slice(2));
|
|
129
|
+
|
|
130
|
+
if (args.grant) {
|
|
131
|
+
await runResend(args.grant);
|
|
132
|
+
} else {
|
|
133
|
+
await runList(args);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
main().catch((err: unknown) => {
|
|
138
|
+
// Belt-and-suspenders (mirrors abandon-sweep.ts) — an unexpected throw
|
|
139
|
+
// (a bad DB connection, etc.) still exits non-zero with a structured
|
|
140
|
+
// message rather than an unhandled-rejection crash.
|
|
141
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
142
|
+
console.error(`[deliverable-resend] unexpected_error: ${message}`);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
});
|