create-ailk 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -8
- package/component-catalog.json +336 -27
- package/component-catalog.md +25 -12
- package/dist/lib/api-boot-assertion.d.ts +46 -0
- package/dist/lib/api-boot-assertion.js +70 -0
- package/dist/lib/import-graph.d.ts +29 -0
- package/dist/lib/import-graph.js +140 -0
- package/dist/module-architecture.d.ts +1 -0
- package/dist/module-architecture.js +253 -127
- package/dist/surfaces.js +105 -7
- package/dist/template-manifest.d.ts +33 -0
- package/dist/template-manifest.js +99 -16
- package/dist/template-names.d.ts +1 -1
- package/dist/template-names.js +1 -0
- package/package.json +3 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +22 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +9 -2
- package/site-templates/marketing-site/ailk-template.json +1 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.json +2 -2
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +1 -0
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.json +10 -2
- package/site-templates/minimalism-site/TEMPLATE-MANIFEST.md +47 -35
- package/site-templates/minimalism-site/ailk-template.json +2 -1
- package/site-templates/minimalism-site/assets/wordmark-placeholder.svg +28 -0
- package/site-templates/minimalism-site/content/en/HomePage/home.mdx +49 -19
- package/site-templates/simple-service/TEMPLATE-MANIFEST.json +240 -0
- package/site-templates/simple-service/TEMPLATE-MANIFEST.md +278 -0
- package/site-templates/simple-service/ailk-template.json +10 -0
- package/site-templates/simple-service/content/en/AboutPage/about.mdx +38 -0
- package/site-templates/simple-service/content/en/ContactPage/contact.mdx +67 -0
- package/site-templates/simple-service/content/en/FAQPage/faq.mdx +56 -0
- package/site-templates/simple-service/content/en/HomePage/home.mdx +85 -0
- package/site-templates/simple-service/content/en/Service/landscape-design.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/lawn-maintenance.mdx +40 -0
- package/site-templates/simple-service/content/en/Service/seasonal-cleanup.mdx +40 -0
- package/site-templates/simple-service/content/en/ServicesList/services.mdx +72 -0
- package/site-templates/simple-service/provenance.json +4 -0
- package/templates/.env.example +72 -7
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +8 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +12 -0
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +12 -0
- package/templates/apps/api/.env.example +13 -0
- package/templates/apps/api/CLAUDE.md +26 -2
- package/templates/apps/api/jest.config.cjs +3 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +407 -0
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +83 -7
- package/templates/apps/api/src/__tests__/server.test.ts +471 -0
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +45 -0
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +2 -0
- package/templates/apps/api/src/config/index.ts +62 -0
- package/templates/apps/api/src/config/modules.ts +7 -3
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +238 -2
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +143 -0
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +23 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +125 -0
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +20 -0
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +317 -0
- package/templates/apps/api/src/lib/__tests__/site.test.ts +238 -0
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +75 -0
- package/templates/apps/api/src/lib/auth-email-budget.ts +160 -0
- package/templates/apps/api/src/lib/content-adapter.ts +57 -31
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +76 -0
- package/templates/apps/api/src/lib/error-serializer.ts +23 -3
- package/templates/apps/api/src/lib/outbound-http.ts +7 -2
- package/templates/apps/api/src/lib/require-commerce-claim.ts +250 -0
- package/templates/apps/api/src/lib/site.ts +208 -9
- package/templates/apps/api/src/lib/stripe.ts +66 -0
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +182 -0
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +42 -0
- package/templates/apps/api/src/middleware/rate-limit.ts +7 -1
- package/templates/apps/api/src/openapi/spec.ts +543 -2
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +48 -0
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +309 -0
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +123 -0
- package/templates/apps/api/src/routes/auth/transactional-email.ts +273 -0
- package/templates/apps/api/src/routes/auth/user.ts +61 -0
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +434 -0
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +888 -0
- package/templates/apps/api/src/routes/checkout/index.ts +34 -0
- package/templates/apps/api/src/routes/checkout/session.ts +139 -0
- package/templates/apps/api/src/routes/checkout/sessions.ts +197 -0
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +554 -0
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +108 -0
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +450 -0
- package/templates/apps/api/src/routes/content/get-page.ts +94 -12
- package/templates/apps/api/src/routes/content/index.ts +13 -73
- package/templates/apps/api/src/routes/content/list-pages.ts +15 -3
- package/templates/apps/api/src/routes/content/rate-limit.ts +15 -6
- package/templates/apps/api/src/routes/content/search.ts +129 -0
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +447 -31
- package/templates/apps/api/src/routes/leads/index.ts +100 -13
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +351 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +5 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +4 -1
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +58 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +107 -6
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +87 -2
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +51 -0
- package/templates/apps/api/src/routes/project-listings/start.ts +5 -1
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +686 -0
- package/templates/apps/api/src/routes/requests/confirm.ts +601 -0
- package/templates/apps/api/src/routes/requests/index.ts +54 -0
- package/templates/apps/api/src/routes/webhooks/README.md +265 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +749 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +330 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +262 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +285 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +213 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +301 -0
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +648 -0
- package/templates/apps/api/src/routes/webhooks/index.ts +89 -0
- package/templates/apps/api/src/routes/webhooks/stripe.ts +1399 -0
- package/templates/apps/api/src/server.ts +439 -49
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +39 -0
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +217 -40
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +620 -0
- package/templates/apps/api/src/services/__tests__/consent.test.ts +2 -0
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +300 -5
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +20 -0
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +368 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +160 -0
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +166 -0
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +154 -0
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +137 -0
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +444 -0
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +238 -0
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +78 -0
- package/templates/apps/api/src/services/abandon-email.ts +8 -2
- package/templates/apps/api/src/services/aeo-score.ts +49 -12
- package/templates/apps/api/src/services/audit-log.ts +668 -0
- package/templates/apps/api/src/services/consent.ts +18 -0
- package/templates/apps/api/src/services/default-tenant.ts +70 -31
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +141 -13
- package/templates/apps/api/src/services/dunning-policy.ts +294 -0
- package/templates/apps/api/src/services/entitlements.ts +80 -0
- package/templates/apps/api/src/services/lead-promotion.ts +13 -14
- package/templates/apps/api/src/services/lead-routing.ts +161 -2
- package/templates/apps/api/src/services/leads-inbox.ts +23 -2
- package/templates/apps/api/src/services/listing-config.ts +27 -9
- package/templates/apps/api/src/services/project-listings.ts +201 -6
- package/templates/apps/api/src/services/public-site-resolver.ts +106 -0
- package/templates/apps/api/src/services/request-confirm.ts +646 -0
- package/templates/apps/api/src/services/revalidation.ts +173 -0
- package/templates/apps/api/src/services/verified-email.ts +71 -0
- package/templates/apps/api/src/vercel-handler.ts +75 -35
- package/templates/apps/mcp/.env.example +17 -0
- package/templates/apps/mcp/CLAUDE.md +57 -24
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +6 -0
- package/templates/apps/mcp/__tests__/audit.test.ts +137 -8
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +79 -36
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +111 -2
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +82 -2
- package/templates/apps/mcp/__tests__/http-transport.test.ts +541 -2
- package/templates/apps/mcp/__tests__/index.test.ts +39 -0
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +143 -0
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +111 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +65 -0
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +188 -0
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +165 -0
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +124 -0
- package/templates/apps/mcp/__tests__/list_pages.test.ts +116 -0
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +7 -1
- package/templates/apps/mcp/__tests__/parity.test.ts +10 -7
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +89 -22
- package/templates/apps/mcp/__tests__/read_content.test.ts +322 -1
- package/templates/apps/mcp/__tests__/search_content.test.ts +105 -2
- package/templates/apps/mcp/__tests__/server.test.ts +67 -14
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +252 -0
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +300 -123
- package/templates/apps/mcp/jest.config.cjs +13 -0
- package/templates/apps/mcp/package.json +2 -0
- package/templates/apps/mcp/src/abuse/index.ts +159 -24
- package/templates/apps/mcp/src/config/modules.ts +10 -5
- package/templates/apps/mcp/src/http-transport.ts +102 -29
- package/templates/apps/mcp/src/index.ts +73 -11
- package/templates/apps/mcp/src/lib/audit-single-page.ts +90 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +94 -0
- package/templates/apps/mcp/src/lib/content-adapter.ts +113 -0
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +139 -0
- package/templates/apps/mcp/src/lib/prisma.ts +39 -0
- package/templates/apps/mcp/src/lib/public-workspace.ts +133 -0
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +59 -0
- package/templates/apps/mcp/src/server.ts +59 -35
- package/templates/apps/mcp/src/tools/audit.ts +26 -2
- package/templates/apps/mcp/src/tools/get_business_hours.ts +4 -3
- package/templates/apps/mcp/src/tools/get_faq.ts +4 -3
- package/templates/apps/mcp/src/tools/get_locations.ts +5 -3
- package/templates/apps/mcp/src/tools/get_offerings.ts +16 -4
- package/templates/apps/mcp/src/tools/get_pricing.ts +25 -6
- package/templates/apps/mcp/src/tools/get_site_info.ts +4 -3
- package/templates/apps/mcp/src/tools/index.ts +6 -4
- package/templates/apps/mcp/src/tools/list_pages.ts +4 -3
- package/templates/apps/mcp/src/tools/purchase.ts +128 -45
- package/templates/apps/mcp/src/tools/read_content.ts +20 -6
- package/templates/apps/mcp/src/tools/search_content.ts +11 -4
- package/templates/apps/web/.env.example +71 -5
- package/templates/apps/web/CLAUDE.md +44 -0
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +18 -2
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +11 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +282 -0
- package/templates/apps/web/app/[locale]/(auth)/__tests__/password-reset.test.tsx +515 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/forgot-password-card.tsx +89 -0
- package/templates/apps/web/app/[locale]/(auth)/forgot-password/page.tsx +109 -0
- package/templates/apps/web/app/[locale]/(auth)/layout.tsx +92 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/page.tsx +135 -0
- package/templates/apps/web/app/[locale]/(auth)/reset-password/reset-password-card.tsx +109 -0
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/page.test.tsx +48 -2
- package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/page.tsx +20 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +125 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +96 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +101 -0
- package/templates/apps/web/app/[locale]/(auth)/use-auth-card-flow.ts +102 -0
- package/templates/apps/web/app/[locale]/(auth)/verify-email/page.tsx +87 -0
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +20 -10
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +84 -0
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +58 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +63 -8
- package/templates/apps/web/app/[locale]/(authed)/layout.tsx +18 -1
- package/templates/apps/web/app/[locale]/cancellation/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/layout.tsx +133 -87
- package/templates/apps/web/app/[locale]/purchase/__tests__/cancel.test.tsx +162 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/module-scope.test.ts +68 -0
- package/templates/apps/web/app/[locale]/purchase/__tests__/success.test.tsx +361 -0
- package/templates/apps/web/app/[locale]/purchase/cancel/page.tsx +115 -0
- package/templates/apps/web/app/[locale]/purchase/success/page.tsx +307 -0
- package/templates/apps/web/app/[locale]/returns/page.tsx +44 -0
- package/templates/apps/web/app/[locale]/shipping/page.tsx +44 -0
- package/templates/apps/web/app/api/revalidate/__tests__/route.test.ts +670 -0
- package/templates/apps/web/app/api/revalidate/route.ts +194 -0
- package/templates/apps/web/auth.ts +407 -8
- package/templates/apps/web/jest.config.cjs +8 -4
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +214 -0
- package/templates/apps/web/lib/__tests__/auth-routes.test.ts +74 -0
- package/templates/apps/web/lib/__tests__/meeting-slugs.test.ts +72 -0
- package/templates/apps/web/lib/__tests__/structured-data.test.tsx +18 -1
- package/templates/apps/web/lib/auth-email.ts +318 -0
- package/templates/apps/web/lib/auth-env.ts +85 -0
- package/templates/apps/web/lib/auth-routes.ts +55 -0
- package/templates/apps/web/lib/meeting-slugs.ts +53 -0
- package/templates/apps/web/lib/telemetry-scrub.ts +5 -1
- package/templates/apps/web/messages/en.json +187 -0
- package/templates/apps/web/package.json +2 -1
- package/templates/apps/web/src/lib/commerce-claim.ts +33 -0
- package/templates/apps/web/src/lib/commerce-sections.ts +61 -0
- package/templates/content/en/CancellationPolicyPage/cancellation.mdx +108 -0
- package/templates/content/en/ReturnsPolicyPage/returns.mdx +108 -0
- package/templates/content/en/ShippingPolicyPage/shipping.mdx +108 -0
- package/templates/database/CHANGELOG.md +142 -0
- package/templates/database/README.md +10 -3
- package/templates/database/__tests__/appointment.db.test.ts +106 -0
- package/templates/database/__tests__/artifact-content-store.db.test.ts +316 -0
- package/templates/database/__tests__/composite-fence.db.test.ts +206 -0
- package/templates/database/__tests__/fence-fan-out.db.test.ts +711 -0
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +79 -0
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +100 -0
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +263 -0
- package/templates/database/__tests__/prefixed-ids.db.test.ts +85 -9
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +111 -0
- package/templates/database/__tests__/tenancy.test.ts +141 -0
- package/templates/database/config/schema.prisma +79 -2
- package/templates/database/content/schema.prisma +87 -11
- package/templates/database/inbox/schema.prisma +452 -9
- package/templates/database/jest.config.cjs +5 -3
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +15 -0
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +158 -0
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +40 -0
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +67 -0
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +70 -0
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +181 -0
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +172 -0
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +41 -0
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +185 -0
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +70 -0
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +57 -0
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +68 -0
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +1067 -0
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +210 -0
- package/templates/database/package.json +1 -1
- package/templates/database/tenancy.ts +86 -1
- package/templates/database/tsconfig.json +1 -0
- package/versions.json +34 -34
- package/templates/apps/api/src/routes/content/__tests__/create.test.ts +0 -253
- package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +0 -215
- package/templates/apps/api/src/routes/content/__tests__/update.test.ts +0 -266
- package/templates/apps/api/src/routes/content/create.ts +0 -115
- package/templates/apps/api/src/routes/content/delete.ts +0 -82
- package/templates/apps/api/src/routes/content/update.ts +0 -112
- package/templates/apps/mcp/__tests__/create_page.test.ts +0 -202
- package/templates/apps/mcp/__tests__/delete_page.test.ts +0 -177
- package/templates/apps/mcp/__tests__/update_page.test.ts +0 -196
- package/templates/apps/mcp/src/tools/create_page.ts +0 -96
- package/templates/apps/mcp/src/tools/delete_page.ts +0 -74
- package/templates/apps/mcp/src/tools/update_page.ts +0 -92
- package/templates/apps/web/app/[locale]/sign-in/sign-in-card.tsx +0 -123
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/__tests__/next-path.test.ts +0 -0
- /package/templates/apps/web/app/[locale]/{sign-in → (auth)/sign-in}/next-path.ts +0 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Webhooks — `apps/api/src/routes/webhooks/`
|
|
2
|
+
|
|
3
|
+
Stripe webhook receiver at `POST /v1/webhooks/stripe`. Verifies
|
|
4
|
+
signatures, dispatches events via a `switch`, and fires server-side
|
|
5
|
+
telemetry. Handles `customer.created`, `checkout.session.completed`, and
|
|
6
|
+
`checkout.session.expired` (the last two also stamp the recommender's
|
|
7
|
+
buyer row — below).
|
|
8
|
+
|
|
9
|
+
## Commerce claim — signature first, claim second (D-64, #5796)
|
|
10
|
+
|
|
11
|
+
The receiver is a base-tree **primitive**: it registers on every build, with
|
|
12
|
+
no `isModuleEnabled("commerce")` gate, and decides whether to act on the JWT
|
|
13
|
+
`features[]` claim `commerce` (`lib/require-commerce-claim.ts`, read at call
|
|
14
|
+
time through `checkLicense()`). The order inside `stripe.ts` is fixed and
|
|
15
|
+
load-bearing: **the Stripe signature is verified BEFORE the claim is
|
|
16
|
+
consulted.** An unsigned body therefore never reaches the claim check on its
|
|
17
|
+
own merits, and the two unclaimed outcomes are:
|
|
18
|
+
|
|
19
|
+
- **Signed, verifiable event, licence read and positively unclaimed** →
|
|
20
|
+
**200 `{received: true}`**, the event dropped with a structured `warn` log
|
|
21
|
+
line (`outcome: "dropped_no_commerce_claim"`). A 4xx here would have Stripe
|
|
22
|
+
retry the event for days against a site that turned commerce off. It is
|
|
23
|
+
logged at `warn`, not `info`: a discarded Stripe event is never routine.
|
|
24
|
+
- **Missing secret / non-Buffer body / missing or invalid signature, licence
|
|
25
|
+
read and unclaimed** → the **module-disabled 404**, byte-identical to a
|
|
26
|
+
build that never carried the commerce module. A site without the claim is
|
|
27
|
+
indistinguishable from a site without the module, which is the point.
|
|
28
|
+
|
|
29
|
+
With the claim, every mapping in the § above is exactly what it was before
|
|
30
|
+
the gate existed.
|
|
31
|
+
|
|
32
|
+
### The claim is a TRI-state, and `undetermined` is never a drop
|
|
33
|
+
|
|
34
|
+
Security review (2026-09-16, High) against the first cut of this gate: a
|
|
35
|
+
boolean `hasCommerceClaim()` returned one `false` both for "the licence says
|
|
36
|
+
no" and for "the licence could not be read". On the second, a **live** commerce
|
|
37
|
+
site whose read merely failed would ack a signed `checkout.session.completed`
|
|
38
|
+
200 and lose it — Stripe records success, never redelivers, and the charge is
|
|
39
|
+
never mapped to a customer. The pre-#5796 module flag was build-time and its
|
|
40
|
+
off state 404'd, which Stripe retried; the runtime gate introduced the loss.
|
|
41
|
+
|
|
42
|
+
So `resolveCommerceClaim()` answers `claimed | unclaimed | undetermined`, and
|
|
43
|
+
**`undetermined` → 503 with `Retry-After: 300`**, which makes Stripe queue the
|
|
44
|
+
event and redeliver it once the read succeeds — well inside Stripe's three-day
|
|
45
|
+
redelivery window. The drop and the hold are both logged at `warn`
|
|
46
|
+
(`outcome: "dropped_no_commerce_claim"` and `outcome: "license_undetermined"`):
|
|
47
|
+
neither a discarded nor a held Stripe event is routine.
|
|
48
|
+
|
|
49
|
+
`undetermined` covers every read that established no "no" (operator-disposed,
|
|
50
|
+
round 2):
|
|
51
|
+
|
|
52
|
+
- **`checkLicense()` threw.**
|
|
53
|
+
- **A revoked licence** (`revoked === true`). It keeps verifying locally until
|
|
54
|
+
JWT `exp` (ADR 0014), so this is a paying commerce site mid-window whose
|
|
55
|
+
events are still real. Closing the gate is right; dropping the events is the
|
|
56
|
+
review's High under another label.
|
|
57
|
+
- **A licence is configured yet resolves free** — the expired case, and any
|
|
58
|
+
other JWT that no longer verifies. `checkLicense()` returns the _same_
|
|
59
|
+
`FREE_RESULT` for expired, invalid and keyless alike, so the only signal that
|
|
60
|
+
separates them is whether a licence is CONFIGURED at all. The gate tests that
|
|
61
|
+
the same way `check-license.ts` resolves a token — `AILK_LICENSE_KEY`, else
|
|
62
|
+
the refresh storage file — for presence only: it parses no JWT and
|
|
63
|
+
re-implements no verification. Reading the storage file matters, because a
|
|
64
|
+
deployment can be fully licensed with no env key at all; an env-only test
|
|
65
|
+
read such a site as keyless, so its EXPIRED licence dropped already-charged
|
|
66
|
+
events.
|
|
67
|
+
- **The licence storage file is UNREADABLE** (#5820) — EACCES, EIO, EMFILE, a
|
|
68
|
+
directory at its path, a filesystem that went away. See below.
|
|
69
|
+
|
|
70
|
+
`unclaimed` is therefore reserved for a positively-established "no": a licence
|
|
71
|
+
that reads successfully and simply lacks the claim, or a keyless deployment
|
|
72
|
+
that configures no licence at all — where "no licence at all" now means the
|
|
73
|
+
storage file was positively read as ABSENT, not merely that reading it failed.
|
|
74
|
+
|
|
75
|
+
### `unreadable` is not `absent` (#5820)
|
|
76
|
+
|
|
77
|
+
The gate reads the storage file through `readStoredJwtState()`
|
|
78
|
+
(`packages/license/src/storage.ts`), which answers **`absent` · `unreadable` ·
|
|
79
|
+
`present`**:
|
|
80
|
+
|
|
81
|
+
- **`absent`** — ENOENT, or a file holding nothing but whitespace. The only
|
|
82
|
+
state that is evidence of an unlicensed site, and therefore the only one on
|
|
83
|
+
this path that may drop a signed event.
|
|
84
|
+
- **`unreadable`** — every other read failure. Evidence of nothing, so it
|
|
85
|
+
resolves `undetermined` → **503 + `Retry-After: 300`**, logged with the
|
|
86
|
+
errno `code` (`outcome: "license_storage_unreadable"`).
|
|
87
|
+
- **`present`** — the token; resolution continues exactly as for an env key.
|
|
88
|
+
|
|
89
|
+
This closes the last gap in the tri-state. `readStoredJwt()` catches every read
|
|
90
|
+
error and returns the same `null` it returns for "no file", so before this a
|
|
91
|
+
transient EACCES on a storage-licensed site read as keyless: a signed
|
|
92
|
+
`checkout.session.completed` in that window was acked 200, Stripe never
|
|
93
|
+
redelivered it, and the charge was never mapped. `readStoredJwt()`'s own
|
|
94
|
+
contract is unchanged — `checkLicense()` and every other caller still see one
|
|
95
|
+
`null` for both states — so only the gate's resolution moved.
|
|
96
|
+
|
|
97
|
+
**The checkout launcher does not hold.** It answers the module-disabled 404 on
|
|
98
|
+
both `unclaimed` and `undetermined`: a checkout that cannot be started loses
|
|
99
|
+
nothing, so there is no call to leak a "configured but unreadable" shape on it.
|
|
100
|
+
The webhook holds because by the time an event arrives the buyer's money has
|
|
101
|
+
already moved.
|
|
102
|
+
|
|
103
|
+
## How to add a new event handler
|
|
104
|
+
|
|
105
|
+
Add a `case` arm to the `switch` in `stripe.ts`:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
case "invoice.payment_succeeded": {
|
|
109
|
+
const invoice = event.data.object as { ... };
|
|
110
|
+
await fireTelemetryWithTimeout(request, "invoice-paid", { ... });
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Customer forks edit `stripe.ts` directly. v1 ships no pluggable handler
|
|
116
|
+
registry — if multiple forks request one, file an issue.
|
|
117
|
+
|
|
118
|
+
## Recommender purchase stamp (#4838)
|
|
119
|
+
|
|
120
|
+
A Checkout Session the recommender opened (`POST /v1/flows/:slug/checkout`,
|
|
121
|
+
#4841) carries `metadata.flowCheckoutId` — the `FlowCheckout` buyer row's
|
|
122
|
+
id. On `checkout.session.completed` the handler stamps that row
|
|
123
|
+
`status: "completed"` (+ `completedAt`, `stripeSessionId`,
|
|
124
|
+
`stripeCustomerId`); on `checkout.session.expired` it stamps
|
|
125
|
+
`status: "expired"`. Both are ONE guarded update —
|
|
126
|
+
`updateMany({ where: { id, status: "created" } })` — so the stamp is
|
|
127
|
+
at-most-once regardless of the in-memory store below: a replay, a restart
|
|
128
|
+
between deliveries, or a late `expired` after a `completed` matches zero
|
|
129
|
+
rows and logs `outcome: "stale_or_unknown"`. A charged `amount_total` that
|
|
130
|
+
differs from the quoted `amountMinor` is logged (`expected` / `received`)
|
|
131
|
+
and the row is still stamped — the quote is never overwritten. The stamp
|
|
132
|
+
runs beside the telemetry + billing tasks, never instead of them, and an
|
|
133
|
+
event without the key never touches `flowCheckout`. Always 200.
|
|
134
|
+
|
|
135
|
+
## Dunning (#6034)
|
|
136
|
+
|
|
137
|
+
Two arms record a per-subscriber lifecycle state so a failed renewal is no
|
|
138
|
+
longer silent:
|
|
139
|
+
|
|
140
|
+
- **`invoice.payment_failed`** — arms dunning for the `User` matched by the
|
|
141
|
+
invoice's Stripe customer id: the recorded state becomes `past_due`,
|
|
142
|
+
`dunningArmedAt` is stamped once (never moved while the subscription stays
|
|
143
|
+
failing), `accessState` stays `granted`. **This arm never revokes.**
|
|
144
|
+
- **`customer.subscription.updated`** — maps the narrowed Stripe status
|
|
145
|
+
(`services/dunning-policy.ts`'s reuse of
|
|
146
|
+
`@working-theory/stripe-client`'s `toSubscriptionLifecycleStatus`) onto the
|
|
147
|
+
same record. `active`/`trialing` return the entry to a clean state
|
|
148
|
+
(`dunningArmedAt: null`, `accessRevokesAt: null`) so a recovered
|
|
149
|
+
subscriber's read shows nothing. `past_due`/`unpaid` arm exactly as
|
|
150
|
+
`invoice.payment_failed` does. `canceled` is the **only revoking
|
|
151
|
+
transition** — access is revoked once a per-tenant grace period, computed
|
|
152
|
+
from the FIRST failure, has already passed; if it hasn't, access is
|
|
153
|
+
retained and a revocation is scheduled.
|
|
154
|
+
|
|
155
|
+
### The record
|
|
156
|
+
|
|
157
|
+
One `UserData` row per subscriber (`userId_key` unique), reserved key
|
|
158
|
+
`commerce.subscription_dunning`, holding a MAP keyed by Stripe subscription
|
|
159
|
+
id — one customer can hold more than one subscription, and a flat blob would
|
|
160
|
+
let a second subscription's `active` clobber the first's `past_due`:
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
{
|
|
164
|
+
"v": 1,
|
|
165
|
+
"subscriptions": {
|
|
166
|
+
"sub_1AbC…": {
|
|
167
|
+
"status": "past_due", // "past_due" | "active" | "canceled"
|
|
168
|
+
"accessState": "granted", // "granted" | "revoked"
|
|
169
|
+
"dunningArmedAt": "2026-09-18T04:12:00.000Z", // ISO-8601, or null
|
|
170
|
+
"accessRevokesAt": null, // ISO-8601, or null — set only on `canceled`
|
|
171
|
+
"lastEventAt": 1789000000, // Stripe event.created, seconds
|
|
172
|
+
"lastEventId": "evt_1XyZ…",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Capped at 50 subscriptions per customer, evicting the oldest SETTLED entries
|
|
179
|
+
(`status: "active"`, `accessState: "granted"`) first — an entry still being
|
|
180
|
+
dunned is never evicted mid-decision.
|
|
181
|
+
|
|
182
|
+
**No scheduler flips this.** `accessRevokesAt` is a lazily-evaluated
|
|
183
|
+
deadline; nothing wakes up to act on it. Two exported read-side predicates
|
|
184
|
+
(`services/dunning-policy.ts`) are the reader's contract instead:
|
|
185
|
+
|
|
186
|
+
- `isSubscriberRevoked(entry, now?)` — access is gone: either already stamped
|
|
187
|
+
`revoked`, or `now` has passed the scheduled `accessRevokesAt`.
|
|
188
|
+
- `isSubscriberActionRequired(entry, now?)` — the buyer must act (#6038's
|
|
189
|
+
banner predicate). Deliberately WIDER than `status === "past_due"`: a
|
|
190
|
+
subscription cancelled with its grace window still open
|
|
191
|
+
(`status: "canceled"`, `accessState: "granted"`, `accessRevokesAt`
|
|
192
|
+
non-null) is a buyer about to lose access who hasn't been told.
|
|
193
|
+
|
|
194
|
+
**Ordering, ties, and durable replay.** Every write is a compare-and-swap
|
|
195
|
+
whose `where` restates BOTH the observed `updatedAt` AND the observed
|
|
196
|
+
`value` — a concurrent writer makes the predicate miss rather than silently
|
|
197
|
+
overwrite. The SAME `event.id` replayed (even after a process restart, when
|
|
198
|
+
the in-memory `hasSeen` map has forgotten it) is a no-op; a strictly OLDER
|
|
199
|
+
event is stale; an EQUAL-timestamp write that would move the entry toward
|
|
200
|
+
LESS restriction (setting `status` to `active`, clearing `accessRevokesAt`,
|
|
201
|
+
or moving `accessState` from `revoked` to `granted`) is rejected as a stale
|
|
202
|
+
tie — this is what stops a reordered delivery from permanently un-revoking a
|
|
203
|
+
cancelled subscriber. A genuinely newer event is always admitted, including
|
|
204
|
+
one that restores a terminal entry to `active`/`granted` (a real
|
|
205
|
+
reactivation).
|
|
206
|
+
|
|
207
|
+
### The grace policy
|
|
208
|
+
|
|
209
|
+
`dunningPolicyFor(siteId)` reads a `SiteSetting` row keyed
|
|
210
|
+
`commerce.dunning_policy:<siteId>` (`SiteSetting.key` has no `siteId`
|
|
211
|
+
column of its own, so per-tenant scoping lives in the key string — never an
|
|
212
|
+
unsuffixed fallback). Absent, malformed, non-numeric, negative, non-finite,
|
|
213
|
+
or out-of-range (`> 90`) all resolve to a documented default (14 days) or a
|
|
214
|
+
clamp, logged, never thrown. On a **multi-tenant** deployment this key is
|
|
215
|
+
shared by every seller on the deployment (one process-wide `siteId`) — a
|
|
216
|
+
one-time `warn` log names that on the first read.
|
|
217
|
+
|
|
218
|
+
### The reserved key namespace
|
|
219
|
+
|
|
220
|
+
The record above would be worthless if its own subject could rewrite it:
|
|
221
|
+
`GET`/`PUT`/`DELETE /v1/user/data/:key` (`routes/auth/user.ts`) refuse a
|
|
222
|
+
`commerce.`- or `system.`-prefixed key (case-insensitive) with `403
|
|
223
|
+
{error: "reserved-key"}` before any Prisma call. `PUT` additionally bounds
|
|
224
|
+
the body at 64 KiB. Every unreserved key's behavior is unchanged.
|
|
225
|
+
|
|
226
|
+
## Idempotency caveat (load-bearing)
|
|
227
|
+
|
|
228
|
+
Stripe uses **at-least-once delivery semantics** and occasionally
|
|
229
|
+
double-delivers events. The in-memory store (`lib/webhook-idempotency.ts`)
|
|
230
|
+
short-circuits a replayed `event.id` within one process; it is not durable
|
|
231
|
+
across a restart or a second API instance. Telemetry-only side effects make
|
|
232
|
+
that an acceptable counting drift.
|
|
233
|
+
|
|
234
|
+
**A side effect with write semantics carries its own durable layer.** The
|
|
235
|
+
recommender stamp above uses a guarded conditional update (the row's
|
|
236
|
+
`status: "created"` is the key). A future write that cannot be expressed as
|
|
237
|
+
a guarded update (confirmation email, refund processing) needs a
|
|
238
|
+
`webhook_events` table keyed on `event.id` with an early-return for
|
|
239
|
+
already-seen IDs — add it first.
|
|
240
|
+
|
|
241
|
+
## Env contract
|
|
242
|
+
|
|
243
|
+
Both vars are **server-only** — never use a `NEXT_PUBLIC_` prefix.
|
|
244
|
+
|
|
245
|
+
| Var | Required | Notes |
|
|
246
|
+
| ----------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
247
|
+
| `STRIPE_WEBHOOK_SECRET` | For the route to do anything | `whsec_*` from Stripe Dashboard |
|
|
248
|
+
| `STRIPE_SECRET_KEY` | For checkout + sandbox check | Must match an allow-listed test-mode prefix (`sk_test_*` / `pk_test_*` / `rk_test_*`); anything else — including live and restricted-live keys — is runtime-blocked by `assertSandboxKey`. The commit-time hook is a partial backstop only: it greps for `sk_live_` / `pk_live_` and does **not** catch restricted-live (`rk_live_`) keys, so the runtime allow-list is the load-bearing guard |
|
|
249
|
+
|
|
250
|
+
Without `STRIPE_WEBHOOK_SECRET`, the route returns 503 on every request.
|
|
251
|
+
|
|
252
|
+
## Deployment note
|
|
253
|
+
|
|
254
|
+
Customer forks must:
|
|
255
|
+
|
|
256
|
+
1. Configure a Stripe Dashboard webhook endpoint pointing at
|
|
257
|
+
`https://<your-host>/v1/webhooks/stripe`.
|
|
258
|
+
2. Copy the endpoint's **Signing secret** (`whsec_*`) into
|
|
259
|
+
`STRIPE_WEBHOOK_SECRET` in their environment.
|
|
260
|
+
|
|
261
|
+
Without both, telemetry will not fire and 503s will be logged.
|
|
262
|
+
|
|
263
|
+
## Brief
|
|
264
|
+
|
|
265
|
+
`docs/internal/decisions/ailk-stripe-webhook-receiver.md`
|