create-ailk 0.1.1 → 0.3.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/component-catalog.md +210 -52
- package/dist/cli.js +0 -0
- package/dist/lib/apply-module-patches.d.ts +47 -0
- package/dist/lib/apply-module-patches.js +370 -0
- package/dist/lib/extract-module-bundle.d.ts +12 -3
- package/dist/lib/extract-module-bundle.js +236 -24
- package/dist/lib/fetch-module.d.ts +3 -38
- package/dist/lib/fetch-module.js +26 -8
- package/dist/lib/module-license-gate.d.ts +2 -0
- package/dist/lib/module-license-gate.js +1 -0
- package/dist/lib/paid-paths.d.ts +16 -0
- package/dist/lib/paid-paths.js +90 -0
- package/dist/module-architecture.d.ts +139 -23
- package/dist/module-architecture.js +668 -25
- package/dist/parse-args.d.ts +2 -2
- package/dist/parse-args.js +3 -1
- package/dist/programmatic.d.ts +2 -1
- package/dist/programmatic.js +15 -4
- package/dist/surfaces.d.ts +12 -36
- package/dist/surfaces.js +35 -5
- package/dist/sync-routes.js +31 -1
- package/package.json +14 -15
- package/templates/.claude/agents/web.md +2 -3
- package/templates/.claude/rules/architecture.md +5 -5
- package/templates/.claude/skills/README.md +2 -1
- package/templates/.env.example +21 -9
- package/templates/CONVENTIONS.md +6 -7
- package/templates/apps/api/.env.example +9 -0
- package/templates/apps/api/CLAUDE.md +35 -4
- package/templates/apps/api/package.json +0 -1
- package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
- package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
- package/templates/apps/api/src/config/index.ts +19 -0
- package/templates/apps/api/src/config/modules.ts +12 -14
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
- package/templates/apps/api/src/lib/slice-load.ts +90 -0
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
- package/templates/apps/api/src/openapi/index.ts +9 -3
- package/templates/apps/api/src/openapi/spec.ts +505 -65
- package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
- package/templates/apps/api/src/routes/content/index.ts +38 -11
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
- package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
- package/templates/apps/api/src/server.ts +471 -133
- 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/consent.ts +236 -0
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
- package/templates/apps/api/src/services/listing-config.ts +58 -0
- package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
- package/templates/apps/api/src/services/project-listings.ts +602 -29
- package/templates/apps/api/src/vercel-handler.ts +60 -26
- package/templates/apps/mcp/.env.example +8 -0
- package/templates/apps/mcp/CLAUDE.md +2 -2
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
- package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
- package/templates/apps/mcp/src/config/modules.ts +14 -13
- package/templates/apps/mcp/src/server.ts +8 -4
- package/templates/apps/mcp/src/tools/index.ts +7 -22
- package/templates/apps/web/.env.example +15 -0
- package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
- package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
- package/templates/apps/web/app/[locale]/layout.tsx +13 -1
- package/templates/apps/web/app/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +20 -13
- package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
- package/templates/apps/web/lib/site-brand.tsx +4 -1
- package/templates/apps/web/lib/site-theme.ts +74 -0
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +1 -6
- package/templates/apps/web/public/android-chrome-192x192.png +0 -0
- package/templates/apps/web/public/android-chrome-512x512.png +0 -0
- package/templates/apps/web/public/apple-touch-icon.png +0 -0
- package/templates/apps/web/public/favicon-16x16.png +0 -0
- package/templates/apps/web/public/favicon-32x32.png +0 -0
- package/templates/apps/web/public/favicon.ico +0 -0
- package/templates/apps/web/public/favicon.svg +6 -3
- package/templates/apps/web/public/lockup-horizontal.svg +4 -0
- package/templates/apps/web/public/logomark.svg +4 -0
- package/templates/apps/web/public/site.webmanifest +2 -2
- package/templates/apps/web/public/wordmark.svg +4 -0
- package/templates/content/_site.mdx +12 -0
- package/templates/database/CHANGELOG.md +94 -0
- package/templates/database/inbox/schema.prisma +165 -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/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- package/templates/database/scripts/db-generate-locked.sh +0 -0
- package/templates/package.json +1 -1
- package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
- package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
- package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
- package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
- package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
- package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
- package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
- package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
- package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
- package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
- package/templates/apps/api/src/__tests__/server.test.ts +0 -253
- package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
- package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
- package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
- package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
- package/templates/apps/api/src/bin/seed-presets.ts +0 -58
- package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
- package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
- package/templates/apps/api/src/lib/stripe.ts +0 -52
- package/templates/apps/api/src/lib/tenant-db.ts +0 -225
- package/templates/apps/api/src/lib/ws-token.ts +0 -91
- package/templates/apps/api/src/middleware/tenant.ts +0 -99
- package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
- package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
- package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
- package/templates/apps/api/src/routes/billing/index.ts +0 -36
- package/templates/apps/api/src/routes/billing/portal.ts +0 -182
- package/templates/apps/api/src/routes/billing/read.ts +0 -75
- package/templates/apps/api/src/routes/billing/usage.ts +0 -153
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
- package/templates/apps/api/src/routes/checkout/index.ts +0 -13
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
- package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
- package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
- package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
- package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
- package/templates/apps/api/src/routes/flows/README.md +0 -147
- package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
- package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
- package/templates/apps/api/src/routes/flows/index.ts +0 -202
- package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
- package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
- package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
- package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
- package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
- package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
- package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
- package/templates/apps/api/src/routes/schedule/index.ts +0 -249
- package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
- package/templates/apps/api/src/routes/slack/actions.ts +0 -177
- package/templates/apps/api/src/routes/slack/index.ts +0 -38
- package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
- package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
- package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
- package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
- package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
- package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
- package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
- package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
- package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
- package/templates/apps/api/src/routes/webhooks/README.md +0 -80
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
- package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
- package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
- package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
- package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
- package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
- package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
- package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
- package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
- package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
- package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
- package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
- package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
- package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
- package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
- package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
- package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
- package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
- package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
- package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
- package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
- package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
- package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
- package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
- package/templates/apps/api/src/services/flow-engine.ts +0 -1278
- package/templates/apps/api/src/services/lead-promotion.ts +0 -176
- package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
- package/templates/apps/api/src/services/playbook-compile.ts +0 -398
- package/templates/apps/api/src/services/playbook-render.ts +0 -263
- package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
- package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
- package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
- package/templates/apps/api/src/services/recommender-capture.ts +0 -835
- package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
- package/templates/apps/api/src/services/scheduling/index.ts +0 -63
- package/templates/apps/api/src/services/scheduling/types.ts +0 -88
- package/templates/apps/api/src/services/tenant-context.ts +0 -451
- package/templates/apps/api/src/services/usage-metering.ts +0 -699
- package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
- package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
- package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
- package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
- package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
- package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
- package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
- package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
- package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
- package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
- package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
- package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
- package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
- package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
- package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
- package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
- package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
- package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
- package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
- package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
- package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
- package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
- package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
- package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
- package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
- package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
- package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
- package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
- package/templates/apps/web/content/en/blog/README.txt +0 -25
- package/templates/apps/web/content/en/docs/index.mdx +0 -28
- package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
- package/templates/apps/web/lib/blog-list-data.ts +0 -95
- package/templates/apps/web/lib/blog-post-data.ts +0 -79
- package/templates/apps/web/lib/source.ts +0 -34
- package/templates/apps/web/public/logomark-dark.svg +0 -4
- package/templates/apps/web/public/logomark-light.svg +0 -4
- package/templates/apps/web/source.config.ts +0 -78
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
"use server";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* checkout.actions.ts — the recommender's two email-moment Server Actions
|
|
5
|
-
* (#4842 D7), siblings of `flow.actions.ts` (ADR 0013 `*.actions.ts`):
|
|
6
|
-
*
|
|
7
|
-
* - `startCheckoutAction` — Pay: validates with the shared
|
|
8
|
-
* `flowCheckoutInputSchema` (#4841 — a body with NO money field; the
|
|
9
|
-
* server recomputes `price()` from the register it served, PRD L104),
|
|
10
|
-
* calls `flows.checkout`, and on `ok` hands the visitor to Stripe's
|
|
11
|
-
* hosted page via `redirect(url)` (D1 — hosted Checkout, Sam 2026-09-05).
|
|
12
|
-
* - `saveRecommendationAction` — "Email me this plan": validates with
|
|
13
|
-
* `flowSaveInputSchema`, calls `flows.save`, returns the lead id.
|
|
14
|
-
*
|
|
15
|
-
* Both mirror `submitFlowStepAction`'s idiom exactly: shared-schema
|
|
16
|
-
* validation, the visitor-IP forwarding helper (duplicated here — the
|
|
17
|
-
* `flow.actions.ts` original is module-private and that file is this unit's
|
|
18
|
-
* regression floor), a status → kind map, and NEVER a throw to the client.
|
|
19
|
-
* Logging is PII-safe: slug + kind only — the email is never logged.
|
|
20
|
-
*
|
|
21
|
-
* `redirect()` throws `NEXT_REDIRECT` by design, so it is called OUTSIDE the
|
|
22
|
-
* try/catch that maps a thrown api-client error to `network` — inside it,
|
|
23
|
-
* the catch would swallow the very navigation the action exists to perform.
|
|
24
|
-
*
|
|
25
|
-
* The Stripe return URLs are BUILT HERE, never accepted from the client
|
|
26
|
-
* (security review, #4875): a direct caller could otherwise mint a session
|
|
27
|
-
* that redirects post-payment to an arbitrary host, since the shared
|
|
28
|
-
* `flowRedirectUrlSchema` pins only `http(s)` + a host. The client hands over
|
|
29
|
-
* the permalink `payload` it already has; the action decodes it, requires it
|
|
30
|
-
* to name THIS flow, and composes `<site origin>/<safe locale>/flows/<slug>/
|
|
31
|
-
* results/<payload>?checkout=success|cancelled` from the app's own configured
|
|
32
|
-
* origin (`resolveBaseUrl()` — the same source `layout.tsx` / `sitemap.xml`
|
|
33
|
-
* use for every absolute URL).
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import { flows } from "@working-theory/api-client";
|
|
37
|
-
import type { FlowCheckoutInputRaw, FlowSaveInput } from "@working-theory/validation";
|
|
38
|
-
import {
|
|
39
|
-
decodePermalink,
|
|
40
|
-
flowCheckoutInputSchema,
|
|
41
|
-
flowSaveInputSchema,
|
|
42
|
-
flowSlugSchema,
|
|
43
|
-
} from "@working-theory/validation";
|
|
44
|
-
import { headers } from "next/headers";
|
|
45
|
-
import { redirect } from "next/navigation";
|
|
46
|
-
|
|
47
|
-
import { resolveBaseUrl } from "../../../../src/lib/routes";
|
|
48
|
-
|
|
49
|
-
export type EmailMomentFailureKind =
|
|
50
|
-
| "invalid_input"
|
|
51
|
-
| "not_configured"
|
|
52
|
-
| "rate_limited"
|
|
53
|
-
| "unavailable"
|
|
54
|
-
| "network";
|
|
55
|
-
|
|
56
|
-
export interface EmailMomentFailure {
|
|
57
|
-
ok: false;
|
|
58
|
-
kind: EmailMomentFailureKind;
|
|
59
|
-
retryAfterSeconds?: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** `startCheckoutAction` never returns on success — it `redirect()`s. */
|
|
63
|
-
export type CheckoutActionResult = EmailMomentFailure;
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The client-supplied checkout input: the wire body MINUS the two return
|
|
67
|
-
* URLs (server-built, above) PLUS the permalink `payload` they are built from.
|
|
68
|
-
*/
|
|
69
|
-
export type StartCheckoutInput = Omit<FlowCheckoutInputRaw, "successUrl" | "cancelUrl"> & {
|
|
70
|
-
payload: string;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
// Mirrors `FlowStepperClient.tsx:80–88` (#4707) — the locale segment of a
|
|
74
|
-
// built URL is validated against the same build-time-baked allowlist, never
|
|
75
|
-
// interpolated raw.
|
|
76
|
-
const DEFAULT_LOCALE = "en";
|
|
77
|
-
const ENABLED_LOCALES = (process.env.NEXT_PUBLIC_ENABLED_LOCALES ?? DEFAULT_LOCALE).split(",");
|
|
78
|
-
|
|
79
|
-
/** Validate a candidate locale against the configured set; unknown → default. */
|
|
80
|
-
function resolveSafeLocale(candidate: string): string {
|
|
81
|
-
return ENABLED_LOCALES.includes(candidate) ? candidate : DEFAULT_LOCALE;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* The permalink's absolute URL on THIS site, or `undefined` when the payload
|
|
86
|
-
* is not a valid permalink of `slug` (never a URL to somewhere else).
|
|
87
|
-
*/
|
|
88
|
-
function permalinkUrlFor(slug: string, locale: string, payload: string): string | undefined {
|
|
89
|
-
const decoded = decodePermalink(payload);
|
|
90
|
-
if (!decoded.ok || decoded.value.flowSlug !== slug) return undefined;
|
|
91
|
-
const origin = resolveBaseUrl().replace(/\/+$/, "");
|
|
92
|
-
return `${origin}/${resolveSafeLocale(locale)}/flows/${slug}/results/${payload}`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export type SaveActionResult = { ok: true; leadId: string } | EmailMomentFailure;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Reads the visitor's real IP from THIS action's own incoming request —
|
|
99
|
-
* never from the browser-supplied payload — preferring `x-forwarded-for`
|
|
100
|
-
* and falling back to `x-real-ip`. Returns `undefined` rather than throwing
|
|
101
|
-
* on any failure (a local copy of `flow.actions.ts`'s helper, #4730).
|
|
102
|
-
*/
|
|
103
|
-
async function resolveVisitorIpInit(): Promise<
|
|
104
|
-
{ headers: Record<string, string> } | undefined
|
|
105
|
-
> {
|
|
106
|
-
try {
|
|
107
|
-
const incoming = await headers();
|
|
108
|
-
const visitorIp =
|
|
109
|
-
incoming.get("x-forwarded-for") ?? incoming.get("x-real-ip");
|
|
110
|
-
return visitorIp
|
|
111
|
-
? { headers: { "x-forwarded-for": visitorIp } }
|
|
112
|
-
: undefined;
|
|
113
|
-
} catch {
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** PII-safe error identity for logs — the `recommender.ts` idiom (`err.name` only). */
|
|
119
|
-
function errorName(err: unknown): string {
|
|
120
|
-
return err instanceof Error ? err.name : "unknown";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** The shared `status → kind` map (D7). Never throws. */
|
|
124
|
-
function failureFor(res: { status: number; body: unknown }): EmailMomentFailure {
|
|
125
|
-
if (res.status === 400) return { ok: false, kind: "invalid_input" };
|
|
126
|
-
if (res.status === 409) return { ok: false, kind: "not_configured" };
|
|
127
|
-
if (res.status === 429) {
|
|
128
|
-
const body = res.body as { retryAfterSeconds?: number } | null;
|
|
129
|
-
return {
|
|
130
|
-
ok: false,
|
|
131
|
-
kind: "rate_limited",
|
|
132
|
-
...(typeof body?.retryAfterSeconds === "number"
|
|
133
|
-
? { retryAfterSeconds: body.retryAfterSeconds }
|
|
134
|
-
: {}),
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
return { ok: false, kind: "unavailable" };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* exactOptionalPropertyTypes-safe rebuild of the parsed body: Zod's inferred
|
|
142
|
-
* optional fields type as `T | undefined`, which does not structurally match
|
|
143
|
-
* the api-client's `sessionId?: string` / `locale?: string` — so an absent
|
|
144
|
-
* optional key is OMITTED rather than sent as an explicit `undefined`.
|
|
145
|
-
*/
|
|
146
|
-
function emailMomentFields(parsed: FlowSaveInput): FlowSaveInput {
|
|
147
|
-
const { answers, email, locale, sessionId } = parsed;
|
|
148
|
-
return {
|
|
149
|
-
answers,
|
|
150
|
-
email,
|
|
151
|
-
...(locale !== undefined ? { locale } : {}),
|
|
152
|
-
...(sessionId !== undefined ? { sessionId } : {}),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export async function startCheckoutAction(
|
|
157
|
-
slug: string,
|
|
158
|
-
input: StartCheckoutInput,
|
|
159
|
-
): Promise<CheckoutActionResult> {
|
|
160
|
-
const parsedSlug = flowSlugSchema.safeParse(slug);
|
|
161
|
-
if (!parsedSlug.success) return { ok: false, kind: "invalid_input" };
|
|
162
|
-
|
|
163
|
-
// Only the named fields cross into the wire body — a caller-supplied
|
|
164
|
-
// `successUrl` / `cancelUrl` (or any other stray key) is dropped here.
|
|
165
|
-
const { answers, email, locale, sessionId, offerKey, seats, period, creditStop, payload } =
|
|
166
|
-
input;
|
|
167
|
-
if (typeof payload !== "string" || payload.length === 0 || payload.length > 4096) {
|
|
168
|
-
return { ok: false, kind: "invalid_input" };
|
|
169
|
-
}
|
|
170
|
-
const permalinkUrl = permalinkUrlFor(parsedSlug.data, locale ?? DEFAULT_LOCALE, payload);
|
|
171
|
-
if (permalinkUrl === undefined) return { ok: false, kind: "invalid_input" };
|
|
172
|
-
|
|
173
|
-
const parsed = flowCheckoutInputSchema.safeParse({
|
|
174
|
-
answers,
|
|
175
|
-
email,
|
|
176
|
-
...(locale !== undefined ? { locale } : {}),
|
|
177
|
-
...(sessionId !== undefined ? { sessionId } : {}),
|
|
178
|
-
offerKey,
|
|
179
|
-
seats,
|
|
180
|
-
period,
|
|
181
|
-
...(creditStop !== undefined ? { creditStop } : {}),
|
|
182
|
-
successUrl: `${permalinkUrl}?checkout=success`,
|
|
183
|
-
cancelUrl: `${permalinkUrl}?checkout=cancelled`,
|
|
184
|
-
});
|
|
185
|
-
if (!parsed.success) return { ok: false, kind: "invalid_input" };
|
|
186
|
-
|
|
187
|
-
const wireBody: FlowCheckoutInputRaw = {
|
|
188
|
-
...emailMomentFields(parsed.data),
|
|
189
|
-
offerKey: parsed.data.offerKey,
|
|
190
|
-
seats: parsed.data.seats,
|
|
191
|
-
period: parsed.data.period,
|
|
192
|
-
creditStop: parsed.data.creditStop,
|
|
193
|
-
successUrl: parsed.data.successUrl,
|
|
194
|
-
cancelUrl: parsed.data.cancelUrl,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
let url: string;
|
|
198
|
-
try {
|
|
199
|
-
const visitorIpInit = await resolveVisitorIpInit();
|
|
200
|
-
const res = visitorIpInit
|
|
201
|
-
? await flows.checkout(parsedSlug.data, wireBody, visitorIpInit)
|
|
202
|
-
: await flows.checkout(parsedSlug.data, wireBody);
|
|
203
|
-
if (!res.ok) {
|
|
204
|
-
const failure = failureFor(res);
|
|
205
|
-
console.error(
|
|
206
|
-
`[startCheckoutAction] slug=${parsedSlug.data} kind=${failure.kind} status=${res.status}`,
|
|
207
|
-
);
|
|
208
|
-
return failure;
|
|
209
|
-
}
|
|
210
|
-
url = res.body.url;
|
|
211
|
-
} catch (err) {
|
|
212
|
-
// Name only — never the error object (it can carry the request payload).
|
|
213
|
-
console.error(
|
|
214
|
-
`[startCheckoutAction] slug=${parsedSlug.data} kind=network errName=${errorName(err)}`,
|
|
215
|
-
);
|
|
216
|
-
return { ok: false, kind: "network" };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Outside the try/catch on purpose — see the file header.
|
|
220
|
-
redirect(url);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export async function saveRecommendationAction(
|
|
224
|
-
slug: string,
|
|
225
|
-
body: FlowSaveInput,
|
|
226
|
-
): Promise<SaveActionResult> {
|
|
227
|
-
const parsedSlug = flowSlugSchema.safeParse(slug);
|
|
228
|
-
if (!parsedSlug.success) return { ok: false, kind: "invalid_input" };
|
|
229
|
-
|
|
230
|
-
const parsed = flowSaveInputSchema.safeParse(body);
|
|
231
|
-
if (!parsed.success) return { ok: false, kind: "invalid_input" };
|
|
232
|
-
|
|
233
|
-
const payload = emailMomentFields(parsed.data);
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const visitorIpInit = await resolveVisitorIpInit();
|
|
237
|
-
const res = visitorIpInit
|
|
238
|
-
? await flows.save(parsedSlug.data, payload, visitorIpInit)
|
|
239
|
-
: await flows.save(parsedSlug.data, payload);
|
|
240
|
-
if (!res.ok) {
|
|
241
|
-
const failure = failureFor(res);
|
|
242
|
-
console.error(
|
|
243
|
-
`[saveRecommendationAction] slug=${parsedSlug.data} kind=${failure.kind} status=${res.status}`,
|
|
244
|
-
);
|
|
245
|
-
return failure;
|
|
246
|
-
}
|
|
247
|
-
return { ok: true, leadId: res.body.leadId };
|
|
248
|
-
} catch (err) {
|
|
249
|
-
console.error(
|
|
250
|
-
`[saveRecommendationAction] slug=${parsedSlug.data} kind=network errName=${errorName(err)}`,
|
|
251
|
-
);
|
|
252
|
-
return { ok: false, kind: "network" };
|
|
253
|
-
}
|
|
254
|
-
}
|