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.
Files changed (273) hide show
  1. package/component-catalog.md +50 -53
  2. package/dist/lib/apply-module-patches.d.ts +47 -0
  3. package/dist/lib/apply-module-patches.js +370 -0
  4. package/dist/lib/extract-module-bundle.d.ts +12 -3
  5. package/dist/lib/extract-module-bundle.js +236 -24
  6. package/dist/lib/fetch-module.d.ts +3 -38
  7. package/dist/lib/fetch-module.js +26 -8
  8. package/dist/lib/module-license-gate.d.ts +2 -0
  9. package/dist/lib/module-license-gate.js +1 -0
  10. package/dist/lib/paid-paths.d.ts +16 -0
  11. package/dist/lib/paid-paths.js +90 -0
  12. package/dist/module-architecture.d.ts +139 -23
  13. package/dist/module-architecture.js +668 -25
  14. package/dist/parse-args.d.ts +2 -2
  15. package/dist/parse-args.js +3 -1
  16. package/dist/programmatic.d.ts +2 -1
  17. package/dist/programmatic.js +15 -4
  18. package/dist/surfaces.d.ts +12 -36
  19. package/dist/surfaces.js +35 -5
  20. package/dist/sync-routes.js +31 -1
  21. package/package.json +2 -2
  22. package/templates/.claude/agents/web.md +2 -3
  23. package/templates/.claude/rules/architecture.md +5 -5
  24. package/templates/.claude/skills/README.md +2 -1
  25. package/templates/.env.example +21 -9
  26. package/templates/CONVENTIONS.md +6 -7
  27. package/templates/apps/api/.env.example +9 -0
  28. package/templates/apps/api/CLAUDE.md +33 -5
  29. package/templates/apps/api/package.json +0 -1
  30. package/templates/apps/api/src/__tests__/cors.test.ts +195 -0
  31. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +60 -2
  32. package/templates/apps/api/src/__tests__/vercel-handler.test.ts +159 -0
  33. package/templates/apps/api/src/config/__tests__/modules.test.ts +76 -0
  34. package/templates/apps/api/src/config/index.ts +19 -0
  35. package/templates/apps/api/src/config/modules.ts +12 -14
  36. package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -0
  37. package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +89 -0
  38. package/templates/apps/api/src/lib/slice-load.ts +90 -0
  39. package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +52 -32
  40. package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +182 -0
  41. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +293 -0
  42. package/templates/apps/api/src/openapi/index.ts +9 -3
  43. package/templates/apps/api/src/openapi/spec.ts +505 -65
  44. package/templates/apps/api/src/openapi/surface-drift.ts +195 -0
  45. package/templates/apps/api/src/routes/content/__tests__/create.test.ts +14 -0
  46. package/templates/apps/api/src/routes/content/__tests__/delete.test.ts +14 -0
  47. package/templates/apps/api/src/routes/content/__tests__/update.test.ts +14 -0
  48. package/templates/apps/api/src/routes/content/index.ts +38 -11
  49. package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +17 -0
  50. package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +17 -0
  51. package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +796 -0
  52. package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +10 -0
  53. package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +38 -0
  54. package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +814 -0
  55. package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +10 -0
  56. package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +24 -0
  57. package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +17 -0
  58. package/templates/apps/api/src/routes/project-listings/index.ts +37 -10
  59. package/templates/apps/api/src/routes/project-listings/me.ts +10 -0
  60. package/templates/apps/api/src/routes/project-listings/patch-draft.ts +5 -0
  61. package/templates/apps/api/src/routes/project-listings/public.ts +5 -0
  62. package/templates/apps/api/src/routes/project-listings/respond.ts +11 -0
  63. package/templates/apps/api/src/routes/project-listings/resume-token.ts +5 -0
  64. package/templates/apps/api/src/routes/project-listings/start.ts +30 -0
  65. package/templates/apps/api/src/routes/project-listings/submit.ts +5 -0
  66. package/templates/apps/api/src/routes/project-listings/verify-email.ts +150 -0
  67. package/templates/apps/api/src/server.ts +459 -135
  68. package/templates/apps/api/src/services/deliverable-fulfillment.ts +10 -0
  69. package/templates/apps/api/src/services/listing-config.ts +58 -0
  70. package/templates/apps/api/src/services/project-listing-resume-email.ts +326 -0
  71. package/templates/apps/api/src/services/project-listings.ts +229 -4
  72. package/templates/apps/api/src/vercel-handler.ts +60 -26
  73. package/templates/apps/mcp/.env.example +8 -0
  74. package/templates/apps/mcp/CLAUDE.md +2 -2
  75. package/templates/apps/mcp/__tests__/catalog-drift.test.ts +21 -0
  76. package/templates/apps/mcp/__tests__/config/modules.test.ts +78 -0
  77. package/templates/apps/mcp/__tests__/module-exclusion.test.ts +1 -1
  78. package/templates/apps/mcp/src/config/modules.ts +14 -13
  79. package/templates/apps/mcp/src/server.ts +8 -4
  80. package/templates/apps/mcp/src/tools/index.ts +7 -22
  81. package/templates/apps/web/.env.example +15 -0
  82. package/templates/apps/web/app/[locale]/(authed)/{waitlist/__tests__ → __tests__}/gate.test.tsx +4 -4
  83. package/templates/apps/web/app/[locale]/flows/[slug]/FlowStepperClient.tsx +2 -1
  84. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.permalink.test.tsx +2 -1
  85. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/FlowStepperClient.test.tsx +2 -1
  86. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/flow.actions.test.ts +1 -0
  87. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/two-route-tool.test.tsx +1 -0
  88. package/templates/apps/web/app/[locale]/flows/[slug]/flow.actions.ts +2 -1
  89. package/templates/apps/web/app/[locale]/flows/[slug]/page.tsx +1 -0
  90. package/templates/apps/web/app/[locale]/flows/[slug]/start/page.tsx +1 -0
  91. package/templates/apps/web/app/llms.txt/route.ts +12 -9
  92. package/templates/apps/web/jest.config.cjs +14 -13
  93. package/templates/apps/web/next.config.mjs +1 -3
  94. package/templates/apps/web/package.json +0 -6
  95. package/templates/apps/web/public/android-chrome-192x192.png +0 -0
  96. package/templates/apps/web/public/android-chrome-512x512.png +0 -0
  97. package/templates/apps/web/public/apple-touch-icon.png +0 -0
  98. package/templates/apps/web/public/favicon-16x16.png +0 -0
  99. package/templates/apps/web/public/favicon-32x32.png +0 -0
  100. package/templates/apps/web/public/favicon.ico +0 -0
  101. package/templates/apps/web/public/favicon.svg +6 -3
  102. package/templates/apps/web/public/lockup-horizontal.svg +4 -0
  103. package/templates/apps/web/public/logomark.svg +4 -0
  104. package/templates/apps/web/public/site.webmanifest +2 -2
  105. package/templates/apps/web/public/wordmark.svg +4 -0
  106. package/templates/database/CHANGELOG.md +33 -0
  107. package/templates/database/inbox/schema.prisma +67 -0
  108. package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +123 -0
  109. package/templates/database/package.json +1 -1
  110. package/templates/package.json +1 -1
  111. package/templates/.claude/skills/scaffold-commerce/SKILL.md +0 -807
  112. package/templates/.claude/skills/scaffold-commerce/references/lookup-keys-template.md +0 -71
  113. package/templates/.claude/skills/scaffold-commerce/references/marketplace-brand-pages.md +0 -83
  114. package/templates/.claude/skills/scaffold-commerce/templates/checkout-route.template.ts +0 -370
  115. package/templates/.claude/skills/scaffold-commerce/templates/invoice-route.template.ts +0 -376
  116. package/templates/.claude/skills/scaffold-commerce/templates/marketplace-cta-route.tsx.tmpl +0 -72
  117. package/templates/.claude/skills/scaffold-commerce/templates/payment-link-route.template.ts +0 -471
  118. package/templates/.claude/skills/scaffold-commerce/templates/portal-route.template.ts +0 -365
  119. package/templates/.claude/skills/scaffold-commerce/templates/stripe-config.template.ts +0 -60
  120. package/templates/.claude/skills/scaffold-commerce/templates/subscription-route.template.ts +0 -446
  121. package/templates/apps/api/src/__tests__/server.test.ts +0 -253
  122. package/templates/apps/api/src/bin/deliverable-resend.ts +0 -144
  123. package/templates/apps/api/src/bin/deliverable-upload.ts +0 -114
  124. package/templates/apps/api/src/bin/followup-sweep.ts +0 -44
  125. package/templates/apps/api/src/bin/listing-csv-sweep.ts +0 -46
  126. package/templates/apps/api/src/bin/seed-presets.ts +0 -58
  127. package/templates/apps/api/src/bin/seed-waitlist-experiments.ts +0 -104
  128. package/templates/apps/api/src/bin/session-retention-sweep.ts +0 -46
  129. package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -74
  130. package/templates/apps/api/src/lib/stripe.ts +0 -52
  131. package/templates/apps/api/src/lib/tenant-db.ts +0 -225
  132. package/templates/apps/api/src/lib/ws-token.ts +0 -91
  133. package/templates/apps/api/src/middleware/tenant.ts +0 -99
  134. package/templates/apps/api/src/routes/billing/__tests__/portal.test.ts +0 -450
  135. package/templates/apps/api/src/routes/billing/__tests__/read.test.ts +0 -262
  136. package/templates/apps/api/src/routes/billing/__tests__/usage.test.ts +0 -436
  137. package/templates/apps/api/src/routes/billing/index.ts +0 -36
  138. package/templates/apps/api/src/routes/billing/portal.ts +0 -182
  139. package/templates/apps/api/src/routes/billing/read.ts +0 -75
  140. package/templates/apps/api/src/routes/billing/usage.ts +0 -153
  141. package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -687
  142. package/templates/apps/api/src/routes/checkout/index.ts +0 -13
  143. package/templates/apps/api/src/routes/checkout/sessions.ts +0 -197
  144. package/templates/apps/api/src/routes/deliverables/__tests__/index.test.ts +0 -393
  145. package/templates/apps/api/src/routes/deliverables/index.ts +0 -200
  146. package/templates/apps/api/src/routes/flow-checkouts/__tests__/index.test.ts +0 -443
  147. package/templates/apps/api/src/routes/flow-checkouts/index.ts +0 -82
  148. package/templates/apps/api/src/routes/flows/README.md +0 -147
  149. package/templates/apps/api/src/routes/flows/__tests__/index.test.ts +0 -752
  150. package/templates/apps/api/src/routes/flows/__tests__/recommender.test.ts +0 -671
  151. package/templates/apps/api/src/routes/flows/index.ts +0 -202
  152. package/templates/apps/api/src/routes/flows/recommender.ts +0 -443
  153. package/templates/apps/api/src/routes/project-listings/__tests__/copy-edit.test.ts +0 -815
  154. package/templates/apps/api/src/routes/project-listings/__tests__/me-route-precedence.test.ts +0 -221
  155. package/templates/apps/api/src/routes/project-listings/__tests__/tenant-isolation.test.ts +0 -566
  156. package/templates/apps/api/src/routes/project-listings/get.ts +0 -70
  157. package/templates/apps/api/src/routes/project-listings/list.ts +0 -47
  158. package/templates/apps/api/src/routes/project-listings/patch.ts +0 -136
  159. package/templates/apps/api/src/routes/schedule/__tests__/index.test.ts +0 -490
  160. package/templates/apps/api/src/routes/schedule/index.ts +0 -249
  161. package/templates/apps/api/src/routes/slack/__tests__/actions.test.ts +0 -385
  162. package/templates/apps/api/src/routes/slack/actions.ts +0 -177
  163. package/templates/apps/api/src/routes/slack/index.ts +0 -38
  164. package/templates/apps/api/src/routes/waitlist-experiments/__tests__/tenant-isolation.test.ts +0 -577
  165. package/templates/apps/api/src/routes/waitlist-experiments/comparison.ts +0 -78
  166. package/templates/apps/api/src/routes/waitlist-experiments/index.ts +0 -41
  167. package/templates/apps/api/src/routes/waitlist-experiments/list.ts +0 -64
  168. package/templates/apps/api/src/routes/waitlist-experiments/respond.ts +0 -42
  169. package/templates/apps/api/src/routes/waitlist-experiments/signup.ts +0 -84
  170. package/templates/apps/api/src/routes/waitlist-experiments/signups.ts +0 -119
  171. package/templates/apps/api/src/routes/waitlist-signups/__tests__/index.test.ts +0 -238
  172. package/templates/apps/api/src/routes/waitlist-signups/index.ts +0 -101
  173. package/templates/apps/api/src/routes/webhooks/README.md +0 -80
  174. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -315
  175. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -247
  176. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -270
  177. package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -198
  178. package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -633
  179. package/templates/apps/api/src/routes/webhooks/index.ts +0 -41
  180. package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -481
  181. package/templates/apps/api/src/routes/workspaces/__tests__/config-tenant-isolation.test.ts +0 -356
  182. package/templates/apps/api/src/routes/workspaces/__tests__/tenant-isolation.test.ts +0 -671
  183. package/templates/apps/api/src/routes/workspaces/config.ts +0 -125
  184. package/templates/apps/api/src/routes/workspaces/index.ts +0 -194
  185. package/templates/apps/api/src/routes/workspaces/sessions.ts +0 -218
  186. package/templates/apps/api/src/services/__tests__/flow-aggregate.test.ts +0 -425
  187. package/templates/apps/api/src/services/__tests__/flow-engine.test.ts +0 -2840
  188. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -393
  189. package/templates/apps/api/src/services/__tests__/listing-csv-sweep.test.ts +0 -560
  190. package/templates/apps/api/src/services/__tests__/listing-promotion.test.ts +0 -684
  191. package/templates/apps/api/src/services/__tests__/playbook-compile.test.ts +0 -406
  192. package/templates/apps/api/src/services/__tests__/playbook-render.test.ts +0 -290
  193. package/templates/apps/api/src/services/__tests__/project-listing-decision.test.ts +0 -736
  194. package/templates/apps/api/src/services/__tests__/project-listing-flow.test.ts +0 -475
  195. package/templates/apps/api/src/services/__tests__/project-listing-issue.test.ts +0 -340
  196. package/templates/apps/api/src/services/__tests__/recommender-capture.test.ts +0 -983
  197. package/templates/apps/api/src/services/__tests__/tenant-context-cascade.test.ts +0 -71
  198. package/templates/apps/api/src/services/__tests__/tenant-context.test.ts +0 -379
  199. package/templates/apps/api/src/services/__tests__/usage-metering.test.ts +0 -768
  200. package/templates/apps/api/src/services/__tests__/waitlist-dashboard.test.ts +0 -1314
  201. package/templates/apps/api/src/services/__tests__/waitlist-experiments.test.ts +0 -341
  202. package/templates/apps/api/src/services/__tests__/waitlist-followup.test.ts +0 -567
  203. package/templates/apps/api/src/services/__tests__/waitlist-scoring.test.ts +0 -474
  204. package/templates/apps/api/src/services/__tests__/waitlist-signups.test.ts +0 -354
  205. package/templates/apps/api/src/services/flow-aggregate.ts +0 -232
  206. package/templates/apps/api/src/services/flow-checkouts.ts +0 -123
  207. package/templates/apps/api/src/services/flow-engine.ts +0 -1296
  208. package/templates/apps/api/src/services/lead-promotion.ts +0 -176
  209. package/templates/apps/api/src/services/listing-csv-sweep.ts +0 -455
  210. package/templates/apps/api/src/services/listing-promotion.ts +0 -342
  211. package/templates/apps/api/src/services/playbook-compile.ts +0 -398
  212. package/templates/apps/api/src/services/playbook-render.ts +0 -263
  213. package/templates/apps/api/src/services/project-listing-decision.ts +0 -510
  214. package/templates/apps/api/src/services/project-listing-flow.ts +0 -426
  215. package/templates/apps/api/src/services/project-listing-issue.ts +0 -251
  216. package/templates/apps/api/src/services/recommender-capture.ts +0 -835
  217. package/templates/apps/api/src/services/scheduling/cal-provider.ts +0 -392
  218. package/templates/apps/api/src/services/scheduling/index.ts +0 -63
  219. package/templates/apps/api/src/services/scheduling/types.ts +0 -88
  220. package/templates/apps/api/src/services/tenant-context.ts +0 -451
  221. package/templates/apps/api/src/services/usage-metering.ts +0 -699
  222. package/templates/apps/api/src/services/waitlist-dashboard.ts +0 -947
  223. package/templates/apps/api/src/services/waitlist-experiments.ts +0 -213
  224. package/templates/apps/api/src/services/waitlist-followup.ts +0 -486
  225. package/templates/apps/api/src/services/waitlist-scoring.ts +0 -166
  226. package/templates/apps/api/src/services/waitlist-signups.ts +0 -165
  227. package/templates/apps/mcp/__tests__/schedule_tools.test.ts +0 -242
  228. package/templates/apps/mcp/src/tools/create_booking.ts +0 -58
  229. package/templates/apps/mcp/src/tools/get_event_meta.ts +0 -55
  230. package/templates/apps/mcp/src/tools/get_flow.ts +0 -61
  231. package/templates/apps/mcp/src/tools/list_availability.ts +0 -67
  232. package/templates/apps/mcp/src/tools/submit_flow_step.ts +0 -83
  233. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/page.tsx +0 -309
  234. package/templates/apps/web/app/[locale]/(authed)/waitlist/[experimentId]/signups/[leadId]/page.tsx +0 -149
  235. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/comparison.test.tsx +0 -273
  236. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/list.test.tsx +0 -171
  237. package/templates/apps/web/app/[locale]/(authed)/waitlist/__tests__/signup.test.tsx +0 -179
  238. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ExperimentsTable.tsx +0 -116
  239. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/OfferFunnelTable.tsx +0 -66
  240. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/RollupTicker.tsx +0 -61
  241. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/ScoreTrace.tsx +0 -114
  242. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/SignupsTable.tsx +0 -139
  243. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/WaitlistComparisonTabs.tsx +0 -85
  244. package/templates/apps/web/app/[locale]/(authed)/waitlist/components/status-badges.tsx +0 -67
  245. package/templates/apps/web/app/[locale]/(authed)/waitlist/page.tsx +0 -179
  246. package/templates/apps/web/app/[locale]/blog/[[...slug]]/page.tsx +0 -178
  247. package/templates/apps/web/app/[locale]/dev/purchase/__tests__/actions.test.ts +0 -160
  248. package/templates/apps/web/app/[locale]/dev/purchase/error.tsx +0 -25
  249. package/templates/apps/web/app/[locale]/dev/purchase/page.tsx +0 -117
  250. package/templates/apps/web/app/[locale]/dev/purchase/purchase.actions.ts +0 -58
  251. package/templates/apps/web/app/[locale]/docs/[[...slug]]/page.tsx +0 -119
  252. package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/checkout.actions.test.ts +0 -287
  253. package/templates/apps/web/app/[locale]/flows/[slug]/checkout.actions.ts +0 -254
  254. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsClient.tsx +0 -532
  255. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/ResultsEmailStep.tsx +0 -121
  256. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/ResultsClient.checkout.test.tsx +0 -457
  257. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/__tests__/page.test.tsx +0 -396
  258. package/templates/apps/web/app/[locale]/flows/[slug]/results/[payload]/page.tsx +0 -114
  259. package/templates/apps/web/app/[locale]/projects/apply/ListingApplicationClient.tsx +0 -110
  260. package/templates/apps/web/app/[locale]/projects/apply/__tests__/ListingApplicationClient.test.tsx +0 -96
  261. package/templates/apps/web/app/[locale]/schedule/__tests__/page.test.tsx +0 -117
  262. package/templates/apps/web/app/[locale]/schedule/confirmed/__tests__/page.test.tsx +0 -96
  263. package/templates/apps/web/app/[locale]/schedule/confirmed/page.tsx +0 -92
  264. package/templates/apps/web/app/[locale]/schedule/page.tsx +0 -82
  265. package/templates/apps/web/content/en/blog/README.txt +0 -25
  266. package/templates/apps/web/content/en/docs/index.mdx +0 -28
  267. package/templates/apps/web/lib/__mocks__/source-server.js +0 -31
  268. package/templates/apps/web/lib/blog-list-data.ts +0 -95
  269. package/templates/apps/web/lib/blog-post-data.ts +0 -79
  270. package/templates/apps/web/lib/source.ts +0 -34
  271. package/templates/apps/web/public/logomark-dark.svg +0 -4
  272. package/templates/apps/web/public/logomark-light.svg +0 -4
  273. 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
- }