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,136 @@
|
|
|
1
|
+
// oss: false
|
|
2
|
+
/**
|
|
3
|
+
* PATCH /v1/project-listings/:id
|
|
4
|
+
*
|
|
5
|
+
* Tenant-scoped listing mutation (#4998 AC3, reshaped by #5033). The body is
|
|
6
|
+
* `.strict()` and carries at most two optional fields, at least one:
|
|
7
|
+
*
|
|
8
|
+
* - `status` — `active` | `rejected`, the only transition there is. The
|
|
9
|
+
* lifecycle collapsed to five states with `closed` COMPUTED (#5033 D3),
|
|
10
|
+
* so `approved -> live` and `live -> closed` no longer exist and `closed`
|
|
11
|
+
* is not a value this route (or any route) can write. Routed through
|
|
12
|
+
* `decideListing` — the SAME function `POST /v1/slack/actions` calls — so
|
|
13
|
+
* the stamp, the window end and the "already decided" idempotency live in
|
|
14
|
+
* one place. On a changed decision the founder is emailed once via
|
|
15
|
+
* `sendListingDecisionEmail`.
|
|
16
|
+
* - `featured` — the operator's shelf flag (#5033 D6), through
|
|
17
|
+
* `setListingFeatured`. Whether it reaches the public read at all is the
|
|
18
|
+
* site's `featuredEnabled` config, read server-side; this route sets the
|
|
19
|
+
* row's own flag and never the config.
|
|
20
|
+
*
|
|
21
|
+
* Sent together, the featured write lands first and the decision's response
|
|
22
|
+
* is what the caller gets back, so one call cannot return a row that
|
|
23
|
+
* contradicts either write.
|
|
24
|
+
*
|
|
25
|
+
* `:id` is validated at the boundary the same way `get.ts` does; a foreign or
|
|
26
|
+
* missing id is `404`. An operator re-PATCHing an already-decided listing
|
|
27
|
+
* gets `200` with the row unchanged (`decideListing`'s idempotent "second
|
|
28
|
+
* click" branch — #4999 AC).
|
|
29
|
+
*/
|
|
30
|
+
import { zProjectListingId } from "@working-theory/ids";
|
|
31
|
+
import { projectListingPatchSchema, projectListingSchema } from "@working-theory/validation";
|
|
32
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
decideListing,
|
|
37
|
+
sendListingDecisionEmail,
|
|
38
|
+
} from "../../services/project-listing-decision.js";
|
|
39
|
+
import { setListingFeatured, PROJECT_LISTING_ROLES } from "../../services/project-listings.js";
|
|
40
|
+
|
|
41
|
+
import { sendValidated } from "./respond.js";
|
|
42
|
+
|
|
43
|
+
const paramsSchema = z.object({ id: zProjectListingId });
|
|
44
|
+
|
|
45
|
+
function unauthorized(reply: FastifyReply) {
|
|
46
|
+
return reply.status(401).send({
|
|
47
|
+
ok: false,
|
|
48
|
+
error: { code: "unauthorized", message: "Authentication required" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function forbidden(reply: FastifyReply) {
|
|
53
|
+
return reply.status(403).send({
|
|
54
|
+
ok: false,
|
|
55
|
+
error: { code: "forbidden", message: "Forbidden" },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function invalidParams(reply: FastifyReply) {
|
|
60
|
+
return reply.status(400).send({
|
|
61
|
+
ok: false,
|
|
62
|
+
error: { code: "validation_failed", message: "Invalid project listing id" },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function invalidBody(reply: FastifyReply, message: string) {
|
|
67
|
+
return reply.status(400).send({
|
|
68
|
+
ok: false,
|
|
69
|
+
error: { code: "invalid_input", message },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function notFound(reply: FastifyReply) {
|
|
74
|
+
return reply.status(404).send({
|
|
75
|
+
ok: false,
|
|
76
|
+
error: { code: "not_found", message: "Project listing not found" },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function patchProjectListingHandler(
|
|
81
|
+
request: FastifyRequest,
|
|
82
|
+
reply: FastifyReply,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const tenant = request.tenant;
|
|
85
|
+
if (!tenant) return unauthorized(reply);
|
|
86
|
+
if (!PROJECT_LISTING_ROLES.has(tenant.role)) {
|
|
87
|
+
request.log.warn(
|
|
88
|
+
{ method: request.method, path: request.url, status: 403 },
|
|
89
|
+
"[project-listings] patch denied; insufficient role",
|
|
90
|
+
);
|
|
91
|
+
return forbidden(reply);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const parsedParams = paramsSchema.safeParse(request.params);
|
|
95
|
+
if (!parsedParams.success) return invalidParams(reply);
|
|
96
|
+
|
|
97
|
+
const parsedBody = projectListingPatchSchema.safeParse(request.body);
|
|
98
|
+
if (!parsedBody.success) {
|
|
99
|
+
return invalidBody(reply, parsedBody.error.issues[0]?.message ?? "invalid input");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { status, featured } = parsedBody.data;
|
|
103
|
+
|
|
104
|
+
if (featured !== undefined) {
|
|
105
|
+
const featuredResult = await setListingFeatured(
|
|
106
|
+
tenant.workspaceId,
|
|
107
|
+
parsedParams.data.id,
|
|
108
|
+
featured,
|
|
109
|
+
);
|
|
110
|
+
if (!featuredResult.ok) return notFound(reply);
|
|
111
|
+
if (status === undefined) {
|
|
112
|
+
return sendValidated(reply, projectListingSchema, featuredResult.value, "patch");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (status === undefined) {
|
|
117
|
+
// Unreachable — the body schema refuses an empty object — but the
|
|
118
|
+
// narrowing has to be discharged before `decideListing`.
|
|
119
|
+
return invalidBody(reply, "patch body must carry at least one of 'status' or 'featured'");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const decisionResult = await decideListing({
|
|
123
|
+
id: parsedParams.data.id,
|
|
124
|
+
decision: status,
|
|
125
|
+
decidedBy: tenant.userId,
|
|
126
|
+
siteId: tenant.workspaceId,
|
|
127
|
+
});
|
|
128
|
+
if (!decisionResult.ok) {
|
|
129
|
+
if (decisionResult.error.kind === "not_found") return notFound(reply);
|
|
130
|
+
return invalidBody(reply, decisionResult.error.message);
|
|
131
|
+
}
|
|
132
|
+
if (decisionResult.changed) {
|
|
133
|
+
await sendListingDecisionEmail(decisionResult.value, status);
|
|
134
|
+
}
|
|
135
|
+
return sendValidated(reply, projectListingSchema, decisionResult.value, "patch");
|
|
136
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /v1/project-listings/public?siteKey=
|
|
3
|
+
*
|
|
4
|
+
* The anonymous page-render read (#4998 AC4, reshaped by #5033). Every
|
|
5
|
+
* `active` row for the site, ordered: the three newest by `decidedAt` among
|
|
6
|
+
* them first, then the rest by `waitlistCount` desc, then `decidedAt` desc.
|
|
7
|
+
* A row whose window has elapsed comes back with a COMPUTED `closed` status —
|
|
8
|
+
* no job runs, nothing is stored, and a consumer partitions its shelves on
|
|
9
|
+
* what it is handed. `featured` surfaces only where the site's
|
|
10
|
+
* `SiteListingConfig` enables it. `founderEmail` never leaves this route —
|
|
11
|
+
* `projectListingPublicSchema` (`.omit({founderEmail})`) is what the response
|
|
12
|
+
* is re-parsed against before send.
|
|
13
|
+
*/
|
|
14
|
+
import {
|
|
15
|
+
projectListingPublicListResponseSchema,
|
|
16
|
+
publicSiteKeySchema,
|
|
17
|
+
} from "@working-theory/validation";
|
|
18
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
import { siteIdForPublicKey } from "../../services/listing-config.js";
|
|
22
|
+
import { publicProjectListings } from "../../services/project-listings.js";
|
|
23
|
+
|
|
24
|
+
import { sendValidated } from "./respond.js";
|
|
25
|
+
|
|
26
|
+
const querySchema = z.object({ siteKey: publicSiteKeySchema });
|
|
27
|
+
|
|
28
|
+
export const publicProjectListingsRoute: FastifyPluginAsync = async (fastify) => {
|
|
29
|
+
fastify.get("/v1/project-listings/public", async (request, reply) => {
|
|
30
|
+
const parsedQuery = querySchema.safeParse(request.query);
|
|
31
|
+
if (!parsedQuery.success) {
|
|
32
|
+
return reply.status(400).send({
|
|
33
|
+
ok: false,
|
|
34
|
+
error: { code: "validation_failed", message: "Invalid siteKey" },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// #5048 D2 — one identifier for the whole mountable contract. The shelves
|
|
39
|
+
// and the application form are configured with the same public key, so a
|
|
40
|
+
// site never has to hold two different names for itself.
|
|
41
|
+
const siteId = await siteIdForPublicKey(parsedQuery.data.siteKey);
|
|
42
|
+
if (siteId === null) {
|
|
43
|
+
return reply.status(400).send({
|
|
44
|
+
ok: false,
|
|
45
|
+
error: { code: "unknown_site_key", message: "Unknown site key" },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const data = await publicProjectListings(siteId);
|
|
50
|
+
return sendValidated(reply, projectListingPublicListResponseSchema, data, "public");
|
|
51
|
+
});
|
|
52
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response-schema validation for the project-listings routes (#4998),
|
|
3
|
+
* mirroring `waitlist-experiments/respond.ts`'s discipline: response schemas
|
|
4
|
+
* are contract-only unless re-parsed against their own `.strict()` schema
|
|
5
|
+
* before the value leaves the process. A shape drift here is a genuine
|
|
6
|
+
* internal error (the service layer already guards every field), never a
|
|
7
|
+
* client-facing 4xx.
|
|
8
|
+
*/
|
|
9
|
+
import { logger } from "@working-theory/observability";
|
|
10
|
+
import type { FastifyReply } from "fastify";
|
|
11
|
+
import type { z } from "zod";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse `data` against `schema`; on success send the ORIGINAL `data` object
|
|
15
|
+
* (not the parsed clone) at `200`, matching the waitlist-experiments
|
|
16
|
+
* envelope. On failure, log the zod issues (never the raw `data`, which may
|
|
17
|
+
* carry founder PII) and reply `500`.
|
|
18
|
+
*/
|
|
19
|
+
export function sendValidated<T>(
|
|
20
|
+
reply: FastifyReply,
|
|
21
|
+
schema: z.ZodType<T>,
|
|
22
|
+
data: T,
|
|
23
|
+
routeName: string,
|
|
24
|
+
): FastifyReply {
|
|
25
|
+
const parsed = schema.safeParse(data);
|
|
26
|
+
if (!parsed.success) {
|
|
27
|
+
logger.error(
|
|
28
|
+
{ route: routeName, issues: parsed.error.issues },
|
|
29
|
+
"[project-listings] response failed its own contract schema",
|
|
30
|
+
);
|
|
31
|
+
return reply.status(500).send({
|
|
32
|
+
ok: false,
|
|
33
|
+
error: { code: "internal_error", message: "Response failed contract validation" },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return reply.status(200).send({ ok: true, data });
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resume token's transport (#5036 D1).
|
|
3
|
+
*
|
|
4
|
+
* `start` hands the client an opaque token; `patch` and `submit` present it
|
|
5
|
+
* back. It travels in the `Authorization: Bearer` header rather than in the
|
|
6
|
+
* body or the query string for two reasons that matter on an anonymous
|
|
7
|
+
* surface: a query parameter lands in access logs, proxy logs and
|
|
8
|
+
* `Referer` headers, and a body field cannot be sent on a future GET. The
|
|
9
|
+
* header is where a credential belongs, and these routes sit in `leadsScope`
|
|
10
|
+
* where no auth preHandler runs, so nothing else in the stack reads it.
|
|
11
|
+
*/
|
|
12
|
+
import type { FastifyRequest } from "fastify";
|
|
13
|
+
|
|
14
|
+
const BEARER = /^Bearer (.+)$/;
|
|
15
|
+
|
|
16
|
+
/** The presented token, or null when the header is absent or malformed. */
|
|
17
|
+
export function resumeTokenFrom(request: FastifyRequest): string | null {
|
|
18
|
+
const header = request.headers.authorization;
|
|
19
|
+
if (typeof header !== "string") return null;
|
|
20
|
+
const match = BEARER.exec(header.trim());
|
|
21
|
+
return match ? match[1]! : null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /v1/project-listings
|
|
3
|
+
*
|
|
4
|
+
* `start` — step one of the application (#5036 D1). Anonymous, registered in
|
|
5
|
+
* the same rate-limited `leadsScope` as `/v1/leads/*`. Validates with
|
|
6
|
+
* `projectListingStartSchema` (the founder's name, email and consent, and
|
|
7
|
+
* optionally a requested window) and writes the `ProjectListing` row at
|
|
8
|
+
* `status: "draft"`. Returns 201 `{ ok: true, data: { id, resumeToken } }`.
|
|
9
|
+
*
|
|
10
|
+
* NO Lead is written here. The founder's contact details are already captured
|
|
11
|
+
* on the listing row itself at step one, so a Lead would add no capture — and
|
|
12
|
+
* `routeLead` sends an operator notification, which on an anonymous route
|
|
13
|
+
* means a stranger could mail the operator by opening a draft they never
|
|
14
|
+
* finish (PR #5042 security review, finding 4). `submit` writes the Lead.
|
|
15
|
+
*
|
|
16
|
+
* This route REPLACES the single anonymous create. The application became a
|
|
17
|
+
* ten-step form that saves on every advance, so the create had to split into
|
|
18
|
+
* start / patch / submit; validation moved with it, from write-time to
|
|
19
|
+
* submit-time. Nothing here asserts the application is complete, because at
|
|
20
|
+
* step one it never is.
|
|
21
|
+
*
|
|
22
|
+
* The response body is the ONLY place the resume token is ever readable: the
|
|
23
|
+
* server stores a digest of it. A client that loses it has lost the draft.
|
|
24
|
+
*
|
|
25
|
+
* NO `siteId` (#5048 D2). The payload names the site by its PUBLIC KEY and
|
|
26
|
+
* this route resolves the id through `siteIdForPublicKey` — a body-supplied
|
|
27
|
+
* tenant identifier is exactly what the consuming site's proxy route existed
|
|
28
|
+
* to prevent, and the resolution moving here is what lets that route go away.
|
|
29
|
+
* An unknown key is a 400; it never falls back to the deployment's own site.
|
|
30
|
+
*
|
|
31
|
+
* NO `slug`, and no site config values (#5033 D1/D2): the slug is derived from
|
|
32
|
+
* `name` at submit, and the site's window bounds are read server-side. The
|
|
33
|
+
* applicant may REQUEST a window; the server clamps it rather than rejecting
|
|
34
|
+
* the application, because a founder who could post their own bound could post
|
|
35
|
+
* an unbounded one.
|
|
36
|
+
*
|
|
37
|
+
* PII-safe logging (mirrors `routes/leads/index.ts`): routing metadata only —
|
|
38
|
+
* never founderEmail, founderName, or the resume token.
|
|
39
|
+
*
|
|
40
|
+
* The response uses the API's own `{ ok, data }` / `{ ok, error }` envelope
|
|
41
|
+
* (`apps/api/CLAUDE.md`) on BOTH the success and the failure path — the
|
|
42
|
+
* anonymous create it replaces answered bare on success and enveloped on
|
|
43
|
+
* failure, so a client branching on `body.ok` read a success as a failure.
|
|
44
|
+
*/
|
|
45
|
+
import {
|
|
46
|
+
projectListingStartResponseSchema,
|
|
47
|
+
projectListingStartSchema,
|
|
48
|
+
} from "@working-theory/validation";
|
|
49
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
50
|
+
|
|
51
|
+
import { siteIdForPublicKey } from "../../services/listing-config.js";
|
|
52
|
+
import { startProjectListingDraft } from "../../services/project-listings.js";
|
|
53
|
+
|
|
54
|
+
export const startProjectListingRoute: FastifyPluginAsync = async (fastify) => {
|
|
55
|
+
fastify.post("/v1/project-listings", async (request, reply) => {
|
|
56
|
+
const parsed = projectListingStartSchema.safeParse(request.body);
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
return reply.status(400).send({
|
|
59
|
+
ok: false,
|
|
60
|
+
error: {
|
|
61
|
+
code: "invalid_input",
|
|
62
|
+
message: parsed.error.issues[0]?.message ?? "invalid input",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// #5048 D2 — the site is resolved SERVER-SIDE from the payload's public
|
|
68
|
+
// key. The body never carries a `siteId`, and `projectListingStartSchema`
|
|
69
|
+
// is `.strict()`, so a body that still does is the 400 above naming the
|
|
70
|
+
// unrecognised key rather than a silently-honoured tenant override. This
|
|
71
|
+
// is the one property the consuming site's proxy route existed to
|
|
72
|
+
// guarantee, and resolving it here is what lets that route go away.
|
|
73
|
+
//
|
|
74
|
+
// An unknown key is a 400 and never falls back to the deployment's own
|
|
75
|
+
// site: a typo would otherwise file one site's applications against
|
|
76
|
+
// another with nothing to notice it.
|
|
77
|
+
const siteId = await siteIdForPublicKey(parsed.data.siteKey);
|
|
78
|
+
if (siteId === null) {
|
|
79
|
+
return reply.status(400).send({
|
|
80
|
+
ok: false,
|
|
81
|
+
error: { code: "unknown_site_key", message: "Unknown site key" },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await startProjectListingDraft(siteId, parsed.data);
|
|
86
|
+
// #5275 — a `consents` map declining a REQUIRED purpose contradicts the
|
|
87
|
+
// body's own `consent: true`; no row was created.
|
|
88
|
+
if (!result.ok) {
|
|
89
|
+
return reply.status(400).send({
|
|
90
|
+
ok: false,
|
|
91
|
+
error: { code: result.error.kind, message: result.error.message },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
request.log.info(
|
|
96
|
+
{ siteId, listingId: result.id, ip: request.ip },
|
|
97
|
+
"project listing draft started",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const body = projectListingStartResponseSchema.parse({
|
|
101
|
+
id: result.id,
|
|
102
|
+
resumeToken: result.resumeToken,
|
|
103
|
+
});
|
|
104
|
+
return reply.status(201).send({ ok: true, data: body });
|
|
105
|
+
});
|
|
106
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /v1/project-listings/drafts/:id/submit
|
|
3
|
+
*
|
|
4
|
+
* `submit` — the moment an application stops being a draft (#5036 D1).
|
|
5
|
+
* Anonymous, token-gated, registered in the rate-limited `leadsScope`.
|
|
6
|
+
*
|
|
7
|
+
* It carries NO body. Everything it validates is already on the row, put
|
|
8
|
+
* there by ten steps of `patch`, and the server assembles and validates it
|
|
9
|
+
* against `projectListingSubmitSchema`. An incomplete draft comes back `422`
|
|
10
|
+
* with the offending fields NAMED, so the form can send the founder back to
|
|
11
|
+
* the step that owns each one rather than saying "something is missing".
|
|
12
|
+
*
|
|
13
|
+
* Three server-side rules bind here and only here: completeness; the
|
|
14
|
+
* concurrent-listing limit (`409 listing_limit_reached` — at most two
|
|
15
|
+
* non-terminal listings per founder, drafts included, #5036 D7); and the
|
|
16
|
+
* slug, derived from `name` at last (#5033 D2) and returned in the response,
|
|
17
|
+
* which is the only place a caller learns it.
|
|
18
|
+
*
|
|
19
|
+
* The Lead write also happens here rather than at step one, via `routeLead` —
|
|
20
|
+
* so an anonymous stranger cannot cause an operator notification by opening a
|
|
21
|
+
* draft they never finish (PR #5042 security review, finding 4). The Slack
|
|
22
|
+
* notification fires AFTER the transition, never before, and never fails a
|
|
23
|
+
* submit that already succeeded (`postListingApplication` never throws). Both
|
|
24
|
+
* moved here from the old create route because this — not step one — is the
|
|
25
|
+
* moment an operator has an application worth looking at.
|
|
26
|
+
*
|
|
27
|
+
* Every response uses the API's own `{ ok, data }` / `{ ok, error }` envelope
|
|
28
|
+
* (`apps/api/CLAUDE.md`), success included.
|
|
29
|
+
*/
|
|
30
|
+
import { zProjectListingId } from "@working-theory/ids";
|
|
31
|
+
import { projectListingSubmitResponseSchema } from "@working-theory/validation";
|
|
32
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
|
|
35
|
+
import { submitProjectListingDraft } from "../../services/project-listings.js";
|
|
36
|
+
import { postListingApplication } from "../../services/slack-notify.js";
|
|
37
|
+
|
|
38
|
+
import { resumeTokenFrom } from "./resume-token.js";
|
|
39
|
+
|
|
40
|
+
const paramsSchema = z.object({ id: zProjectListingId });
|
|
41
|
+
|
|
42
|
+
export const submitProjectListingRoute: FastifyPluginAsync = async (fastify) => {
|
|
43
|
+
fastify.post("/v1/project-listings/drafts/:id/submit", async (request, reply) => {
|
|
44
|
+
const parsedParams = paramsSchema.safeParse(request.params);
|
|
45
|
+
if (!parsedParams.success) {
|
|
46
|
+
return reply.status(400).send({
|
|
47
|
+
ok: false,
|
|
48
|
+
error: { code: "validation_failed", message: "Invalid project listing id" },
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resumeToken = resumeTokenFrom(request);
|
|
53
|
+
if (resumeToken === null) {
|
|
54
|
+
return reply.status(401).send({
|
|
55
|
+
ok: false,
|
|
56
|
+
error: { code: "unauthorized", message: "A resume token is required" },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sourceMeta = {
|
|
61
|
+
ip: request.ip,
|
|
62
|
+
userAgent: request.headers["user-agent"],
|
|
63
|
+
receivedAt: new Date().toISOString(),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = await submitProjectListingDraft(
|
|
67
|
+
parsedParams.data.id,
|
|
68
|
+
resumeToken,
|
|
69
|
+
sourceMeta,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
if (result.error.kind === "not_found") {
|
|
74
|
+
return reply.status(404).send({
|
|
75
|
+
ok: false,
|
|
76
|
+
error: { code: "not_found", message: "Draft not found" },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (result.error.kind === "incomplete") {
|
|
80
|
+
return reply.status(422).send({
|
|
81
|
+
ok: false,
|
|
82
|
+
error: {
|
|
83
|
+
code: "incomplete_application",
|
|
84
|
+
message: result.error.message,
|
|
85
|
+
missingFields: result.error.missingFields,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (result.error.kind === "listing_limit_reached") {
|
|
90
|
+
return reply.status(409).send({
|
|
91
|
+
ok: false,
|
|
92
|
+
error: { code: "listing_limit_reached", message: result.error.message },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return reply.status(409).send({
|
|
96
|
+
ok: false,
|
|
97
|
+
error: { code: "slug_taken", message: result.error.message },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
request.log.info(
|
|
102
|
+
{ siteId: result.listing.siteId, slug: result.slug, ip: request.ip },
|
|
103
|
+
"project listing applied",
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Awaited (not fire-and-forget) so it can't be killed mid-flight by a
|
|
107
|
+
// serverless runtime tearing down after the response.
|
|
108
|
+
await postListingApplication({
|
|
109
|
+
id: result.listing.id,
|
|
110
|
+
name: result.listing.name,
|
|
111
|
+
oneLiner: result.listing.oneLiner,
|
|
112
|
+
problem: result.listing.problem,
|
|
113
|
+
url: result.listing.url,
|
|
114
|
+
product: result.listing.product,
|
|
115
|
+
founderName: result.listing.founderName,
|
|
116
|
+
founderCompany: result.listing.founderCompany,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const body = projectListingSubmitResponseSchema.parse({ id: result.id, slug: result.slug });
|
|
120
|
+
return reply.status(200).send({ ok: true, data: body });
|
|
121
|
+
});
|
|
122
|
+
};
|