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.
Files changed (284) hide show
  1. package/component-catalog.md +210 -52
  2. package/dist/cli.js +0 -0
  3. package/dist/lib/apply-module-patches.d.ts +47 -0
  4. package/dist/lib/apply-module-patches.js +370 -0
  5. package/dist/lib/extract-module-bundle.d.ts +12 -3
  6. package/dist/lib/extract-module-bundle.js +236 -24
  7. package/dist/lib/fetch-module.d.ts +3 -38
  8. package/dist/lib/fetch-module.js +26 -8
  9. package/dist/lib/module-license-gate.d.ts +2 -0
  10. package/dist/lib/module-license-gate.js +1 -0
  11. package/dist/lib/paid-paths.d.ts +16 -0
  12. package/dist/lib/paid-paths.js +90 -0
  13. package/dist/module-architecture.d.ts +139 -23
  14. package/dist/module-architecture.js +668 -25
  15. package/dist/parse-args.d.ts +2 -2
  16. package/dist/parse-args.js +3 -1
  17. package/dist/programmatic.d.ts +2 -1
  18. package/dist/programmatic.js +15 -4
  19. package/dist/surfaces.d.ts +12 -36
  20. package/dist/surfaces.js +35 -5
  21. package/dist/sync-routes.js +31 -1
  22. package/package.json +14 -15
  23. package/templates/.claude/agents/web.md +2 -3
  24. package/templates/.claude/rules/architecture.md +5 -5
  25. package/templates/.claude/skills/README.md +2 -1
  26. package/templates/.env.example +21 -9
  27. package/templates/CONVENTIONS.md +6 -7
  28. package/templates/apps/api/.env.example +9 -0
  29. package/templates/apps/api/CLAUDE.md +35 -4
  30. package/templates/apps/api/package.json +0 -1
  31. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  32. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  33. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  34. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  35. package/templates/apps/api/src/config/index.ts +19 -0
  36. package/templates/apps/api/src/config/modules.ts +12 -14
  37. package/templates/apps/api/src/lib/__mocks__/prisma.ts +13 -0
  38. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  39. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  40. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  41. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  42. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  43. package/templates/apps/api/src/openapi/index.ts +9 -3
  44. package/templates/apps/api/src/openapi/spec.ts +505 -65
  45. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  46. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  49. package/templates/apps/api/src/routes/content/index.ts +38 -11
  50. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +28 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +144 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +478 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +717 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +35 -0
  58. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +484 -0
  59. package/templates/apps/api/src/routes/project-listings/index.ts +52 -10
  60. package/templates/apps/api/src/routes/project-listings/me.ts +86 -0
  61. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  63. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  64. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  65. package/templates/apps/api/src/routes/project-listings/start.ts +38 -0
  66. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  67. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  68. package/templates/apps/api/src/server.ts +471 -133
  69. package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +65 -0
  70. package/templates/apps/api/src/services/__tests__/consent.test.ts +282 -0
  71. package/templates/apps/api/src/services/consent.ts +236 -0
  72. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  73. package/templates/apps/api/src/services/listing-config.ts +58 -0
  74. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  75. package/templates/apps/api/src/services/project-listings.ts +602 -29
  76. package/templates/apps/api/src/vercel-handler.ts +60 -26
  77. package/templates/apps/mcp/.env.example +8 -0
  78. package/templates/apps/mcp/CLAUDE.md +2 -2
  79. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  80. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  81. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  82. package/templates/apps/mcp/src/config/modules.ts +14 -13
  83. package/templates/apps/mcp/src/server.ts +8 -4
  84. package/templates/apps/mcp/src/tools/index.ts +7 -22
  85. package/templates/apps/web/.env.example +15 -0
  86. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  87. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  88. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  90. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  91. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  92. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  93. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  94. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  95. package/templates/apps/web/app/[locale]/layout.tsx +13 -1
  96. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  97. package/templates/apps/web/jest.config.cjs +20 -13
  98. package/templates/apps/web/lib/__tests__/site-theme.test.ts +112 -0
  99. package/templates/apps/web/lib/site-brand.tsx +4 -1
  100. package/templates/apps/web/lib/site-theme.ts +74 -0
  101. package/templates/apps/web/next.config.mjs +1 -3
  102. package/templates/apps/web/package.json +1 -6
  103. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  104. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  105. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  106. package/templates/apps/web/public/favicon-16x16.png +0 -0
  107. package/templates/apps/web/public/favicon-32x32.png +0 -0
  108. package/templates/apps/web/public/favicon.ico +0 -0
  109. package/templates/apps/web/public/favicon.svg +6 -3
  110. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  111. package/templates/apps/web/public/logomark.svg +4 -0
  112. package/templates/apps/web/public/site.webmanifest +2 -2
  113. package/templates/apps/web/public/wordmark.svg +4 -0
  114. package/templates/content/_site.mdx +12 -0
  115. package/templates/database/CHANGELOG.md +94 -0
  116. package/templates/database/inbox/schema.prisma +165 -0
  117. package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +32 -0
  118. package/templates/database/migrations/20260911180000_consent_grants/migration.sql +71 -0
  119. package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +30 -0
  120. package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +49 -0
  121. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  122. package/templates/database/package.json +1 -1
  123. package/templates/database/scripts/db-generate-locked.sh +0 -0
  124. package/templates/package.json +1 -1
  125. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  126. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  127. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  128. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  129. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  130. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  131. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  132. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  133. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  134. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  135. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  136. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  137. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  138. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  139. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  140. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  141. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  142. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  143. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  144. package/templates/apps/api/src/lib/stripe.ts +0 -52
  145. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  146. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  147. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  148. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  149. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  150. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  151. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  152. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  153. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  154. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  155. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  156. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  157. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  158. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  159. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  160. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  161. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  162. package/templates/apps/api/src/routes/flows/README.md +0 -147
  163. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  164. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  165. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  166. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  167. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  168. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  169. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  170. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  171. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  172. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  173. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  174. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  175. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  176. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  177. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  178. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  179. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  180. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  181. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  182. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  183. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  184. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  185. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  186. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  187. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  188. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  189. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  190. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  191. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  192. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  193. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  194. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  195. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  196. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  197. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  198. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  199. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  200. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  201. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  202. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  203. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  204. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  205. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  206. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  207. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  208. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  209. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  210. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  211. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  212. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  213. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  214. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  215. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  216. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  217. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  218. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  219. package/templates/apps/api/src/services/flow-engine.ts +0 -1278
  220. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  221. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  222. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  223. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  224. package/templates/apps/api/src/services/project-listing-decision.ts +0 -490
  225. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  226. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  227. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  228. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  229. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  230. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  231. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  232. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  233. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  234. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  235. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  236. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  237. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  238. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  239. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  240. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  241. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  242. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  243. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  246. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  247. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  248. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  249. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  250. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  251. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  252. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  253. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  254. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  255. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  256. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  257. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  258. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  259. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  260. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  261. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  262. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  263. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  264. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  265. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  266. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  267. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  268. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  269. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  270. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  271. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  272. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  273. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  274. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  275. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  276. package/templates/apps/web/content/en/blog/README.txt +0 -25
  277. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  278. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  279. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  280. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  281. package/templates/apps/web/lib/source.ts +0 -34
  282. package/templates/apps/web/public/logomark-dark.svg +0 -4
  283. package/templates/apps/web/public/logomark-light.svg +0 -4
  284. package/templates/apps/web/source.config.ts +0 -78
@@ -1,392 +0,0 @@
1
- // oss: false
2
- /**
3
- * Cal.com provider (#4416) — the first implementation of the scheduling seam.
4
- *
5
- * Everything Cal-specific lives here and nowhere else: the per-endpoint
6
- * `cal-api-version` headers, the event-type lookup that turns a
7
- * `"<owner>/<event-slug>"` link into a numeric event-type id, the slot map
8
- * keyed by day, and the booking body. `types.ts` names none of it, so #4288's
9
- * self-hosted engine implements the same two methods without inheriting any
10
- * of Cal's vocabulary.
11
- *
12
- * CREDENTIALS ARE SERVER-SIDE ONLY. `CAL_API_KEY` is read here, inside
13
- * `apps/api`. The browser reaches Cal exclusively through this app's own
14
- * `/v1/schedule/*` routes; the key is never serialized into a section's data,
15
- * a page's props, or any response body.
16
- *
17
- * Cal v2 endpoints used (each pins its own API version, per Cal's docs):
18
- * GET /v2/event-types?username=&eventTypeSlug= cal-api-version: 2024-06-14
19
- * GET /v2/slots?eventTypeId=&start=&end=&timeZone= cal-api-version: 2024-09-04
20
- * POST /v2/bookings cal-api-version: 2024-08-13
21
- */
22
-
23
- import type {
24
- AvailabilityResult,
25
- BookingResult,
26
- SchedulingEventMeta,
27
- SchedulingPrice,
28
- SchedulingSlot,
29
- } from "@working-theory/validation";
30
-
31
- import {
32
- schedulingError,
33
- type CreateBookingRequest,
34
- type GetEventMetaRequest,
35
- type ListAvailabilityRequest,
36
- type SchedulingOutcome,
37
- type SchedulingProvider,
38
- } from "./types.js";
39
-
40
- const DEFAULT_BASE_URL = "https://api.cal.com";
41
- const EVENT_TYPES_API_VERSION = "2024-06-14";
42
- const SLOTS_API_VERSION = "2024-09-04";
43
- const BOOKINGS_API_VERSION = "2024-08-13";
44
-
45
- /** Cal answers in seconds-free ISO; anything else means the response drifted. */
46
- const REQUEST_TIMEOUT_MS = 10_000;
47
-
48
- export interface CalProviderOptions {
49
- apiKey: string;
50
- /** Overridable for tests and self-hosted Cal instances. */
51
- baseUrl?: string;
52
- /** Injectable for tests; defaults to the platform `fetch`. */
53
- fetchImpl?: typeof fetch;
54
- }
55
-
56
- /** `"sam/fit-check"` → `{ username: "sam", eventTypeSlug: "fit-check" }`. */
57
- function splitCalLink(
58
- calLink: string,
59
- ): { username: string; eventTypeSlug: string } | null {
60
- const parts = calLink.split("/");
61
- if (parts.length !== 2) return null;
62
- const [username, eventTypeSlug] = parts;
63
- if (!username || !eventTypeSlug) return null;
64
- return { username, eventTypeSlug };
65
- }
66
-
67
- /**
68
- * Cal describes an event's location as a small tagged union whose shape has
69
- * changed across versions. Rather than model every variant, read the one
70
- * field every variant carries and turn it into the words a booker shows
71
- * ("Google Meet", "Phone call") — falling back to no meeting type at all,
72
- * which the UI renders by simply omitting that meta item.
73
- */
74
- const LOCATION_LABELS: Record<string, string> = {
75
- integrations_google_meet: "Google Meet",
76
- "integrations:google:meet": "Google Meet",
77
- "integrations:daily": "Cal Video",
78
- integrations_daily: "Cal Video",
79
- "integrations:zoom": "Zoom",
80
- integrations_zoom: "Zoom",
81
- phone: "Phone call",
82
- userPhone: "Phone call",
83
- attendeeInPerson: "In person",
84
- inPerson: "In person",
85
- link: "Web link",
86
- };
87
-
88
- /**
89
- * Cal states an event's price as two sibling fields — `price` in minor units
90
- * (0 for every event on an account with no payment app connected) and
91
- * `currency` (`"usd"`). Free and unpriced are therefore the same state at the
92
- * source. Kept in minor units: converting to major units needs the currency's
93
- * decimal rules and the viewer's locale, which only the rendering site has.
94
- */
95
- function readPrice(eventType: Record<string, unknown>): SchedulingPrice | undefined {
96
- const amount = eventType["price"];
97
- const currency = eventType["currency"];
98
- if (typeof amount !== "number" || !Number.isInteger(amount) || amount < 0) {
99
- return undefined;
100
- }
101
- if (typeof currency !== "string" || !/^[A-Za-z]{3}$/.test(currency)) {
102
- return undefined;
103
- }
104
- return { amountMinor: amount, currency };
105
- }
106
-
107
- function readMeetingType(eventType: Record<string, unknown>): string | undefined {
108
- const locations = eventType["locations"];
109
- if (!Array.isArray(locations) || locations.length === 0) return undefined;
110
- const first = locations[0];
111
- if (typeof first !== "object" || first === null) return undefined;
112
- const raw = (first as Record<string, unknown>)["type"];
113
- if (typeof raw !== "string" || raw === "") return undefined;
114
- return LOCATION_LABELS[raw] ?? undefined;
115
- }
116
-
117
- /**
118
- * Cal's `/v2/slots` payload is a map of day → slot list, and the slot entries
119
- * have carried `start`/`time` across versions. Flatten it into the seam's
120
- * `{ start, end }` pairs, deriving `end` from the event's own duration so the
121
- * shape is identical no matter which key Cal used.
122
- */
123
- function flattenSlots(
124
- data: unknown,
125
- durationMinutes: number,
126
- ): SchedulingSlot[] {
127
- if (typeof data !== "object" || data === null) return [];
128
- const slots: SchedulingSlot[] = [];
129
- for (const daySlots of Object.values(data as Record<string, unknown>)) {
130
- if (!Array.isArray(daySlots)) continue;
131
- for (const entry of daySlots) {
132
- const start =
133
- typeof entry === "string"
134
- ? entry
135
- : typeof entry === "object" && entry !== null
136
- ? ((entry as Record<string, unknown>)["start"] ??
137
- (entry as Record<string, unknown>)["time"])
138
- : undefined;
139
- if (typeof start !== "string") continue;
140
- const startMs = Date.parse(start);
141
- if (Number.isNaN(startMs)) continue;
142
- slots.push({
143
- start: new Date(startMs).toISOString(),
144
- end: new Date(startMs + durationMinutes * 60_000).toISOString(),
145
- });
146
- }
147
- }
148
- slots.sort((a, b) => a.start.localeCompare(b.start));
149
- return slots;
150
- }
151
-
152
- /**
153
- * Build the Cal provider. A factory rather than a class, per this repo's
154
- * object discipline: adapters and clients are `createX()` functions, and
155
- * everything below closes over `options` instead of carrying instance state.
156
- */
157
- export function createCalSchedulingProvider(
158
- options: CalProviderOptions,
159
- ): SchedulingProvider {
160
- const apiKey = options.apiKey;
161
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
162
- const fetchImpl = options.fetchImpl ?? fetch;
163
-
164
- async function request(
165
- path: string,
166
- apiVersion: string,
167
- init?: RequestInit,
168
- ): Promise<SchedulingOutcome<unknown>> {
169
- const controller = new AbortController();
170
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
171
- try {
172
- const res = await fetchImpl(`${baseUrl}${path}`, {
173
- ...init,
174
- signal: controller.signal,
175
- headers: {
176
- Authorization: `Bearer ${apiKey}`,
177
- "cal-api-version": apiVersion,
178
- "Content-Type": "application/json",
179
- ...(init?.headers ?? {}),
180
- },
181
- });
182
-
183
- const body: unknown = await res.json().catch(() => null);
184
- if (res.status === 404) {
185
- return schedulingError("event_not_found", "Cal returned 404.");
186
- }
187
- if (!res.ok) {
188
- // 409 is Cal's "someone took it" for a booking create; everything
189
- // else is an upstream failure this app cannot resolve.
190
- if (res.status === 409) {
191
- return schedulingError(
192
- "slot_unavailable",
193
- "Cal reported the slot is no longer available.",
194
- );
195
- }
196
- return schedulingError(
197
- "provider_error",
198
- `Cal responded ${String(res.status)}.`,
199
- );
200
- }
201
- return { ok: true, value: body };
202
- } catch (err) {
203
- const detail = err instanceof Error ? err.message : "unknown error";
204
- return schedulingError("provider_error", `Cal request failed: ${detail}`);
205
- } finally {
206
- clearTimeout(timer);
207
- }
208
- }
209
-
210
- /** Resolve `"<owner>/<event-slug>"` to the numeric id + derived event meta. */
211
- async function resolveEventType(
212
- calLink: string,
213
- timeZoneOverride?: string,
214
- ): Promise<SchedulingOutcome<{ id: number; meta: SchedulingEventMeta }>> {
215
- const parts = splitCalLink(calLink);
216
- if (!parts) {
217
- return schedulingError(
218
- "invalid_input",
219
- "calLink is not owner/event-slug.",
220
- );
221
- }
222
-
223
- const query = new URLSearchParams({
224
- username: parts.username,
225
- eventTypeSlug: parts.eventTypeSlug,
226
- });
227
- const res = await request(
228
- `/v2/event-types?${query.toString()}`,
229
- EVENT_TYPES_API_VERSION,
230
- );
231
- if (!res.ok) return res;
232
-
233
- const envelope = res.value as { data?: unknown } | null;
234
- const raw = envelope?.data;
235
- // Cal has returned both a bare object and a single-element array here.
236
- const eventType = (Array.isArray(raw) ? raw[0] : raw) as
237
- | Record<string, unknown>
238
- | undefined;
239
- if (!eventType || typeof eventType !== "object") {
240
- return schedulingError("event_not_found", "Cal returned no event type.");
241
- }
242
-
243
- const id = eventType["id"];
244
- const lengthInMinutes =
245
- eventType["lengthInMinutes"] ?? eventType["length"] ?? undefined;
246
- if (typeof id !== "number" || typeof lengthInMinutes !== "number") {
247
- return schedulingError(
248
- "provider_error",
249
- "Cal event type is missing id or duration.",
250
- );
251
- }
252
-
253
- const title = eventType["title"];
254
- const scheduleTimeZone = eventType["scheduleTimeZone"];
255
- const meetingType = readMeetingType(eventType);
256
- // Cal allows an empty description; omit it rather than carry "".
257
- const description = eventType["description"];
258
- const price = readPrice(eventType);
259
- return {
260
- ok: true,
261
- value: {
262
- id,
263
- meta: {
264
- title:
265
- typeof title === "string" && title ? title : parts.eventTypeSlug,
266
- durationMinutes: lengthInMinutes,
267
- ...(typeof description === "string" && description
268
- ? { description }
269
- : {}),
270
- ...(price !== undefined ? { price } : {}),
271
- ...(meetingType !== undefined ? { meetingType } : {}),
272
- timeZone:
273
- timeZoneOverride ??
274
- (typeof scheduleTimeZone === "string" && scheduleTimeZone
275
- ? scheduleTimeZone
276
- : "UTC"),
277
- },
278
- },
279
- };
280
- }
281
-
282
- return {
283
- id: "cal",
284
-
285
- async getEventMeta(
286
- req: GetEventMetaRequest,
287
- ): Promise<SchedulingOutcome<SchedulingEventMeta>> {
288
- // One event-type lookup and nothing else — the availability read's
289
- // /v2/slots call is exactly what this method exists to avoid.
290
- const resolved = await resolveEventType(req.calLink);
291
- if (!resolved.ok) return resolved;
292
- return { ok: true, value: resolved.value.meta };
293
- },
294
-
295
- async listAvailability(
296
- req: ListAvailabilityRequest,
297
- ): Promise<SchedulingOutcome<AvailabilityResult>> {
298
- const resolved = await resolveEventType(req.calLink, req.timeZone);
299
- if (!resolved.ok) return resolved;
300
-
301
- const query = new URLSearchParams({
302
- eventTypeId: String(resolved.value.id),
303
- start: req.from,
304
- end: req.to,
305
- timeZone: resolved.value.meta.timeZone,
306
- });
307
- const res = await request(
308
- `/v2/slots?${query.toString()}`,
309
- SLOTS_API_VERSION,
310
- );
311
- if (!res.ok) return res;
312
-
313
- const envelope = res.value as { data?: unknown } | null;
314
- return {
315
- ok: true,
316
- value: {
317
- event: resolved.value.meta,
318
- slots: flattenSlots(
319
- envelope?.data,
320
- resolved.value.meta.durationMinutes,
321
- ),
322
- },
323
- };
324
- },
325
-
326
- async createBooking(
327
- req: CreateBookingRequest,
328
- ): Promise<SchedulingOutcome<BookingResult>> {
329
- const resolved = await resolveEventType(
330
- req.calLink,
331
- req.attendee.timeZone,
332
- );
333
- if (!resolved.ok) return resolved;
334
-
335
- const res = await request("/v2/bookings", BOOKINGS_API_VERSION, {
336
- method: "POST",
337
- body: JSON.stringify({
338
- start: req.start,
339
- eventTypeId: resolved.value.id,
340
- attendee: {
341
- name: req.attendee.name,
342
- email: req.attendee.email,
343
- timeZone: req.attendee.timeZone ?? resolved.value.meta.timeZone,
344
- language: "en",
345
- },
346
- ...(req.attendee.notes !== undefined
347
- ? { metadata: { notes: req.attendee.notes } }
348
- : {}),
349
- }),
350
- });
351
- if (!res.ok) return res;
352
-
353
- const envelope = res.value as { data?: unknown } | null;
354
- const booking = envelope?.data as Record<string, unknown> | undefined;
355
- if (!booking || typeof booking !== "object") {
356
- return schedulingError("provider_error", "Cal returned no booking.");
357
- }
358
-
359
- const rawId = booking["uid"] ?? booking["id"];
360
- const start = booking["start"];
361
- if (
362
- (typeof rawId !== "string" && typeof rawId !== "number") ||
363
- typeof start !== "string"
364
- ) {
365
- return schedulingError(
366
- "provider_error",
367
- "Cal booking is missing uid or start.",
368
- );
369
- }
370
-
371
- const startMs = Date.parse(start);
372
- const end = booking["end"];
373
- const meetingUrl = booking["meetingUrl"];
374
- return {
375
- ok: true,
376
- value: {
377
- bookingId: String(rawId),
378
- start: new Date(startMs).toISOString(),
379
- end:
380
- typeof end === "string" && !Number.isNaN(Date.parse(end))
381
- ? new Date(Date.parse(end)).toISOString()
382
- : new Date(
383
- startMs + resolved.value.meta.durationMinutes * 60_000,
384
- ).toISOString(),
385
- ...(typeof meetingUrl === "string" && meetingUrl
386
- ? { meetingUrl }
387
- : {}),
388
- },
389
- };
390
- },
391
- };
392
- }
@@ -1,63 +0,0 @@
1
- // oss: false
2
- /**
3
- * Scheduling service entry point (#4416) — provider resolution.
4
- *
5
- * `resolveSchedulingProvider()` returns `null` when no provider is
6
- * configured, and that null is a FIRST-CLASS STATE, not an error: the routes
7
- * turn it into a 503 `scheduling_not_configured`, the MCP twins surface it as
8
- * `not_configured`, and the `booker` section renders its degraded state with
9
- * a direct link out to the provider's own booking page. A deployment that
10
- * never sets `CAL_API_KEY` still ships a page that books.
11
- *
12
- * Config:
13
- * CAL_API_KEY — required to enable scheduling. Server-side only.
14
- * CAL_API_BASE_URL — optional; self-hosted Cal instances.
15
- * SCHEDULING_PROVIDER — optional; defaults to "cal". The config flip that
16
- * #4288's self-hosted engine will answer to.
17
- */
18
-
19
- import { createCalSchedulingProvider } from "./cal-provider.js";
20
- import type { SchedulingProvider } from "./types.js";
21
-
22
- export {
23
- createCalSchedulingProvider,
24
- type CalProviderOptions,
25
- } from "./cal-provider.js";
26
- export {
27
- schedulingError,
28
- type CreateBookingRequest,
29
- type GetEventMetaRequest,
30
- type ListAvailabilityRequest,
31
- type SchedulingError,
32
- type SchedulingOutcome,
33
- type SchedulingProvider,
34
- } from "./types.js";
35
-
36
- /**
37
- * Build the configured provider, or `null` when scheduling is unconfigured.
38
- *
39
- * Read per call rather than cached at module load so a test (and a process
40
- * that loads its env late) sees the current environment — the same posture
41
- * `packages/email`'s Resend client takes with `RESEND_API_KEY`.
42
- */
43
- export function resolveSchedulingProvider(): SchedulingProvider | null {
44
- const providerId = process.env["SCHEDULING_PROVIDER"] ?? "cal";
45
- if (providerId !== "cal") return null;
46
-
47
- const apiKey = process.env["CAL_API_KEY"];
48
- if (!apiKey) return null;
49
-
50
- const baseUrl = process.env["CAL_API_BASE_URL"];
51
- return createCalSchedulingProvider({
52
- apiKey,
53
- ...(baseUrl ? { baseUrl } : {}),
54
- });
55
- }
56
-
57
- /**
58
- * Whether scheduling is configured. Cheap enough to call from a route and
59
- * side-effect free — used for the degraded-state branch.
60
- */
61
- export function isSchedulingConfigured(): boolean {
62
- return resolveSchedulingProvider() !== null;
63
- }
@@ -1,88 +0,0 @@
1
- // oss: false
2
- /**
3
- * The scheduling provider seam (#4416, meta read #4633).
4
- *
5
- * THREE CALLS. That is the whole interface, and keeping it that small is the
6
- * point: the `booker` section is written against `listAvailability` +
7
- * `createBooking`, and a booking tile against `getEventMeta` — the meta-only
8
- * read that exists so rendering an event's facts never costs an availability
9
- * window. The self-hosted scheduling engine (#4288) ships as a second
10
- * implementation of this file's `SchedulingProvider` and nothing in
11
- * `packages/ui`, `apps/mcp`, or the routes changes — same section, same
12
- * content, a config flip.
13
- *
14
- * Nothing Cal-specific appears below. Cal's event-type ids, its per-endpoint
15
- * `cal-api-version` headers, and its slot-map response shape are all confined
16
- * to `cal-provider.ts`; a second provider owes this file only the two methods
17
- * and the shared error vocabulary.
18
- */
19
-
20
- import type {
21
- AvailabilityResult,
22
- BookingResult,
23
- SchedulingAttendee,
24
- SchedulingErrorKind,
25
- SchedulingEventMeta,
26
- } from "@working-theory/validation";
27
-
28
- /** A failure, in the provider-independent vocabulary the routes map to status codes. */
29
- export interface SchedulingError {
30
- kind: SchedulingErrorKind;
31
- /** Operator-facing detail. Safe to log; the routes decide what to expose. */
32
- message: string;
33
- }
34
-
35
- export type SchedulingOutcome<T> =
36
- | { ok: true; value: T }
37
- | { ok: false; error: SchedulingError };
38
-
39
- /** The range + presentation options for an availability read. */
40
- export interface ListAvailabilityRequest {
41
- /** `"<owner>/<event-slug>"`. */
42
- calLink: string;
43
- /** Range start, ISO-8601 instant. */
44
- from: string;
45
- /** Range end, ISO-8601 instant. */
46
- to: string;
47
- /** Present slots in this IANA timezone; omitted → the event's own. */
48
- timeZone?: string;
49
- }
50
-
51
- /**
52
- * The meta-only read (#4633): one event's derived facts, no slots. The route
53
- * fans out over its comma-separated `calLinks`, one of these per link.
54
- */
55
- export interface GetEventMetaRequest {
56
- /** `"<owner>/<event-slug>"`. */
57
- calLink: string;
58
- }
59
-
60
- /** One slot plus who is taking it. */
61
- export interface CreateBookingRequest {
62
- calLink: string;
63
- /** The chosen slot's start, ISO-8601 instant. */
64
- start: string;
65
- attendee: SchedulingAttendee;
66
- }
67
-
68
- export interface SchedulingProvider {
69
- /** Stable identifier for logs and the degraded-state message. */
70
- readonly id: string;
71
- listAvailability(
72
- request: ListAvailabilityRequest,
73
- ): Promise<SchedulingOutcome<AvailabilityResult>>;
74
- createBooking(
75
- request: CreateBookingRequest,
76
- ): Promise<SchedulingOutcome<BookingResult>>;
77
- getEventMeta(
78
- request: GetEventMetaRequest,
79
- ): Promise<SchedulingOutcome<SchedulingEventMeta>>;
80
- }
81
-
82
- /** Convenience constructor so implementations read as one line per failure. */
83
- export function schedulingError(
84
- kind: SchedulingErrorKind,
85
- message: string,
86
- ): { ok: false; error: SchedulingError } {
87
- return { ok: false, error: { kind, message } };
88
- }