create-ailk 0.2.0 → 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 +50 -53
- 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 +2 -2
- 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 +33 -5
- 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 +3 -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 +17 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -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 +10 -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 +10 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
- package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
- package/templates/apps/api/src/routes/project-listings/me.ts +10 -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 +30 -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 +459 -135
- 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 +229 -4
- 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/llms.txt/route.ts +12 -9
- package/templates/apps/web/jest.config.cjs +14 -13
- package/templates/apps/web/next.config.mjs +1 -3
- package/templates/apps/web/package.json +0 -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/database/CHANGELOG.md +33 -0
- package/templates/database/inbox/schema.prisma +67 -0
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
- package/templates/database/package.json +1 -1
- 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__/me-route-precedence.test.ts +0 -221
- 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__/listing-promotion.test.ts +0 -684
- 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 -1296
- 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/listing-promotion.ts +0 -342
- 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 -510
- 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
|
@@ -7,104 +7,544 @@
|
|
|
7
7
|
*
|
|
8
8
|
* When this surface grows beyond a handful of routes, swap in a generator
|
|
9
9
|
* and delete this file; the route handlers don't depend on the spec shape.
|
|
10
|
+
*
|
|
11
|
+
* Hand-authored does NOT mean self-described: this file is byte-synced into
|
|
12
|
+
* every consuming site, so the site's own name and the deployment's own API
|
|
13
|
+
* origin are resolved at serve time (#5306, #5307) rather than written in as
|
|
14
|
+
* literals. Everything a literal can correctly say about every deployment —
|
|
15
|
+
* route shapes, schemas — stays a literal.
|
|
10
16
|
*/
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
11
20
|
import { SCHEMA_ORG_TYPES } from "@working-theory/content-adapters";
|
|
21
|
+
import { logger } from "@working-theory/observability";
|
|
22
|
+
import { parse as parseYaml } from "yaml";
|
|
23
|
+
|
|
24
|
+
import { config } from "../config/index.js";
|
|
25
|
+
import { isModuleEnabled } from "../config/modules.js";
|
|
26
|
+
import { getContentRoot } from "../lib/content-adapter.js";
|
|
12
27
|
|
|
28
|
+
import { toOpenApiPath, type RegisteredRoute } from "./surface-drift.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The version of the PUBLIC API CONTRACT — the `/v1` route prefix this spec
|
|
32
|
+
* documents — and deliberately not the version of any npm package (#5307
|
|
33
|
+
* AC-4). The `@working-theory/*` packages move on their own 0.x line and are
|
|
34
|
+
* released per-change; the contract an agent codes against moves only when a
|
|
35
|
+
* route's shape does. Syncing this to a package version would make every
|
|
36
|
+
* package release look like an API change to a caller that pins on it.
|
|
37
|
+
*
|
|
38
|
+
* Bump the major only alongside a new `/vN` prefix.
|
|
39
|
+
*/
|
|
13
40
|
export const API_VERSION = "1.0.0";
|
|
14
41
|
|
|
15
|
-
|
|
42
|
+
/** Name used when the site's own identity cannot be resolved (#5307 AC-3). */
|
|
43
|
+
const NEUTRAL_DISPLAY_NAME = "Content API";
|
|
44
|
+
|
|
45
|
+
/** Spec title used when the site's own name cannot be resolved (#5307 AC-3). */
|
|
46
|
+
const NEUTRAL_SPEC_TITLE = "Public Content API";
|
|
47
|
+
|
|
48
|
+
/** Root-index description used when `_site.mdx` carries none. */
|
|
49
|
+
const NEUTRAL_DESCRIPTION = "Local auth + session backend + public content API";
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* How this deployment describes itself — resolved from the site's own config
|
|
53
|
+
* and this deployment's own env, never from a literal naming the kit.
|
|
54
|
+
*/
|
|
55
|
+
export interface ApiIdentity {
|
|
56
|
+
/** `siteName` from `_site.mdx`. Absent when the file is missing/unreadable. */
|
|
57
|
+
siteName?: string;
|
|
58
|
+
/** `siteIdentity.description` from `_site.mdx`, when present. */
|
|
59
|
+
siteDescription?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The public origin this API is reachable at, from `AILK_PUBLIC_API_URL`.
|
|
62
|
+
*
|
|
63
|
+
* NOT `_site.mdx`'s `baseUrl` (#5306 AC-3). `baseUrl` is the SITE origin;
|
|
64
|
+
* the API is deployed on a different host — `samhenry.org` serves the site
|
|
65
|
+
* and `api.samhenry.org` serves this API. Pointing `servers` at `baseUrl`
|
|
66
|
+
* would look right in review, pass every test that does not deploy, and
|
|
67
|
+
* send agents to the site's web origin instead of its API. The two are
|
|
68
|
+
* separate facts and only the deployment knows the second one.
|
|
69
|
+
*/
|
|
70
|
+
publicApiUrl?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let cachedIdentity: ApiIdentity | undefined;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve this deployment's identity, reading `_site.mdx` once per process.
|
|
77
|
+
*
|
|
78
|
+
* Deliberately synchronous, and deliberately a local frontmatter read rather
|
|
79
|
+
* than `@working-theory/site-config`'s async `loadSiteConfig`: the spec is
|
|
80
|
+
* built inside a sync route handler, and only two scalar fields are needed.
|
|
81
|
+
* This mirrors `lib/locales.ts`, which reads `project.yaml` the same way and
|
|
82
|
+
* for the same boundary reason.
|
|
83
|
+
*
|
|
84
|
+
* Every failure path degrades to the neutral default (#5307 AC-3) — a missing
|
|
85
|
+
* or malformed `_site.mdx` must never take the API down, and must never fall
|
|
86
|
+
* back to the kit's name.
|
|
87
|
+
*/
|
|
88
|
+
export function resolveApiIdentity(): ApiIdentity {
|
|
89
|
+
if (!cachedIdentity) cachedIdentity = readApiIdentity();
|
|
90
|
+
return cachedIdentity;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Test/utility helper — drop the cached identity so the next call re-reads. */
|
|
94
|
+
export function resetApiIdentity(): void {
|
|
95
|
+
cachedIdentity = undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readApiIdentity(): ApiIdentity {
|
|
99
|
+
const identity: ApiIdentity = {};
|
|
100
|
+
|
|
101
|
+
const publicApiUrl = normaliseOrigin(config.AILK_PUBLIC_API_URL);
|
|
102
|
+
if (publicApiUrl) identity.publicApiUrl = publicApiUrl;
|
|
103
|
+
|
|
104
|
+
const frontmatter = readSiteFrontmatter();
|
|
105
|
+
const siteName = frontmatter?.["siteName"];
|
|
106
|
+
if (typeof siteName === "string" && siteName.trim()) {
|
|
107
|
+
identity.siteName = siteName.trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const siteIdentity = frontmatter?.["siteIdentity"];
|
|
111
|
+
const description =
|
|
112
|
+
siteIdentity && typeof siteIdentity === "object"
|
|
113
|
+
? (siteIdentity as Record<string, unknown>)["description"]
|
|
114
|
+
: undefined;
|
|
115
|
+
if (typeof description === "string" && description.trim()) {
|
|
116
|
+
identity.siteDescription = description.trim();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!identity.siteName) {
|
|
120
|
+
logger.warn(
|
|
121
|
+
{ neutralName: NEUTRAL_DISPLAY_NAME },
|
|
122
|
+
"No readable siteName in _site.mdx — the API will describe itself with a neutral default",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return identity;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Read and parse `_site.mdx` frontmatter, or `undefined` on any failure. */
|
|
130
|
+
function readSiteFrontmatter(): Record<string, unknown> | undefined {
|
|
131
|
+
const path = join(getContentRoot(), "_site.mdx");
|
|
132
|
+
if (!existsSync(path)) return undefined;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const raw = readFileSync(path, "utf8");
|
|
136
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
137
|
+
if (!match?.[1]) return undefined;
|
|
138
|
+
|
|
139
|
+
const parsed: unknown = parseYaml(match[1]);
|
|
140
|
+
return parsed && typeof parsed === "object"
|
|
141
|
+
? (parsed as Record<string, unknown>)
|
|
142
|
+
: undefined;
|
|
143
|
+
} catch {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Accept an absolute http(s) origin and strip a trailing slash. Anything else
|
|
150
|
+
* resolves to `undefined` so the caller degrades to a document-relative
|
|
151
|
+
* server rather than publishing a malformed absolute URL (#5306 AC-2).
|
|
152
|
+
*/
|
|
153
|
+
function normaliseOrigin(value: string | undefined): string | undefined {
|
|
154
|
+
const trimmed = value?.trim();
|
|
155
|
+
if (!trimmed) return undefined;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const url = new URL(trimmed);
|
|
159
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
160
|
+
logger.warn(
|
|
161
|
+
{ AILK_PUBLIC_API_URL: trimmed },
|
|
162
|
+
"AILK_PUBLIC_API_URL is not an http(s) URL — the OpenAPI servers block will fall back to a document-relative origin",
|
|
163
|
+
);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return trimmed.replace(/\/+$/, "");
|
|
167
|
+
} catch {
|
|
168
|
+
logger.warn(
|
|
169
|
+
{ AILK_PUBLIC_API_URL: trimmed },
|
|
170
|
+
"AILK_PUBLIC_API_URL is not a parseable URL — the OpenAPI servers block will fall back to a document-relative origin",
|
|
171
|
+
);
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The name this API introduces itself by on the root index. */
|
|
177
|
+
export function apiDisplayName(identity: ApiIdentity): string {
|
|
178
|
+
return identity.siteName ? `${identity.siteName} API` : NEUTRAL_DISPLAY_NAME;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The description this API introduces itself by on the root index. */
|
|
182
|
+
export function apiDescription(identity: ApiIdentity): string {
|
|
183
|
+
return identity.siteDescription ?? NEUTRAL_DESCRIPTION;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Resolve the `servers` block.
|
|
188
|
+
*
|
|
189
|
+
* With an origin configured, one entry naming this deployment. Without one,
|
|
190
|
+
* a single document-relative entry: per OpenAPI 3.1 a `/` server resolves
|
|
191
|
+
* against the origin the document was served from, so an agent that fetched
|
|
192
|
+
* this spec from `https://api.example.com/v1/openapi.json` still reaches the
|
|
193
|
+
* right host. That makes absence a correct-by-construction degradation rather
|
|
194
|
+
* than a silently wrong URL (#5306 AC-2), and the entry's own description
|
|
195
|
+
* names the unset variable so the reason travels with the served document.
|
|
196
|
+
*/
|
|
197
|
+
function resolveServers(
|
|
198
|
+
publicApiUrl: string | undefined,
|
|
199
|
+
): Array<{ url: string; description: string }> {
|
|
200
|
+
const origin = normaliseOrigin(publicApiUrl);
|
|
201
|
+
if (origin) {
|
|
202
|
+
return [{ url: origin, description: "This deployment" }];
|
|
203
|
+
}
|
|
204
|
+
return [
|
|
205
|
+
{
|
|
206
|
+
url: "/",
|
|
207
|
+
description:
|
|
208
|
+
"This deployment — relative to the origin that served this document " +
|
|
209
|
+
"(AILK_PUBLIC_API_URL is unset)",
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* The route table a built server registered (#5314).
|
|
216
|
+
*
|
|
217
|
+
* Carried on the Fastify instance (`server.decorate`) and threaded into the
|
|
218
|
+
* two surfaces as an argument — deliberately NOT module-level state. A
|
|
219
|
+
* module-level array is shared by every server built in one process, and
|
|
220
|
+
* `apps/api/src/vercel-handler.ts`'s `getServer()` is a check-then-set: it
|
|
221
|
+
* tests `!serverInstance`, then awaits twice before assigning, so two
|
|
222
|
+
* concurrent invocations can both call `build()`. With shared state, one
|
|
223
|
+
* build's reset would land mid-fill of the other and leave a truncated or
|
|
224
|
+
* doubled table — the surfaces would then silently misreport, which is the
|
|
225
|
+
* exact failure this card exists to remove. Per-instance state makes that
|
|
226
|
+
* unreachable whether or not the race fires.
|
|
227
|
+
*
|
|
228
|
+
* Registration is CONDITIONAL — `isModuleEnabled()` gates paid-module routes
|
|
229
|
+
* and `shouldMountTestAuth()` gates the test-auth route — so this is what THIS
|
|
230
|
+
* deployment serves, not a catalogue of what the build could serve (spec D1).
|
|
231
|
+
* A route absent because its module is off is absent from both surfaces too,
|
|
232
|
+
* which is why a disabled slice never reads as drift.
|
|
233
|
+
*/
|
|
234
|
+
|
|
235
|
+
/** The decorator key both surfaces read the route table from. */
|
|
236
|
+
export const REGISTERED_ROUTES_DECORATOR = "registeredRoutes";
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Read the route table off a Fastify instance (or anything carrying the
|
|
240
|
+
* decorator), returning `[]` when it is absent.
|
|
241
|
+
*
|
|
242
|
+
* Empty is a supported state, not a bug: `buildOpenApiSpec()` is called
|
|
243
|
+
* directly by unit tests with no server in play, and this file is byte-synced
|
|
244
|
+
* into every consuming site. `paths` degrades to its hand-authored entries
|
|
245
|
+
* rather than publishing an empty document.
|
|
246
|
+
*/
|
|
247
|
+
export function readRegisteredRoutes(source: unknown): RegisteredRoute[] {
|
|
248
|
+
const routes = (source as Record<string, unknown> | null | undefined)?.[
|
|
249
|
+
REGISTERED_ROUTES_DECORATOR
|
|
250
|
+
];
|
|
251
|
+
return Array.isArray(routes) ? (routes as RegisteredRoute[]) : [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Root-index keys this API has already published, by Fastify path (#5314 D6).
|
|
256
|
+
*
|
|
257
|
+
* Deriving the root index without this overlay would rename all eleven keys a
|
|
258
|
+
* consumer may already read, which ADR-0020 counts as a breaking change and
|
|
259
|
+
* would owe a `/v2`. With it, every published key keeps its name and only new
|
|
260
|
+
* keys appear, so the change is purely additive.
|
|
261
|
+
*
|
|
262
|
+
* `billing` and `billingPortalSession` become CONDITIONAL rather than
|
|
263
|
+
* disappearing: they are emitted exactly when their routes register, which is
|
|
264
|
+
* the fix for a root index that advertised both on a build where neither
|
|
265
|
+
* registers and both 404.
|
|
266
|
+
*/
|
|
267
|
+
const CURATED_ROOT_INDEX_KEYS: Record<string, string> = {
|
|
268
|
+
"/health": "health",
|
|
269
|
+
"/v1/openapi.json": "openapi",
|
|
270
|
+
"/v1/auth/tokens": "tokens",
|
|
271
|
+
"/v1/user/profile": "profile",
|
|
272
|
+
"/v1/user/data/:key": "userData",
|
|
273
|
+
"/v1/content/types": "contentTypes",
|
|
274
|
+
"/v1/content/:type": "contentList",
|
|
275
|
+
"/v1/content/:type/:slug": "contentPage",
|
|
276
|
+
"/v1/checkout/sessions": "checkoutSessions",
|
|
277
|
+
"/v1/billing": "billing",
|
|
278
|
+
"/v1/billing/portal-session": "billingPortalSession",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Canonical method order for a root-index value, so `GET/POST /v1/x` is
|
|
283
|
+
* stable across runs rather than following registration order.
|
|
284
|
+
*/
|
|
285
|
+
const METHOD_ORDER = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
286
|
+
|
|
287
|
+
function byCanonicalMethod(a: string, b: string): number {
|
|
288
|
+
const ai = METHOD_ORDER.indexOf(a);
|
|
289
|
+
const bi = METHOD_ORDER.indexOf(b);
|
|
290
|
+
return (
|
|
291
|
+
(ai === -1 ? METHOD_ORDER.length : ai) -
|
|
292
|
+
(bi === -1 ? METHOD_ORDER.length : bi) || a.localeCompare(b)
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A deterministic root-index key for a path with no curated name:
|
|
298
|
+
* `/v1/project-listings/public` becomes `projectListingsPublic`, and a
|
|
299
|
+
* `:param` segment contributes its bare name.
|
|
300
|
+
*/
|
|
301
|
+
function generatedRootIndexKey(path: string): string {
|
|
302
|
+
const segments = path
|
|
303
|
+
.replace(/^\/v1\//, "/")
|
|
304
|
+
.split("/")
|
|
305
|
+
.filter(Boolean)
|
|
306
|
+
.map((segment) => segment.replace(/^:/, ""));
|
|
307
|
+
if (segments.length === 0) return "root";
|
|
308
|
+
|
|
309
|
+
return segments
|
|
310
|
+
.map((segment, index) => {
|
|
311
|
+
const camel = segment
|
|
312
|
+
.replace(/[^a-zA-Z0-9]+(.)/g, (_m, c: string) => c.toUpperCase())
|
|
313
|
+
.replace(/[^a-zA-Z0-9]/g, "");
|
|
314
|
+
return index === 0
|
|
315
|
+
? camel.charAt(0).toLowerCase() + camel.slice(1)
|
|
316
|
+
: camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
317
|
+
})
|
|
318
|
+
.join("");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The root index `endpoints` object, derived from the routes this process
|
|
323
|
+
* registered (#5314 AC-1).
|
|
324
|
+
*
|
|
325
|
+
* One entry per PATH, valued `"METHOD[/METHOD...] /path"` — the shape this
|
|
326
|
+
* surface has always published (`"GET/PATCH /v1/user/profile"`). The SHAPE is
|
|
327
|
+
* preserved, not every value: `tokens` goes from `"POST/GET/DELETE
|
|
328
|
+
* /v1/auth/tokens"` to `"GET/POST /v1/auth/tokens"`, because `DELETE` was
|
|
329
|
+
* never served at that path (it is `/v1/auth/tokens/:id`, now its own entry).
|
|
330
|
+
* Key NAMES are preserved outright, which is what keeps this additive under
|
|
331
|
+
* ADR-0020; the value corrections are the drift being fixed. Curated keys win; everything else gets a
|
|
332
|
+
* generated one, de-duplicated by suffix so two paths can never collapse onto
|
|
333
|
+
* one key and silently hide a route.
|
|
334
|
+
*
|
|
335
|
+
* An empty route table yields an empty object rather than a stale literal: the
|
|
336
|
+
* only caller is the `GET /` handler on a built server, whose own route is in
|
|
337
|
+
* the table by request time, so an empty result would mean the hook had failed
|
|
338
|
+
* and is better surfaced than papered over.
|
|
339
|
+
*/
|
|
340
|
+
export function deriveRootEndpoints(
|
|
341
|
+
routes: RegisteredRoute[],
|
|
342
|
+
): Record<string, string> {
|
|
343
|
+
const methodsByPath = new Map<string, string[]>();
|
|
344
|
+
for (const { method, url } of routes) {
|
|
345
|
+
const methods = methodsByPath.get(url) ?? [];
|
|
346
|
+
if (!methods.includes(method)) methods.push(method);
|
|
347
|
+
methodsByPath.set(url, methods);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const endpoints: Record<string, string> = {};
|
|
351
|
+
const used = new Set<string>();
|
|
352
|
+
for (const [path, methods] of methodsByPath) {
|
|
353
|
+
const base = CURATED_ROOT_INDEX_KEYS[path] ?? generatedRootIndexKey(path);
|
|
354
|
+
let key = base;
|
|
355
|
+
for (let n = 2; used.has(key); n += 1) key = `${base}${n}`;
|
|
356
|
+
used.add(key);
|
|
357
|
+
endpoints[key] =
|
|
358
|
+
`${[...methods].sort(byCanonicalMethod).join("/")} ${path}`;
|
|
359
|
+
}
|
|
360
|
+
return endpoints;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** An OpenAPI `operationId` for a route with no hand-authored operation. */
|
|
364
|
+
function derivedOperationId(method: string, path: string): string {
|
|
365
|
+
const suffix = generatedRootIndexKey(path);
|
|
366
|
+
return `${method.toLowerCase()}${suffix.charAt(0).toUpperCase()}${suffix.slice(1)}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The minimal OpenAPI 3.1 operation a route table can honestly produce.
|
|
371
|
+
*
|
|
372
|
+
* No route in this app carries a Fastify `schema` — validation is Zod inside
|
|
373
|
+
* the handler — so the route table knows a route's method and path and
|
|
374
|
+
* nothing else (#5314 D2). Inventing a request or response schema here would
|
|
375
|
+
* be worse than omitting one: an agent codes against what the document says.
|
|
376
|
+
*/
|
|
377
|
+
function derivedOperation(method: string, fastifyPath: string) {
|
|
378
|
+
const parameters = fastifyPath
|
|
379
|
+
.split("/")
|
|
380
|
+
.filter((segment) => segment.startsWith(":"))
|
|
381
|
+
.map((segment) => ({
|
|
382
|
+
name: segment.slice(1),
|
|
383
|
+
in: "path",
|
|
384
|
+
required: true,
|
|
385
|
+
schema: { type: "string" },
|
|
386
|
+
}));
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
operationId: derivedOperationId(method, fastifyPath),
|
|
390
|
+
summary: `${method.toUpperCase()} ${toOpenApiPath(fastifyPath)}`,
|
|
391
|
+
description:
|
|
392
|
+
"Derived from this deployment's route table (#5314). Request and " +
|
|
393
|
+
"response shapes are not described: this route carries no schema the " +
|
|
394
|
+
"route table can read.",
|
|
395
|
+
...(parameters.length > 0 && { parameters }),
|
|
396
|
+
responses: { "200": { description: "OK" } },
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* `paths` = every registered route, overlaid with the hand-authored operations.
|
|
402
|
+
*
|
|
403
|
+
* The derived half makes the endpoint SET complete and correct by construction
|
|
404
|
+
* (#5314 AC-1/AC-4); the overlay keeps every hand-authored schema, example and
|
|
405
|
+
* description exactly as written (#5314 D2). Hand-authored wins per method, and
|
|
406
|
+
* path-level keys on a hand-authored item are preserved.
|
|
407
|
+
*
|
|
408
|
+
* With an EMPTY registry the hand-authored object is returned untouched. That
|
|
409
|
+
* is the byte-sync degradation floor (#5314 FR7): `buildOpenApiSpec()` is
|
|
410
|
+
* called directly by unit tests with no server in play, and a consuming site
|
|
411
|
+
* that somehow builds no route table still serves the seven documented paths
|
|
412
|
+
* rather than an empty document.
|
|
413
|
+
*/
|
|
414
|
+
function mergeDerivedPaths(
|
|
415
|
+
routes: RegisteredRoute[],
|
|
416
|
+
handAuthored: Record<string, Record<string, unknown>>,
|
|
417
|
+
): Record<string, Record<string, unknown>> {
|
|
418
|
+
if (routes.length === 0) return handAuthored;
|
|
419
|
+
|
|
420
|
+
const paths: Record<string, Record<string, unknown>> = {};
|
|
421
|
+
for (const { method, url } of routes) {
|
|
422
|
+
const path = toOpenApiPath(url);
|
|
423
|
+
paths[path] = {
|
|
424
|
+
...paths[path],
|
|
425
|
+
[method.toLowerCase()]: derivedOperation(method, url),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const [path, item] of Object.entries(handAuthored)) {
|
|
430
|
+
paths[path] = { ...paths[path], ...item };
|
|
431
|
+
}
|
|
432
|
+
return paths;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function buildOpenApiSpec(
|
|
436
|
+
identity: ApiIdentity = resolveApiIdentity(),
|
|
437
|
+
registeredRoutes: RegisteredRoute[] = [],
|
|
438
|
+
) {
|
|
16
439
|
const typeEnum = [...SCHEMA_ORG_TYPES];
|
|
17
440
|
|
|
18
441
|
return {
|
|
19
442
|
openapi: "3.1.0",
|
|
20
443
|
info: {
|
|
21
|
-
title:
|
|
444
|
+
title: identity.siteName
|
|
445
|
+
? `${identity.siteName} — ${NEUTRAL_SPEC_TITLE}`
|
|
446
|
+
: NEUTRAL_SPEC_TITLE,
|
|
22
447
|
version: API_VERSION,
|
|
23
448
|
description:
|
|
24
|
-
"HTTP API exposing
|
|
25
|
-
"of anonymous write routes (lead capture, checkout). Designed
|
|
26
|
-
"consumption (AX) — every content response carries `@context`
|
|
27
|
-
"`@type` at the document root.",
|
|
28
|
-
license
|
|
449
|
+
"HTTP API exposing this site's content as schema.org JSON-LD, plus a " +
|
|
450
|
+
"small set of anonymous write routes (lead capture, checkout). Designed " +
|
|
451
|
+
"for agent consumption (AX) — every content response carries `@context` " +
|
|
452
|
+
"and `@type` at the document root.",
|
|
453
|
+
// No `license`: this file is byte-synced into every consuming site, so a
|
|
454
|
+
// literal here would assert a licence over a deployment that never chose
|
|
455
|
+
// one (#5307 AC-2). OpenAPI 3.1 makes `info.license` optional; a site
|
|
456
|
+
// that wants to publish one declares it where its own licence lives.
|
|
29
457
|
},
|
|
30
|
-
servers:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"201": {
|
|
55
|
-
description: "Checkout session created",
|
|
458
|
+
servers: resolveServers(identity.publicApiUrl),
|
|
459
|
+
paths: mergeDerivedPaths(registeredRoutes, {
|
|
460
|
+
// Module-conditional (commerce, D-47 / #5364) — mirroring the CONDITIONAL
|
|
461
|
+
// treatment `CURATED_ROOT_INDEX_KEYS`'s doc comment above already
|
|
462
|
+
// describes for `billing`/`billingPortalSession`. Those two have no
|
|
463
|
+
// hand-authored `paths` entry here at all (mergeDerivedPaths's DERIVED
|
|
464
|
+
// half is what publishes them, so they only ever appear when actually
|
|
465
|
+
// registered); `/v1/checkout/sessions` is the one hand-authored entry
|
|
466
|
+
// in this object, and mergeDerivedPaths always merges hand-authored
|
|
467
|
+
// paths in regardless of registration — so without this gate, a build
|
|
468
|
+
// with commerce off still advertises the operation while `registeredRoutes`
|
|
469
|
+
// has no matching entry, which is exactly the orphaned-surface drift
|
|
470
|
+
// surface-drift.test.ts exists to catch (#5314).
|
|
471
|
+
...(isModuleEnabled("commerce") && {
|
|
472
|
+
"/v1/checkout/sessions": {
|
|
473
|
+
post: {
|
|
474
|
+
operationId: "createCheckoutSession",
|
|
475
|
+
summary: "Create a Stripe hosted-page checkout session",
|
|
476
|
+
description:
|
|
477
|
+
"Resolves a productCode to a Stripe price via lookup_key and creates a " +
|
|
478
|
+
"hosted-page checkout session. Returns the session ID and redirect URL. " +
|
|
479
|
+
"Sandbox-only: live Stripe keys are rejected at runtime.",
|
|
480
|
+
requestBody: {
|
|
481
|
+
required: true,
|
|
56
482
|
content: {
|
|
57
483
|
"application/json": {
|
|
58
484
|
schema: {
|
|
59
|
-
$ref: "#/components/schemas/
|
|
485
|
+
$ref: "#/components/schemas/CheckoutSessionRequest",
|
|
60
486
|
},
|
|
61
487
|
},
|
|
62
488
|
},
|
|
63
489
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
490
|
+
responses: {
|
|
491
|
+
"201": {
|
|
492
|
+
description: "Checkout session created",
|
|
493
|
+
content: {
|
|
494
|
+
"application/json": {
|
|
495
|
+
schema: {
|
|
496
|
+
$ref: "#/components/schemas/CheckoutSessionResponse",
|
|
497
|
+
},
|
|
498
|
+
},
|
|
69
499
|
},
|
|
70
500
|
},
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
$ref: "#/components/schemas/UnknownProductCodeResponse",
|
|
501
|
+
"400": {
|
|
502
|
+
description: "Validation error — invalid request body",
|
|
503
|
+
content: {
|
|
504
|
+
"application/json": {
|
|
505
|
+
schema: {
|
|
506
|
+
$ref: "#/components/schemas/ValidationErrorResponse",
|
|
507
|
+
},
|
|
79
508
|
},
|
|
80
509
|
},
|
|
81
510
|
},
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
511
|
+
"404": {
|
|
512
|
+
description:
|
|
513
|
+
"Unknown product code — no active Stripe price for the given productCode",
|
|
514
|
+
content: {
|
|
515
|
+
"application/json": {
|
|
516
|
+
schema: {
|
|
517
|
+
$ref: "#/components/schemas/UnknownProductCodeResponse",
|
|
518
|
+
},
|
|
90
519
|
},
|
|
91
520
|
},
|
|
92
521
|
},
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
522
|
+
"500": {
|
|
523
|
+
description:
|
|
524
|
+
"Live-mode forbidden — STRIPE_SECRET_KEY is a live key",
|
|
525
|
+
content: {
|
|
526
|
+
"application/json": {
|
|
527
|
+
schema: {
|
|
528
|
+
$ref: "#/components/schemas/LiveModeForbiddenResponse",
|
|
529
|
+
},
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
"502": {
|
|
534
|
+
description:
|
|
535
|
+
"Checkout unavailable — Stripe returned an unexpected error",
|
|
536
|
+
content: {
|
|
537
|
+
"application/json": {
|
|
538
|
+
schema: {
|
|
539
|
+
$ref: "#/components/schemas/CheckoutUnavailableResponse",
|
|
540
|
+
},
|
|
101
541
|
},
|
|
102
542
|
},
|
|
103
543
|
},
|
|
104
544
|
},
|
|
105
545
|
},
|
|
106
546
|
},
|
|
107
|
-
},
|
|
547
|
+
}),
|
|
108
548
|
"/v1/leads/submit": {
|
|
109
549
|
post: {
|
|
110
550
|
operationId: "submitLead",
|
|
@@ -391,7 +831,7 @@ export function buildOpenApiSpec() {
|
|
|
391
831
|
},
|
|
392
832
|
},
|
|
393
833
|
},
|
|
394
|
-
},
|
|
834
|
+
}),
|
|
395
835
|
components: {
|
|
396
836
|
schemas: {
|
|
397
837
|
CheckoutSessionRequest: {
|