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,807 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: scaffold-commerce
|
|
3
|
-
description: Wire up commerce on an AILK consumer site — either an external link to an existing storefront (Amazon, Etsy, Shopify, eBay, Gumroad, custom) via a `<MarketplaceCTA>` button, or direct payments via Stripe (test-mode only at v1). Use when the user says "let me sell something", "add a buy button", "wire up Stripe", "set up payments", "I have an Amazon store", or otherwise wants commerce wiring on their AILK site. Two-path branching — picks link vs direct first, never assumes Stripe.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# scaffold-commerce
|
|
7
|
-
|
|
8
|
-
Add commerce to an AILK site. The first question is always: **external
|
|
9
|
-
link to an existing storefront, or direct payments on this site?** Both
|
|
10
|
-
paths are conversational and emit minimal, consumer-owned source.
|
|
11
|
-
|
|
12
|
-
This skill is the canonical "sell stuff" entry point. Pricing UI itself
|
|
13
|
-
is `scaffold-page` (#362)'s lane (you scaffold a `/pricing` page with
|
|
14
|
-
that skill, then wire `lookup_key`s into it). Production webhook setup
|
|
15
|
-
is `provision-config` (#367)'s lane.
|
|
16
|
-
|
|
17
|
-
## When to use
|
|
18
|
-
|
|
19
|
-
The user wants to sell something — physical goods, digital goods,
|
|
20
|
-
subscriptions, services, courses, anything with a price. Triggers:
|
|
21
|
-
|
|
22
|
-
- "let me sell something" / "I want to sell"
|
|
23
|
-
- "add a buy button" / "add a buy now"
|
|
24
|
-
- "wire up Stripe" / "set up payments"
|
|
25
|
-
- "I have an Amazon / Etsy / Shopify / eBay / Gumroad store, link it"
|
|
26
|
-
- "what's the simplest way to charge for this?"
|
|
27
|
-
|
|
28
|
-
Skip this skill when:
|
|
29
|
-
|
|
30
|
-
- The user wants to _design_ a pricing page (lay out tiers, copy,
|
|
31
|
-
comparison table) — that's `scaffold-page` (#362) emitting a Product
|
|
32
|
-
page. Wire `lookup_key`s afterwards from this skill's output.
|
|
33
|
-
- The user wants to set up production Stripe (live mode, real webhooks).
|
|
34
|
-
Hard-blocked at v1 — sandbox-only is hook-enforced globally. Defer
|
|
35
|
-
with the same answer regardless: "v1 is test-mode only; production
|
|
36
|
-
setup is `provision-config` (#367)'s lane."
|
|
37
|
-
- The user is on a non-Stripe payment processor (Lemon Squeezy, Paddle,
|
|
38
|
-
Gumroad-as-payment-processor). v1 only supports Stripe for direct
|
|
39
|
-
payments. Acknowledge and stop; future PRs will add `--provider`
|
|
40
|
-
branches.
|
|
41
|
-
|
|
42
|
-
## The first question (always asked)
|
|
43
|
-
|
|
44
|
-
Greet the consumer with a clear two-path branch:
|
|
45
|
-
|
|
46
|
-
> "Two ways to sell on your AILK site:
|
|
47
|
-
>
|
|
48
|
-
> **Link to an existing store** — you already sell on Amazon, Etsy,
|
|
49
|
-
> Shopify, eBay, Gumroad, or your own storefront. We add a 'Buy on
|
|
50
|
-
> {marketplace}' button that links there.
|
|
51
|
-
>
|
|
52
|
-
> **Direct payments via Stripe** — you don't have a storefront yet, or
|
|
53
|
-
> you want checkout on your own site. We create products in your
|
|
54
|
-
> Stripe (test mode), wire env vars, and point you at the next steps.
|
|
55
|
-
>
|
|
56
|
-
> Which one?"
|
|
57
|
-
|
|
58
|
-
Wait for the consumer's choice. Then run the matching path.
|
|
59
|
-
|
|
60
|
-
## Link path
|
|
61
|
-
|
|
62
|
-
The link path emits source the consumer owns — no env wiring, no API
|
|
63
|
-
calls, no `@working-theory/ui` import.
|
|
64
|
-
|
|
65
|
-
### Step 1 — Collect the inputs
|
|
66
|
-
|
|
67
|
-
Ask conversationally for:
|
|
68
|
-
|
|
69
|
-
1. **URL** — the storefront URL. Validate it parses as a URL. Don't
|
|
70
|
-
accept plain text; prompt again if input fails.
|
|
71
|
-
2. **Button text** — defaults to "Buy on {marketplace}" where
|
|
72
|
-
`{marketplace}` is parsed from the URL hostname (e.g.,
|
|
73
|
-
`amazon.com` → "Amazon"). Consumer can override.
|
|
74
|
-
3. **Optional `iconSrc`** — a path to an SVG/PNG of the marketplace
|
|
75
|
-
logo, e.g., `/logos/amazon.svg`. Optional — clean text-only buttons
|
|
76
|
-
are fine. If the consumer wants logos, point them at
|
|
77
|
-
`references/marketplace-brand-pages.md` for sourcing.
|
|
78
|
-
|
|
79
|
-
### Step 2 — Emit the route template
|
|
80
|
-
|
|
81
|
-
Use `templates/marketplace-cta-route.tsx.tmpl` as the starting source.
|
|
82
|
-
The template defines `<MarketplaceCTA>` **inline** at the top of the
|
|
83
|
-
emitted route file (not bundled in `@working-theory/ui`, not imported from a
|
|
84
|
-
shared package). The consumer owns the component and can customize it.
|
|
85
|
-
|
|
86
|
-
The emitted file path is the consumer's call — typical placement is
|
|
87
|
-
`app/buy/page.tsx` for a dedicated landing, or inserted into an
|
|
88
|
-
existing page (e.g., `app/products/[slug]/page.tsx`) as a `<MarketplaceCTA>`
|
|
89
|
-
usage. Ask which.
|
|
90
|
-
|
|
91
|
-
### Step 3 — Confirm and write
|
|
92
|
-
|
|
93
|
-
Show the consumer the emitted file diff (or new file if standalone)
|
|
94
|
-
in a single confirmation step. After approval, write.
|
|
95
|
-
|
|
96
|
-
### Step 4 — No env wiring, no follow-up files
|
|
97
|
-
|
|
98
|
-
The link path is self-contained. Nothing to add to `.env.local`. No
|
|
99
|
-
references file write (the marketplace brand pages cheat-sheet ships
|
|
100
|
-
in the skill folder; the skill points at it but doesn't copy it into
|
|
101
|
-
the consumer's repo).
|
|
102
|
-
|
|
103
|
-
## Direct payments path (Stripe, test-mode only at v1)
|
|
104
|
-
|
|
105
|
-
The Stripe path uses the **Stripe MCP** (configured in the AILK
|
|
106
|
-
consumer scaffold's `.claude/settings.json`) to create products with
|
|
107
|
-
consumer-derived `lookup_key`s, writes test-mode env vars, and
|
|
108
|
-
surfaces created keys for the consumer to grep when wiring pricing UI.
|
|
109
|
-
|
|
110
|
-
### Step 1 — Defense-in-depth live-key check
|
|
111
|
-
|
|
112
|
-
Before any other action:
|
|
113
|
-
|
|
114
|
-
1. **Env detection.** Scan the consumer's `.env.local`, `.env`,
|
|
115
|
-
`.env.development`, and any `.env.example` for `sk_live_` or
|
|
116
|
-
`pk_live_`. If any match, refuse with:
|
|
117
|
-
|
|
118
|
-
> "AILK is sandbox-only at v1. I see a `sk_live_*` or `pk_live_*`
|
|
119
|
-
> value in your env files — replace it with a test-mode key from
|
|
120
|
-
> <https://dashboard.stripe.com/test/apikeys> before continuing.
|
|
121
|
-
> Production setup is `provision-config` (#367)'s lane and is
|
|
122
|
-
> deferred at v1."
|
|
123
|
-
|
|
124
|
-
Stop. No Stripe API calls fire.
|
|
125
|
-
|
|
126
|
-
2. **Paste detection.** If the consumer pastes a `sk_live_` or
|
|
127
|
-
`pk_live_` value during the flow, refuse with the same message
|
|
128
|
-
and stop.
|
|
129
|
-
|
|
130
|
-
This double-check is required by the brief and matches the global
|
|
131
|
-
hook-enforced sandbox-only constraint.
|
|
132
|
-
|
|
133
|
-
### Step 2 — Collect product inputs
|
|
134
|
-
|
|
135
|
-
Ask the consumer to describe each product they want to sell. Per
|
|
136
|
-
product:
|
|
137
|
-
|
|
138
|
-
- **Name** — e.g., "Pro plan", "Pizza", "Online course".
|
|
139
|
-
- **Price + interval** — e.g., "$29/month", "$15 one-time", "$199/year".
|
|
140
|
-
- **Optional features bullet list** — surfaced in the dashboard, not
|
|
141
|
-
required for the API call.
|
|
142
|
-
|
|
143
|
-
Loop until the consumer says "that's all" or equivalent.
|
|
144
|
-
|
|
145
|
-
### Step 3 — Derive `lookup_key`s
|
|
146
|
-
|
|
147
|
-
For each product, derive a stable, human-readable `lookup_key`:
|
|
148
|
-
|
|
149
|
-
- Lowercase the name, replace spaces with underscores.
|
|
150
|
-
- Append `_monthly` / `_yearly` / `_onetime` based on interval.
|
|
151
|
-
- Examples:
|
|
152
|
-
- "Pro plan, $29/month" → `pro_monthly`
|
|
153
|
-
- "Annual subscription, $199/year" → `annual_yearly`
|
|
154
|
-
- "Pizza, $15 one-time" → `pizza_onetime`
|
|
155
|
-
|
|
156
|
-
Show the derivations to the consumer and offer to override per-product
|
|
157
|
-
before any API call.
|
|
158
|
-
|
|
159
|
-
### Step 4 — Conflict handling (per product)
|
|
160
|
-
|
|
161
|
-
For each product, before creating it, query the consumer's Stripe
|
|
162
|
-
account for an existing product with the same `lookup_key` (use the
|
|
163
|
-
Stripe MCP — `stripe.products.list({lookup_keys: [...]})` or equivalent).
|
|
164
|
-
|
|
165
|
-
If one exists, **show + ask** (do not silently skip, do not refuse):
|
|
166
|
-
|
|
167
|
-
```
|
|
168
|
-
A product with lookup_key `pro_monthly` already exists:
|
|
169
|
-
Existing: "Pro plan" — $29/month — created 2026-04-15
|
|
170
|
-
Proposed: "Pro plan" — $29/month — new
|
|
171
|
-
|
|
172
|
-
What would you like to do?
|
|
173
|
-
- Keep existing (no change)
|
|
174
|
-
- Overwrite amounts (update price, keep lookup_key)
|
|
175
|
-
- Pick a different lookup_key (e.g., `pro_monthly_v2`)
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Wait for choice. Apply.
|
|
179
|
-
|
|
180
|
-
### Step 5 — Create products + prices in Stripe
|
|
181
|
-
|
|
182
|
-
For each product (after conflict resolution), use the Stripe MCP:
|
|
183
|
-
|
|
184
|
-
1. **Create product:** `stripe.products.create({ name, ... })`.
|
|
185
|
-
2. **Create price:** `stripe.prices.create({ product: <id>, unit_amount, currency: 'usd', recurring: { interval } | undefined, lookup_key })`.
|
|
186
|
-
|
|
187
|
-
Surface created IDs in chat as you go so the consumer can verify in
|
|
188
|
-
their Stripe dashboard alongside the run.
|
|
189
|
-
|
|
190
|
-
### Step 6 — Write `.env.local`
|
|
191
|
-
|
|
192
|
-
After all products are created, write to the consumer's `.env.local`:
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
STRIPE_SECRET_KEY=sk_test_...
|
|
196
|
-
STRIPE_PUBLISHABLE_KEY=pk_test_...
|
|
197
|
-
STRIPE_WEBHOOK_SECRET=
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
Use the test-mode keys from the consumer's Stripe account (the
|
|
201
|
-
consumer pastes these from
|
|
202
|
-
<https://dashboard.stripe.com/test/apikeys>). Leave
|
|
203
|
-
`STRIPE_WEBHOOK_SECRET` empty — Step 7 fills it.
|
|
204
|
-
|
|
205
|
-
### Step 7 — Webhook setup (dev-only)
|
|
206
|
-
|
|
207
|
-
Tell the consumer to run, in a separate terminal:
|
|
208
|
-
|
|
209
|
-
```bash
|
|
210
|
-
stripe listen --forward-to localhost:3000/api/webhooks/stripe
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
The CLI prints a signing secret (`whsec_...`). Have the consumer paste
|
|
214
|
-
it into `.env.local` as `STRIPE_WEBHOOK_SECRET`.
|
|
215
|
-
|
|
216
|
-
Note: production webhook setup is **deferred to v2** — the consumer
|
|
217
|
-
will register a permanent webhook endpoint in Stripe's dashboard
|
|
218
|
-
during deploy. Point at `provision-config` (#367) or future deploy
|
|
219
|
-
skill for the production handoff. Do not attempt to register a
|
|
220
|
-
production webhook from this skill.
|
|
221
|
-
|
|
222
|
-
### Step 8 — Write `references/lookup-keys.md`
|
|
223
|
-
|
|
224
|
-
Use `references/lookup-keys-template.md` as the source. Instantiate
|
|
225
|
-
with the products created this session:
|
|
226
|
-
|
|
227
|
-
```markdown
|
|
228
|
-
# Lookup Keys — generated by scaffold-commerce
|
|
229
|
-
|
|
230
|
-
| Product | Lookup key | Amount | Interval |
|
|
231
|
-
| -------- | ------------- | ------ | -------- |
|
|
232
|
-
| Pro plan | `pro_monthly` | $29.00 | monthly |
|
|
233
|
-
| ... | ... | ... | ... |
|
|
234
|
-
|
|
235
|
-
When wiring a pricing page (`scaffold-page` (#362) on a `Product` slug),
|
|
236
|
-
import or reference these lookup keys. Stripe lookup keys are stable
|
|
237
|
-
across price recreates — safe to hard-code in your app.
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
The consumer's filled-in `lookup-keys.md` lives in their repo (e.g.,
|
|
241
|
-
project root or a `docs/` folder) — point them at `scaffold-page`
|
|
242
|
-
(#362) for the pricing page that consumes these keys.
|
|
243
|
-
|
|
244
|
-
### Step 9 — Closing summary
|
|
245
|
-
|
|
246
|
-
Print:
|
|
247
|
-
|
|
248
|
-
- Number of products created (with `lookup_key`s).
|
|
249
|
-
- Path to `references/lookup-keys.md`.
|
|
250
|
-
- Reminder: webhook setup is dev-only at v1; production webhook is
|
|
251
|
-
`provision-config` (#367)'s lane.
|
|
252
|
-
- Pointer at `scaffold-page` (#362) for wiring pricing UI.
|
|
253
|
-
- Reminder: sandbox-only at v1. Live keys are hook-blocked globally.
|
|
254
|
-
|
|
255
|
-
### Step 10 — Surface-selection branch (`feature:commerce` only, FEAT-023 U1)
|
|
256
|
-
|
|
257
|
-
This step is **additive** — it runs after Step 9's closing summary and
|
|
258
|
-
never changes anything above it. A site without the `commerce`
|
|
259
|
-
entitlement gets exactly today's flow (Steps 1–9) and stops there.
|
|
260
|
-
|
|
261
|
-
**10a — Fail-closed entitlement check.** Determine whether this site is
|
|
262
|
-
entitled to `feature:commerce` (the license-manifest feature added in
|
|
263
|
-
FEAT-023 U1 — `commerce` is included in the `pro` / `pro_plus` /
|
|
264
|
-
`enterprise` tiers, absent from `free`):
|
|
265
|
-
|
|
266
|
-
- If the consumer's project has a resolvable license — a
|
|
267
|
-
`WT_LICENSE_KEY` (or equivalent) env var resolving to a
|
|
268
|
-
`@working-theory/license` `License` — call
|
|
269
|
-
`hasFeature("commerce", license)` (`@working-theory/license-client`,
|
|
270
|
-
static-mode: sync, no network). `true` → entitled; proceed to 10b.
|
|
271
|
-
- **Any other case is NOT entitled — fail closed:** no license
|
|
272
|
-
configured, the license lacks `commerce`, or `hasFeature` throws.
|
|
273
|
-
Do nothing further. The consumer already has the full link path +
|
|
274
|
-
product-creation flow from Steps 1–9; there is no additional
|
|
275
|
-
action or message needed.
|
|
276
|
-
|
|
277
|
-
Do not attempt to determine entitlement by asking the consumer their
|
|
278
|
-
plan conversationally — the check is against the resolved license, not
|
|
279
|
-
a verbal claim.
|
|
280
|
-
|
|
281
|
-
**10b — Offer the surface-selection branch.** For an entitled site,
|
|
282
|
-
ask:
|
|
283
|
-
|
|
284
|
-
> "Want to scaffold a payment surface for these products? Pick one (or
|
|
285
|
-
> 'skip' for now):
|
|
286
|
-
>
|
|
287
|
-
> - **Checkout** — a hosted or embedded checkout flow (one-time or
|
|
288
|
-
> subscription).
|
|
289
|
-
> - **Pay-by-link** — a shareable payment link, no page to build.
|
|
290
|
-
> - **Invoice** — send + track invoices with a status badge.
|
|
291
|
-
> - **Subscription** — recurring billing with a status badge.
|
|
292
|
-
> - **Portal** — a self-serve billing-management page for customers."
|
|
293
|
-
|
|
294
|
-
**10c — Re-run the live-key check.** Before dispatching to any surface
|
|
295
|
-
scaffolder, re-run Step 1's live-key check (env scan + paste
|
|
296
|
-
detection) — belt-and-suspenders, since surface scaffolding may run in
|
|
297
|
-
a session well after the original product-creation pass.
|
|
298
|
-
|
|
299
|
-
**10d — Instantiate the shared config.** Copy
|
|
300
|
-
`templates/stripe-config.template.ts` into the consumer's `apps/api`
|
|
301
|
-
(or the surface's own route directory) as the shared
|
|
302
|
-
`loadStripeSurfaceConfig()` the surface's route handler calls at
|
|
303
|
-
request time. This carries the test-mode-only guard
|
|
304
|
-
(`assertSandboxKey`, `@working-theory/stripe-client`) into the new
|
|
305
|
-
surface entry point — the same belt-and-suspenders guard
|
|
306
|
-
`getStripeClient()` already applies, so a live key is refused here too,
|
|
307
|
-
not only at the shared Stripe client singleton.
|
|
308
|
-
|
|
309
|
-
**10e — Dispatch to the matching surface scaffolder.** **Checkout
|
|
310
|
-
(one-time)** (10e-i, FEAT-023 U2), **Pay by link** (10e-ii, FEAT-023 U3),
|
|
311
|
-
**Invoice** (10g, FEAT-023 U4), **Subscription** (10h, FEAT-023 U5), and
|
|
312
|
-
**Portal** (10i, FEAT-023 U6) are all available inline. If a future choice's
|
|
313
|
-
matching skill is not installed, say so plainly:
|
|
314
|
-
|
|
315
|
-
> "Payment-surface scaffolding for `<choice>` isn't available in this
|
|
316
|
-
> AILK version yet — the foundation (entitlement gate + shared
|
|
317
|
-
> config + UI primitives) is in place; the surface itself ships in a
|
|
318
|
-
> later update."
|
|
319
|
-
|
|
320
|
-
Do not attempt to hand-build the surface yourself when its scaffolder
|
|
321
|
-
is missing — that would bypass the recomposition method (§8: capability
|
|
322
|
-
checklist → `packages/ui` primitives, never a verbatim UI lift) every
|
|
323
|
-
FEAT-023 UI surface must follow.
|
|
324
|
-
|
|
325
|
-
**10e-i — Checkout (one-time), FEAT-023 U2.** When the consumer picks
|
|
326
|
-
**Checkout** and wants a one-time payment (not a subscription — v1 only
|
|
327
|
-
scaffolds one-time; acknowledge and stop if the consumer wants
|
|
328
|
-
recurring billing, that's a future `mode: "subscription"` branch):
|
|
329
|
-
|
|
330
|
-
1. **Pick the product(s).** Offer the `lookup_key`s already created this
|
|
331
|
-
session (Step 3/5) as the checkout's product choices. If none were
|
|
332
|
-
created yet, run Steps 2–5 first (or ask for at least one product).
|
|
333
|
-
2. **Instantiate the route.** Copy
|
|
334
|
-
`templates/checkout-route.template.ts` next to the already-instantiated
|
|
335
|
-
`stripe-config.ts` (Step 10d) in the consumer's `apps/api/src/routes/checkout/`
|
|
336
|
-
(checkout creation is server-side only — `apps/web` may not import
|
|
337
|
-
`@working-theory/stripe-client`, architecture.yaml). Register its exported
|
|
338
|
-
`oneTimeCheckoutHandler` on a `POST` route, e.g.
|
|
339
|
-
`POST /api/checkout/one-time`, SCOPED BEHIND the shared per-IP rate-limit
|
|
340
|
-
middleware (`apps/api/src/middleware/rate-limit.ts`, already present in
|
|
341
|
-
every consumer's `apps/api` — the same one `server.ts` wires around the
|
|
342
|
-
anonymous "leads" routes). This is a public, unauthenticated, single-purpose
|
|
343
|
-
session-creation endpoint by design (see the template's own header comment)
|
|
344
|
-
— the rate limit is its abuse guard against unbounded session-creation /
|
|
345
|
-
card-testing attempts; do not register it unscoped. Ask the consumer to
|
|
346
|
-
confirm the route path if their `apps/api` layout differs from the default.
|
|
347
|
-
|
|
348
|
-
**Two edits are MANDATORY as you instantiate it. The route is fail-closed
|
|
349
|
-
until both are done — every request 404s or 500s. This is deliberate: a
|
|
350
|
-
checkout route that is wrong in either respect is exploitable by an
|
|
351
|
-
anonymous internet caller.**
|
|
352
|
-
|
|
353
|
-
**(a) Pin the purchasable-product allowlist.** Replace the placeholder in
|
|
354
|
-
the route's `ALLOWED_LOOKUP_KEYS` with EXACTLY the `lookup_key`s the
|
|
355
|
-
consumer selected at step 1 — nothing else:
|
|
356
|
-
|
|
357
|
-
```ts
|
|
358
|
-
const ALLOWED_LOOKUP_KEYS: ReadonlySet<string> = new Set<string>([
|
|
359
|
-
"pizza_onetime", // ← the step-1 selection, verbatim
|
|
360
|
-
"combo_onetime",
|
|
361
|
-
]);
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
Why this is not optional: without it, the route resolves ANY
|
|
365
|
-
caller-supplied `lookupKey` against the merchant's ENTIRE Stripe account.
|
|
366
|
-
A merchant with a public `pizza_onetime` ($15) and a private
|
|
367
|
-
`enterprise_annual_90pct_off` in the same account is one guessed lookup
|
|
368
|
-
key away from selling the discounted price to an anonymous `curl`; any
|
|
369
|
-
`$0` comp/test price becomes a free-fulfillment endpoint. Pin the
|
|
370
|
-
products this surface sells — never the account. Do not offer to
|
|
371
|
-
"allow all keys", and do not read the allowlist from an env var or the
|
|
372
|
-
request: it is a source-pinned constant.
|
|
373
|
-
|
|
374
|
-
**(b) Set `FRONTEND_URL`.** The route locks every `successUrl` /
|
|
375
|
-
`cancelUrl` to the site's own origin, read from `FRONTEND_URL` (the same
|
|
376
|
-
env var the consumer's `apps/api` already uses for CORS —
|
|
377
|
-
`apps/api/src/config/index.ts`). Confirm it is set for each environment
|
|
378
|
-
(dev / preview / prod). If it is unset the route returns
|
|
379
|
-
`500 config-error` and refuses every checkout — fail-closed by design.
|
|
380
|
-
Without the origin lock, an anonymous caller can mint a REAL
|
|
381
|
-
`checkout.stripe.com` session for the real merchant whose success page is
|
|
382
|
-
an attacker's — the victim pays on Stripe's real domain and lands on the
|
|
383
|
-
attacker's "receipt".
|
|
384
|
-
|
|
385
|
-
3. **Compose the checkout page.** In the consumer's `apps/web`, create a
|
|
386
|
-
page that renders `CheckoutForm` (`@working-theory/ui`) with the
|
|
387
|
-
session's products as `CheckoutProduct[]` (`lookupKey` + `name` + price).
|
|
388
|
-
Wire `onCheckout` to POST to the Step-2 route with `{ lookupKey,
|
|
389
|
-
successUrl, cancelUrl }` and, on a `201` response, set
|
|
390
|
-
`window.location.href` to the returned `url`. Pass `testMode` (always
|
|
391
|
-
`true` at v1 — sandbox-only). Ask which page path to use (e.g.
|
|
392
|
-
`app/checkout/page.tsx`), mirroring the link path's "ask which"
|
|
393
|
-
convention. Send the redirects as RELATIVE PATHS (`/checkout/success`,
|
|
394
|
-
`/checkout/cancel`): the route composes them onto the site's own origin
|
|
395
|
-
server-side. A cross-origin absolute URL is refused with
|
|
396
|
-
`400 invalid-redirect-url`.
|
|
397
|
-
4. **Compose the success/cancel pages.** Create two pages (e.g.
|
|
398
|
-
`app/checkout/success/page.tsx`, `app/checkout/cancel/page.tsx`) at the
|
|
399
|
-
`successUrl`/`cancelUrl` the checkout page passed, each rendering
|
|
400
|
-
`PaymentResult` (`@working-theory/ui`) with `outcome="success"` /
|
|
401
|
-
`outcome="cancel"` respectively and the consumer's own copy.
|
|
402
|
-
|
|
403
|
-
**Tell the consumer, in these words: the success page is a DISPLAY-ONLY
|
|
404
|
-
terminal state; it is NEVER proof of payment.** Anyone can navigate
|
|
405
|
-
straight to `/checkout/success` and be shown "Payment successful". So
|
|
406
|
-
never hang fulfillment, an entitlement grant, a license issue, or a file
|
|
407
|
-
download off that page. Real fulfillment must be driven by a verified
|
|
408
|
-
Stripe webhook (signature-checked) or a server-side
|
|
409
|
-
`stripe.checkout.sessions.retrieve(id)` whose `payment_status === "paid"`
|
|
410
|
-
— neither of which this v1 surface scaffolds. If the consumer asks for
|
|
411
|
-
fulfillment, that is the next unit of work, not a tweak to this page.
|
|
412
|
-
|
|
413
|
-
5. **Surface the session id.** After the flow is wired, remind the
|
|
414
|
-
consumer that each created Checkout Session's id is logged (truncated,
|
|
415
|
-
no PII) by the route for verification in
|
|
416
|
-
<https://dashboard.stripe.com/test/payments>.
|
|
417
|
-
6. **Closing note.** Confirm the surface is reachable only because
|
|
418
|
-
`feature:commerce` resolved entitled (10a) — a non-entitled site never
|
|
419
|
-
sees this branch. Note that this entitlement check is SCAFFOLD-TIME only:
|
|
420
|
-
the emitted route reads no license at runtime, so a lapsed
|
|
421
|
-
`feature:commerce` does not switch the endpoint off.
|
|
422
|
-
|
|
423
|
-
Remind the consumer this is test-mode only. Be precise about what the
|
|
424
|
-
live-key guard actually covers: `assertSandboxKey` (10d's
|
|
425
|
-
`loadStripeSurfaceConfig`, and the shared Stripe client singleton) is an
|
|
426
|
-
ALLOW-LIST as of AILK#3734 — it accepts only `sk_test_`, `pk_test_` and
|
|
427
|
-
`rk_test_`, and refuses everything else. A Stripe **restricted** live key
|
|
428
|
-
(`rk_live_…`) — the key type Stripe steers server-side integrations toward
|
|
429
|
-
— IS refused, along with any live prefix Stripe introduces later, because
|
|
430
|
-
it is absent from the allow-list rather than named on a ban-list.
|
|
431
|
-
So: "only recognized test-mode keys are accepted", not "these particular
|
|
432
|
-
live prefixes are blocked". Tell the consumer to use a test-mode key and to
|
|
433
|
-
keep live keys out of the environment entirely anyway — a working guard is
|
|
434
|
-
not a reason to point a live key at a non-production deployment.
|
|
435
|
-
|
|
436
|
-
Finally, confirm all three abuse guards are actually in place: the
|
|
437
|
-
rate-limit middleware is wired (step 2 — an unauthenticated checkout route
|
|
438
|
-
without it is a card-testing vector), `ALLOWED_LOOKUP_KEYS` is pinned to
|
|
439
|
-
the real selection (step 2a), and `FRONTEND_URL` is set for every
|
|
440
|
-
environment (step 2b).
|
|
441
|
-
|
|
442
|
-
**10e-ii — Pay-by-link (payment-link management), FEAT-023 U3.** When
|
|
443
|
-
the consumer picks **Pay by link**, instantiate
|
|
444
|
-
`templates/payment-link-route.template.ts` next to the already-instantiated
|
|
445
|
-
`stripe-config.ts` (Step 10d) in the consumer's
|
|
446
|
-
`apps/api/src/routes/payment-links/` — server-side only, same reason as
|
|
447
|
-
checkout (`apps/web` may not import `@working-theory/stripe-client`,
|
|
448
|
-
architecture.yaml).
|
|
449
|
-
|
|
450
|
-
This is a **MANAGEMENT** surface (list / create / archive payment objects),
|
|
451
|
-
NOT a storefront "buy" action — so its auth posture is the OPPOSITE of the
|
|
452
|
-
checkout route's. Register its three exported handlers INSIDE the
|
|
453
|
-
authenticated scope, so `authPreHandlerPlugin` decorates `request.userId`
|
|
454
|
-
(and applies its same-origin CSRF check) before any handler runs:
|
|
455
|
-
|
|
456
|
-
```ts
|
|
457
|
-
await fastify.register(async (authed) => {
|
|
458
|
-
authed.register(authPreHandlerPlugin, { frontendOrigin: config.frontendUrl });
|
|
459
|
-
authed.get("/api/payment-links", listPaymentLinksHandler);
|
|
460
|
-
authed.post("/api/payment-links", createPaymentLinkHandler);
|
|
461
|
-
authed.patch("/api/payment-links", updatePaymentLinkHandler);
|
|
462
|
-
});
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
The registration is **load-bearing** — do not copy the checkout route's
|
|
466
|
-
public, rate-limited registration here:
|
|
467
|
-
|
|
468
|
-
1. **Authentication + authorization.** Each handler calls `requireSiteAdmin()`
|
|
469
|
-
FIRST — before the entitlement check and before any Stripe call. It
|
|
470
|
-
re-reads `User.role` from the database and admits only `admin` / `owner`.
|
|
471
|
-
Authentication alone is not the bar: on a site with open signup, "any
|
|
472
|
-
logged-in user" is "the whole internet with an extra step".
|
|
473
|
-
2. **Entitlement is NOT authorization.** The `feature:commerce` check answers
|
|
474
|
-
"does this DEPLOYMENT have commerce?", never "may this CALLER manage
|
|
475
|
-
payment links?" — it reads the site's env license and returns the identical
|
|
476
|
-
answer to an anonymous caller. It is retained, layered UNDER the identity
|
|
477
|
-
gate, never as a substitute for it. (Shipping this surface with ONLY the
|
|
478
|
-
entitlement gate was AILK#3744: a P0 that let an anonymous caller list every
|
|
479
|
-
payment link, archive them all — total denial of revenue — and mint new ones
|
|
480
|
-
against the merchant's Stripe account.)
|
|
481
|
-
3. **CSRF** is inherited from the auth scope's same-origin `Origin` check
|
|
482
|
-
(`apps/api/src/middleware/auth.ts`), which exists ONLY inside that scope.
|
|
483
|
-
4. **Fail-closed by construction.** Mis-register the routes outside the auth
|
|
484
|
-
scope and `request.userId` is `undefined`, so every handler answers 401 — a
|
|
485
|
-
broken feature, never an open door.
|
|
486
|
-
5. **Tenancy.** Deliberately NOT `tenantScope`. This surface manages the SITE
|
|
487
|
-
OWNER's single Stripe catalog (`packages/stripe-client` is single-tenant; a
|
|
488
|
-
Stripe Payment Link carries no org binding). Cross-account access is
|
|
489
|
-
structurally impossible — Stripe object ids are account-scoped, so a foreign
|
|
490
|
-
id returns `resource_missing`, which the route maps to the same 404 as an
|
|
491
|
-
unknown id (no enumeration oracle). The multi-tenant module's org-scoped
|
|
492
|
-
billing is a SEPARATE surface (`apps/api/src/routes/billing/`), which DOES
|
|
493
|
-
derive its org from `request.tenant.organizationId`.
|
|
494
|
-
|
|
495
|
-
Test-mode-only caveats are identical to checkout's (step 10e-i) — state the
|
|
496
|
-
`assertSandboxKey` allow-list (AILK#3734): only recognized test-mode prefixes
|
|
497
|
-
are accepted, so every live key is refused by construction.
|
|
498
|
-
|
|
499
|
-
**10f — Shared UI primitives.** Every surface scaffolder composes from
|
|
500
|
-
`@working-theory/ui`'s `MoneyAmount` (primitive), `PaymentStatusBadge`
|
|
501
|
-
(block), and `StripeActionButton` (block) — built once in FEAT-023 U1
|
|
502
|
-
so no surface hand-rolls its own amount formatting, status chip, or
|
|
503
|
-
checkout/portal button.
|
|
504
|
-
|
|
505
|
-
**10g — Invoice surface, shipped (FEAT-023 U4).** When the consumer picks
|
|
506
|
-
"Invoice" at 10b, this surface IS available (unlike the still-unshipped
|
|
507
|
-
choices in 10e). Invoice management is an ADMIN action, never a customer-
|
|
508
|
-
facing or anonymous one (a security review of the sibling U3 pay-by-link
|
|
509
|
-
template found the entitlement check alone is NOT authentication — see
|
|
510
|
-
`docs/research/commerce-template-audit/invoices.md` finding 7):
|
|
511
|
-
|
|
512
|
-
1. Instantiate `templates/invoice-route.template.ts` into the consumer's
|
|
513
|
-
`apps/api` — mirrors 10d's `stripe-config.template.ts` instantiation. It
|
|
514
|
-
exports a Fastify plugin (`invoiceRoutes`: `GET` list/retrieve, `POST`
|
|
515
|
-
create-draft, `POST .../send` finalize+send) built on
|
|
516
|
-
`@working-theory/stripe-client`'s `createInvoice` / `sendInvoice` /
|
|
517
|
-
`listInvoices` / `retrieveInvoice`.
|
|
518
|
-
2. **Register it inside the TENANT-GUARDED scope `apps/api`'s
|
|
519
|
-
`billingRoutes` use** — the inner `tenantScope` in `server.ts` that
|
|
520
|
-
registers `tenantPreHandlerPlugin` (itself nested inside `authScope`) —
|
|
521
|
-
e.g. `await tenantScope.register(invoiceRoutes);`. **NOT** the outer
|
|
522
|
-
`authScope`. This is REQUIRED, not optional, and it is a SECURITY
|
|
523
|
-
boundary, not a style preference:
|
|
524
|
-
- `authPreHandlerPlugin` (inherited from the enclosing `authScope`) gives
|
|
525
|
-
every handler an authenticated caller plus the cookie path's same-origin
|
|
526
|
-
CSRF check. The template then requires `User.role` in {`admin`,`owner`},
|
|
527
|
-
so anonymous and plain-authenticated callers are refused.
|
|
528
|
-
- `tenantPreHandlerPlugin` (default-deny) populates `request.tenant`, so
|
|
529
|
-
the org is **server-derived** and never client-supplied — the same
|
|
530
|
-
property `routes/billing/read.ts` relies on. Registering in `authScope`
|
|
531
|
-
instead leaves `request.tenant` undefined, and the template then fails
|
|
532
|
-
**closed** (403) rather than serving an unscoped admin surface.
|
|
533
|
-
Tell the consumer explicitly that this route is admin-only, and that a
|
|
534
|
-
Bearer-token caller is denied (a bearer token carries no workspace
|
|
535
|
-
binding) — this is a cookie-session admin-UI surface.
|
|
536
|
-
3. Point the consumer at `@working-theory/ui`'s `InvoiceList` (block),
|
|
537
|
-
`InvoiceDetail` (panel), and `InvoiceForm` (block) to compose the
|
|
538
|
-
admin-facing management page — `InvoiceForm` posts directly to the
|
|
539
|
-
instantiated route; `InvoiceList`/`InvoiceDetail` render already-fetched
|
|
540
|
-
invoice records (reusing `PaymentStatusBadge` for status and
|
|
541
|
-
`MoneyAmount` for amounts, per 10f).
|
|
542
|
-
4. Re-run 10c's live-key check before instantiating (already covered by
|
|
543
|
-
this step's ordering, restated for clarity). The guard has no remaining
|
|
544
|
-
gap: AILK#3734 converted `assertSandboxKey` to an allow-list, so a Stripe
|
|
545
|
-
RESTRICTED key is refused like every other non-test-mode key. Finding 10
|
|
546
|
-
in the audit log records the historical deny-list gap and is marked
|
|
547
|
-
resolved there.
|
|
548
|
-
5. **Multi-tenant module — the surface REFUSES, it does not warn.** v1 has
|
|
549
|
-
ONE deployment-wide `STRIPE_SECRET_KEY` and no per-organization Stripe
|
|
550
|
-
customer namespace, so an invoice carries no org attribution and a
|
|
551
|
-
caller-supplied `invoiceId`/`customerId` cannot be honestly checked
|
|
552
|
-
against the caller's org (findings 7 + 9 in the audit log). The template's
|
|
553
|
-
`requireSingleTenantScope` guard therefore admits **only the deployment's
|
|
554
|
-
own operator org** — the fixed single-tenant sentinel `DEFAULT_ORG_ID`,
|
|
555
|
-
which an organization created through the multi-tenant module never
|
|
556
|
-
carries — and **hard-refuses every other org's admin with a 403
|
|
557
|
-
`tenancy_unsupported`**, before any Stripe call. That is what stops one
|
|
558
|
-
tenant's admin reading another tenant's customer PII (names, emails,
|
|
559
|
-
amounts, line items) out of the shared Stripe account. Tell the consumer
|
|
560
|
-
plainly: **invoice management is an OPERATOR surface at v1, not a
|
|
561
|
-
per-tenant self-service one.** The operator can still see every invoice in
|
|
562
|
-
the deployment-wide Stripe account — they hold the `STRIPE_SECRET_KEY` and
|
|
563
|
-
can read the same data from the Stripe dashboard, so this is not an
|
|
564
|
-
escalation — but per-tenant invoicing needs a per-org Stripe Connect /
|
|
565
|
-
customer-namespace model (tracked follow-up).
|
|
566
|
-
|
|
567
|
-
**`User.role` is DEPLOYMENT-OPERATOR-ONLY, and must not be widened to reach
|
|
568
|
-
this surface.** The `requireSiteAdmin` gate keys on `User.role` — a
|
|
569
|
-
per-deployment String (`"user" | "admin" | "owner"`), DISTINCT from the
|
|
570
|
-
org-membership `Role` enum the multi-tenant module uses. Do not grant
|
|
571
|
-
`User.role = "admin"` to an organization's admins as a way of giving them
|
|
572
|
-
invoice access: that is a site-wide operator grant, not an org-scoped one,
|
|
573
|
-
and until per-org Stripe namespacing exists it would turn the tenancy
|
|
574
|
-
refusal above into a cross-tenant PII leak (names, emails, amounts, line
|
|
575
|
-
items). The correct answer to "our org admins need invoices" at v1 is that
|
|
576
|
-
the surface does not support it yet — not a role change.
|
|
577
|
-
|
|
578
|
-
**10h — Subscription surface, shipped (FEAT-023 U5).** When the consumer picks
|
|
579
|
-
"Subscription" at 10b, this surface IS available. It splits into TWO handlers
|
|
580
|
-
with DELIBERATELY OPPOSITE auth postures — the same split checkout (public)
|
|
581
|
-
vs invoice (admin) already established:
|
|
582
|
-
|
|
583
|
-
1. Instantiate `templates/subscription-route.template.ts` into the consumer's
|
|
584
|
-
`apps/api` — mirrors 10d's `stripe-config.template.ts` instantiation. It
|
|
585
|
-
exports `subscribeHandler` (a standalone Fastify handler) and
|
|
586
|
-
`subscriptionRoutes` (a Fastify plugin), built on
|
|
587
|
-
`@working-theory/stripe-client`'s `createCheckoutSession` (`mode:
|
|
588
|
-
"subscription"`, pinned) and `retrieveSubscriptionStatus`.
|
|
589
|
-
2. **Register `subscribeHandler` PUBLICLY, rate-limited — mirrors 10e-i's
|
|
590
|
-
checkout registration exactly.** It is a storefront "subscribe" action
|
|
591
|
-
(the recurring-price analog of the one-time checkout route): pin
|
|
592
|
-
`ALLOWED_LOOKUP_KEYS` to the RECURRING lookup_key(s) the consumer selected
|
|
593
|
-
(step 1's selection, verbatim — never "allow all"), confirm `FRONTEND_URL`
|
|
594
|
-
is set (the same origin-lock 10e-i requires), and scope it behind the
|
|
595
|
-
shared per-IP rate-limit middleware. Both edits are MANDATORY — the route
|
|
596
|
-
is fail-closed until they're done.
|
|
597
|
-
3. **Register `subscriptionRoutes` inside the SAME tenant-guarded scope
|
|
598
|
-
10g's `invoiceRoutes` uses** — e.g. `await tenantScope.register(subscriptionRoutes);`.
|
|
599
|
-
NOT the outer `authScope`, and NOT the same public scope as
|
|
600
|
-
`subscribeHandler`. This is a READ of a specific subscription's status —
|
|
601
|
-
a MANAGEMENT-shaped action, so it inherits 10g's admin + tenancy guards
|
|
602
|
-
verbatim (`requireSiteAdmin` + `requireSingleTenantScope`, the fixed
|
|
603
|
-
single-tenant sentinel `DEFAULT_ORG_ID`). See the template's own header
|
|
604
|
-
comment for the full registration snippet.
|
|
605
|
-
4. Point the consumer at `@working-theory/ui`'s `PricingTable` (section) and
|
|
606
|
-
`SubscriptionStatus` (block) to compose the subscribe + status pages —
|
|
607
|
-
`PricingTable`'s `onSubscribe` POSTs to step 2's route + redirects to the
|
|
608
|
-
returned Stripe Checkout URL (mirrors `CheckoutForm`'s `onCheckout`
|
|
609
|
-
shape); `SubscriptionStatus` renders an already-fetched status (step 3's
|
|
610
|
-
route) via `PaymentStatusBadge` (reused from U1) plus a portal "Manage"
|
|
611
|
-
action pointing at the U6 billing-portal surface — this surface never
|
|
612
|
-
builds self-service plan-change/cancel itself.
|
|
613
|
-
5. Re-run 10c's live-key check before instantiating. State the CURRENT
|
|
614
|
-
behaviour to the consumer: `assertSandboxKey` is an ALLOW-LIST as of
|
|
615
|
-
AILK#3734 — only `sk_test_`, `pk_test_` and `rk_test_` are accepted, so
|
|
616
|
-
every live key including a restricted one is refused. No surface header
|
|
617
|
-
carries the pre-#3734 deny-list caveat any more (AILK#3765 retired the
|
|
618
|
-
last of them); if you find one, it is stale — fix it rather than repeat it.
|
|
619
|
-
6. **Multi-tenant module — same refusal as invoices, same reason.** v1 has
|
|
620
|
-
ONE deployment-wide `STRIPE_SECRET_KEY` and no per-organization Stripe
|
|
621
|
-
customer namespace, so a subscription carries no org attribution.
|
|
622
|
-
`subscriptionRoutes`'s `requireSingleTenantScope` guard therefore admits
|
|
623
|
-
ONLY the deployment's own operator org and hard-refuses every other org's
|
|
624
|
-
admin with a 403 `tenancy_unsupported`, before any Stripe call. Tell the
|
|
625
|
-
consumer plainly: subscription-status reading is an OPERATOR surface at
|
|
626
|
-
v1, not a per-tenant self-service one — the same disclosed limitation as
|
|
627
|
-
10g's invoice surface (see
|
|
628
|
-
`docs/research/commerce-template-audit/subscriptions.md`).
|
|
629
|
-
7. **No subscription webhook is scaffolded at v1** — the status read is
|
|
630
|
-
retrieve-on-view by the subscription's own Stripe id (mirrors 10g's
|
|
631
|
-
`?invoiceId=` shape with `?subscriptionId=`), not webhook-driven. If a
|
|
632
|
-
future need requires webhook-driven status (e.g. proactive
|
|
633
|
-
past-due notices), that is a new unit of work, not a tweak to this
|
|
634
|
-
surface.
|
|
635
|
-
|
|
636
|
-
**10i — Portal surface, shipped (FEAT-023 U6).** When the consumer picks
|
|
637
|
-
"Portal" at 10b, this surface IS available. It is the smallest FEAT-023
|
|
638
|
-
surface (spec §1) — AILK owns only the entry, Stripe hosts the portal
|
|
639
|
-
itself:
|
|
640
|
-
|
|
641
|
-
1. Instantiate `templates/portal-route.template.ts` into the consumer's
|
|
642
|
-
`apps/api` — mirrors 10d's `stripe-config.template.ts` instantiation. It
|
|
643
|
-
exports `portalRoutes` (a Fastify plugin: a single `POST
|
|
644
|
-
/v1/commerce/portal-session`), built on
|
|
645
|
-
`@working-theory/stripe-client`'s `createBillingPortalSession`.
|
|
646
|
-
2. **Register `portalRoutes` inside the SAME tenant-guarded scope 10g's
|
|
647
|
-
`invoiceRoutes` uses** — e.g. `await tenantScope.register(portalRoutes);`.
|
|
648
|
-
NOT the outer `authScope`. Starting a billing-portal session hands the
|
|
649
|
-
caller a live link into a customer's full billing surface, so — like
|
|
650
|
-
10h's `subscriptionRoutes` status read — this is a MANAGEMENT-shaped
|
|
651
|
-
action, never public/anonymous: it inherits 10g's admin + tenancy guards
|
|
652
|
-
verbatim (`requireSiteAdmin` + `requireSingleTenantScope`, the fixed
|
|
653
|
-
single-tenant sentinel `DEFAULT_ORG_ID`). See the template's own header
|
|
654
|
-
comment for the full registration snippet.
|
|
655
|
-
3. Point the consumer at `@working-theory/ui`'s `PortalEntry` (block) to
|
|
656
|
-
compose a standalone billing-management entry point, AND wire it as the
|
|
657
|
-
target of 10h's `SubscriptionStatus` `manageAction` (the U5↔U6 seam,
|
|
658
|
-
spec § design decision) — both `PortalEntry`'s `onManage` and
|
|
659
|
-
`SubscriptionStatus`'s `manageAction.onManage` POST to step 1's route
|
|
660
|
-
with `{ customerId }` and, on a `200` response, set
|
|
661
|
-
`window.location.href` to the returned `url`. Pass `testMode` (always
|
|
662
|
-
`true` at v1 — sandbox-only).
|
|
663
|
-
4. Re-run 10c's live-key check before instantiating. Note the guard's
|
|
664
|
-
current state (AILK#3734, now an allow-list) — restate the CURRENT state
|
|
665
|
-
to the consumer, same as 10h.
|
|
666
|
-
5. **Multi-tenant module — same refusal as invoices/subscriptions, same
|
|
667
|
-
reason.** v1 has ONE deployment-wide `STRIPE_SECRET_KEY` and no
|
|
668
|
-
per-organization Stripe customer namespace, so a `customerId` carries no
|
|
669
|
-
org attribution. `portalRoutes`'s `requireSingleTenantScope` guard
|
|
670
|
-
therefore admits ONLY the deployment's own operator org and hard-refuses
|
|
671
|
-
every other org's admin with a 403 `tenancy_unsupported`, before any
|
|
672
|
-
Stripe call. Tell the consumer plainly: billing-portal sessions are an
|
|
673
|
-
OPERATOR surface at v1, not a per-tenant self-service one — the same
|
|
674
|
-
disclosed limitation as 10g/10h (see
|
|
675
|
-
`docs/research/commerce-template-audit/portal.md`).
|
|
676
|
-
6. **The return URL is fixed, server-derived, never caller-supplied** — a
|
|
677
|
-
single `PORTAL_RETURN_PATH` composed from `FRONTEND_URL` (the template's
|
|
678
|
-
own `SCAFFOLD-TIME EDIT` if the consumer's account/billing page lives
|
|
679
|
-
elsewhere), mirroring `apps/api/src/routes/billing/portal.ts`'s existing
|
|
680
|
-
convention. There is no caller-chosen destination to validate, unlike
|
|
681
|
-
checkout's `successUrl`/`cancelUrl`.
|
|
682
|
-
7. **No portal-session webhook is scaffolded at v1** — mint-on-demand by
|
|
683
|
-
the caller-supplied `customerId`, not webhook-driven, mirroring 10h's
|
|
684
|
-
escalation-boundary posture.
|
|
685
|
-
|
|
686
|
-
## Output style
|
|
687
|
-
|
|
688
|
-
- **Two questions, two paths.** Don't lead with Stripe assumptions.
|
|
689
|
-
- **Live-key checks happen first** for the Stripe path. Always.
|
|
690
|
-
- **Show + ask on conflicts.** Never silently skip or auto-overwrite.
|
|
691
|
-
- **Source the consumer owns.** The link path emits inline source, not
|
|
692
|
-
an `@working-theory/ui` import. The consumer can edit any of it post-emit.
|
|
693
|
-
- **`lookup_key` derivations are visible.** Show the derivation; let
|
|
694
|
-
the consumer override before any API call.
|
|
695
|
-
|
|
696
|
-
## What this skill does NOT do
|
|
697
|
-
|
|
698
|
-
- Live-mode Stripe anything. Hook-blocked globally; SKILL refuses.
|
|
699
|
-
- Bundle marketplace logos (Amazon, Etsy, Shopify, eBay, Gumroad).
|
|
700
|
-
AILK doesn't ship those SVGs (trademark-redistribution concern). The
|
|
701
|
-
consumer drops their own SVG into `public/logos/` and passes
|
|
702
|
-
`iconSrc` to `<MarketplaceCTA>`. See
|
|
703
|
-
`references/marketplace-brand-pages.md` for official brand-kit
|
|
704
|
-
sources.
|
|
705
|
-
- Build a pricing page. That's `scaffold-page` (#362).
|
|
706
|
-
- Register a production webhook endpoint. That's `provision-config`
|
|
707
|
-
(#367) or future deploy skill.
|
|
708
|
-
- Use `pricebook.ts` or env-var price mapping. AILK uses
|
|
709
|
-
`lookup_key`s as the source of truth — they're stable across price
|
|
710
|
-
recreates.
|
|
711
|
-
- Auto-invoke other skills. Always points at the next skill by name;
|
|
712
|
-
consumer drives.
|
|
713
|
-
- Guess entitlement conversationally. The `feature:commerce`
|
|
714
|
-
surface-selection branch (Step 10) checks the resolved license via
|
|
715
|
-
`hasFeature("commerce", license)` — never a verbal "I'm on Pro"
|
|
716
|
-
claim. No resolvable license → not entitled (fail-closed).
|
|
717
|
-
- Build a payment surface itself for a choice OTHER than checkout
|
|
718
|
-
(U3–U6). This skill's Step 10 checks entitlement, offers the choice,
|
|
719
|
-
and dispatches — the surface scaffolders own the actual page/route.
|
|
720
|
-
Checkout (one-time) is the one exception: Step 10e-i wires it inline
|
|
721
|
-
(FEAT-023 U2).
|
|
722
|
-
|
|
723
|
-
## References
|
|
724
|
-
|
|
725
|
-
- `references/marketplace-brand-pages.md` — links to official brand
|
|
726
|
-
kits (Amazon, Etsy, Shopify, eBay, Gumroad). Used by the link path
|
|
727
|
-
when the consumer wants logos. AILK does not bundle the SVGs.
|
|
728
|
-
- `references/lookup-keys-template.md` — template the skill instantiates
|
|
729
|
-
per session, lists every product the consumer created with its
|
|
730
|
-
`lookup_key` and price.
|
|
731
|
-
- `templates/marketplace-cta-route.tsx.tmpl` — emitted route source
|
|
732
|
-
the link path writes. Includes the inline `<MarketplaceCTA>`
|
|
733
|
-
component definition (not a separate package).
|
|
734
|
-
- `templates/stripe-config.template.ts` — shared Stripe surface config
|
|
735
|
-
(env contract + `assertSandboxKey` test-mode guard) every U2–U6
|
|
736
|
-
surface scaffolder instantiates (Step 10d, FEAT-023 U1).
|
|
737
|
-
- `templates/checkout-route.template.ts` — the one-time checkout POST
|
|
738
|
-
route (`mode: "payment"`, `stripe-client/checkout.ts`), instantiated
|
|
739
|
-
by Step 10e-i (FEAT-023 U2).
|
|
740
|
-
- `templates/payment-link-route.template.ts` — the payment-link
|
|
741
|
-
MANAGEMENT route (list / create / archive), instantiated by Step
|
|
742
|
-
10e-ii (FEAT-023 U3). Authenticated + site-admin only, registered
|
|
743
|
-
inside the auth scope — see 10e-ii; its posture is the opposite of
|
|
744
|
-
the public checkout route's.
|
|
745
|
-
- `templates/invoice-route.template.ts` — the Invoice surface's admin-only
|
|
746
|
-
Fastify plugin (create/send/list/retrieve), instantiated at Step 10g
|
|
747
|
-
(FEAT-023 U4, shipped). Registration inside `apps/api`'s tenant-guarded
|
|
748
|
-
scope is REQUIRED — see the template's own header comment.
|
|
749
|
-
- `templates/subscription-route.template.ts` — the Subscription surface's
|
|
750
|
-
`subscribeHandler` (public, rate-limited) + `subscriptionRoutes` (admin +
|
|
751
|
-
tenant-guarded status read), instantiated at Step 10h (FEAT-023 U5,
|
|
752
|
-
shipped). The two handlers have OPPOSITE auth postures — see the
|
|
753
|
-
template's own header comment for both registration points.
|
|
754
|
-
- `templates/portal-route.template.ts` — the Portal surface's admin +
|
|
755
|
-
tenant-guarded `portalRoutes` (mints a billing-portal session for a
|
|
756
|
-
caller-supplied `customerId`), instantiated at Step 10i (FEAT-023 U6,
|
|
757
|
-
shipped). Registration inside `apps/api`'s tenant-guarded scope is
|
|
758
|
-
REQUIRED — see the template's own header comment.
|
|
759
|
-
|
|
760
|
-
## Coordination
|
|
761
|
-
|
|
762
|
-
- **`scaffold-page` (#362)** — pricing UI. After this skill creates
|
|
763
|
-
products and surfaces `lookup_key`s, the consumer scaffolds a
|
|
764
|
-
`/pricing` page that consumes them.
|
|
765
|
-
- **`provision-config` (#367)** — production webhook setup, deploy
|
|
766
|
-
config, persistent leads (related infra). v1 is dev-mode only;
|
|
767
|
-
prod webhook lives there.
|
|
768
|
-
- **Stripe MCP** — used for product/price/lookup_key creation. Should
|
|
769
|
-
already be configured in `.claude/settings.json` from the consumer
|
|
770
|
-
scaffold. If missing, surface a clear "Stripe MCP not configured"
|
|
771
|
-
error and stop; do not invent the configuration.
|
|
772
|
-
- **`@working-theory/stripe-client`** — wrapper for Stripe SDK. The skill does
|
|
773
|
-
not import this directly (the skill is a prompt/template, not
|
|
774
|
-
runtime code). The consumer's `apps/api/routes/checkout/sessions.ts`
|
|
775
|
-
(generic primitive) and the Step-10e-i-instantiated
|
|
776
|
-
`checkout-route.template.ts` (one-time surface) use it.
|
|
777
|
-
`assertSandboxKey` is reused by `stripe-config.template.ts`.
|
|
778
|
-
- **`@working-theory/license-client`** — `hasFeature("commerce", license)`
|
|
779
|
-
is the entitlement read Step 10a consults (static-mode, sync, no
|
|
780
|
-
network). The skill does not import this directly either — it
|
|
781
|
-
describes the check a consumer's own license-aware tooling performs.
|
|
782
|
-
- **FEAT-023 U2–U6 (checkout · pay-by-link · invoice · subscription ·
|
|
783
|
-
portal)** — the five payment surfaces Step 10e dispatches to. Checkout
|
|
784
|
-
(one-time) shipped in U2 (10e-i); pay-by-link shipped in U3 (10e-ii);
|
|
785
|
-
invoice shipped in U4 (10g); subscription shipped in U5 (10h); portal
|
|
786
|
-
shipped in U6 (10i).
|
|
787
|
-
- **FEAT-023 U4 (invoice)** — shipped (Step 10g). `@working-theory/stripe-client`'s
|
|
788
|
-
`invoice.ts` (create/send/list/retrieve) + `@working-theory/ui`'s
|
|
789
|
-
`InvoiceList`/`InvoiceDetail`/`InvoiceForm` compose the surface; the
|
|
790
|
-
route template lives at `templates/invoice-route.template.ts`.
|
|
791
|
-
- **FEAT-023 U5 (subscription)** — shipped (Step 10h).
|
|
792
|
-
`@working-theory/stripe-client`'s `checkout.ts` (`createCheckoutSession`
|
|
793
|
-
`mode: "subscription"` + `retrieveSubscriptionStatus`) +
|
|
794
|
-
`@working-theory/ui`'s `PricingTable`/`SubscriptionStatus` compose the
|
|
795
|
-
surface; the route template lives at
|
|
796
|
-
`templates/subscription-route.template.ts`.
|
|
797
|
-
- **FEAT-023 U6 (portal)** — shipped (Step 10i). `@working-theory/stripe-client`'s
|
|
798
|
-
`billing-portal.ts` (`createBillingPortalSession`) + `@working-theory/ui`'s
|
|
799
|
-
`PortalEntry` (reuses U1's `StripeActionButton`) compose the surface — the
|
|
800
|
-
target of U5's `SubscriptionStatus` "Manage" action; the route template
|
|
801
|
-
lives at `templates/portal-route.template.ts`.
|
|
802
|
-
- **`@working-theory/ui`'s `MoneyAmount` / `PaymentStatusBadge` /
|
|
803
|
-
`StripeActionButton`** — the shared presentational components every
|
|
804
|
-
U2–U6 surface composes (Step 10f, FEAT-023 U1).
|
|
805
|
-
- **`@working-theory/ui`'s `CheckoutForm` / `PaymentResult`** — the
|
|
806
|
-
one-time checkout surface's own capability-bearing blocks (FEAT-023
|
|
807
|
-
U2), composed at Step 10e-i steps 3–4.
|