@pylonsync/create-pylon 0.19.0 → 0.20.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 (675) hide show
  1. package/bin/create-pylon.js +30 -7
  2. package/package.json +1 -1
  3. package/templates/agency/.env.example +7 -0
  4. package/templates/agency/README.md +28 -3
  5. package/templates/agency/app/(marketing)/contact-form.tsx +20 -18
  6. package/templates/agency/app/(marketing)/page.tsx +3 -3
  7. package/templates/agency/app/(marketing)/work/page.tsx +3 -3
  8. package/templates/agency/app/auth-form.tsx +4 -4
  9. package/templates/agency/app/dashboard/dashboard-client.tsx +110 -64
  10. package/templates/agency/app/dashboard/page.tsx +14 -16
  11. package/templates/agency/app/icon.svg +3 -0
  12. package/templates/agency/app/login/page.tsx +12 -14
  13. package/templates/agency/components/marketing.tsx +12 -5
  14. package/templates/agency/functions/seedStudioBackoffice.ts +42 -11
  15. package/templates/agency/lib/agency.ts +22 -0
  16. package/templates/agency/lib/demo.ts +118 -0
  17. package/templates/agency/lib/site.config.ts +19 -13
  18. package/templates/agency/public/favicon.ico +0 -0
  19. package/templates/agency/tests/agency.test.ts +57 -0
  20. package/templates/ai-chat/.env.example +13 -27
  21. package/templates/ai-chat/README.md +53 -60
  22. package/templates/ai-chat/app/(chat)/c/[id]/page.tsx +19 -0
  23. package/templates/ai-chat/app/(chat)/chat-client.tsx +657 -622
  24. package/templates/ai-chat/app/(chat)/layout.tsx +6 -43
  25. package/templates/ai-chat/app/(chat)/not-found.tsx +6 -10
  26. package/templates/ai-chat/app/(chat)/page.tsx +7 -11
  27. package/templates/ai-chat/app/auth-form.tsx +45 -38
  28. package/templates/ai-chat/app/error.tsx +9 -12
  29. package/templates/ai-chat/app/globals.css +464 -102
  30. package/templates/ai-chat/app/layout.tsx +11 -19
  31. package/templates/ai-chat/app/login/page.tsx +39 -24
  32. package/templates/ai-chat/app.ts +118 -43
  33. package/templates/ai-chat/components/chat/composer.tsx +193 -0
  34. package/templates/ai-chat/components/chat/empty-state.tsx +65 -0
  35. package/templates/ai-chat/components/chat/icons.tsx +198 -0
  36. package/templates/ai-chat/components/chat/markdown.tsx +164 -0
  37. package/templates/ai-chat/components/chat/message.tsx +183 -0
  38. package/templates/ai-chat/components/chat/setup.tsx +71 -0
  39. package/templates/ai-chat/components/chat/sidebar.tsx +288 -0
  40. package/templates/ai-chat/functions/_beginTurn.ts +145 -0
  41. package/templates/ai-chat/functions/_finishTurn.ts +55 -0
  42. package/templates/ai-chat/functions/assistantStatus.ts +13 -0
  43. package/templates/ai-chat/functions/claimGuestHistory.ts +39 -0
  44. package/templates/ai-chat/functions/createConversation.ts +28 -0
  45. package/templates/ai-chat/functions/deleteConversation.ts +15 -20
  46. package/templates/ai-chat/functions/prepareGuestClaim.ts +32 -0
  47. package/templates/ai-chat/functions/renameConversation.ts +19 -0
  48. package/templates/ai-chat/functions/respond.ts +88 -0
  49. package/templates/ai-chat/functions/seedExamples.ts +46 -0
  50. package/templates/ai-chat/functions/stopResponse.ts +30 -0
  51. package/templates/ai-chat/lib/brand-icon.ts +7 -0
  52. package/templates/ai-chat/lib/chat.ts +277 -0
  53. package/templates/ai-chat/lib/claim.ts +15 -0
  54. package/templates/ai-chat/lib/examples.ts +179 -0
  55. package/templates/ai-chat/lib/highlight.ts +107 -0
  56. package/templates/ai-chat/lib/markdown.ts +317 -0
  57. package/templates/ai-chat/lib/provider.ts +38 -0
  58. package/templates/ai-chat/lib/site.config.ts +55 -39
  59. package/templates/ai-chat/tests/chat.test.ts +174 -0
  60. package/templates/ai-chat/tests/components.test.tsx +57 -0
  61. package/templates/ai-chat/tests/markdown.test.ts +88 -0
  62. package/templates/ai-chat/tests/provider.test.ts +32 -0
  63. package/templates/backend/consumer/apps/api/app/layout.tsx +33 -0
  64. package/templates/backend/consumer/apps/api/app/page.tsx +54 -0
  65. package/templates/backend/consumer/apps/api/app.ts +166 -0
  66. package/templates/backend/consumer/apps/api/functions/addComment.ts +24 -0
  67. package/templates/backend/consumer/apps/api/functions/createPost.ts +19 -36
  68. package/templates/backend/consumer/apps/api/functions/deletePost.ts +15 -13
  69. package/templates/backend/consumer/apps/api/functions/myProfile.ts +5 -8
  70. package/templates/backend/consumer/apps/api/functions/seedDemo.ts +95 -0
  71. package/templates/backend/consumer/apps/api/functions/setFollow.ts +36 -0
  72. package/templates/backend/consumer/apps/api/functions/setLike.ts +36 -0
  73. package/templates/backend/consumer/apps/api/functions/upsertProfile.ts +43 -37
  74. package/templates/backend/consumer/apps/api/lib/seed.ts +89 -0
  75. package/templates/backend/consumer/apps/api/lib/social.ts +50 -0
  76. package/templates/backend/consumer/apps/api/package.json +8 -2
  77. package/templates/backend/consumer/apps/api/public/images/posts/arch.jpg +0 -0
  78. package/templates/backend/consumer/apps/api/public/images/posts/coffee.jpg +0 -0
  79. package/templates/backend/consumer/apps/api/public/images/posts/dog.jpg +0 -0
  80. package/templates/backend/consumer/apps/api/public/images/posts/lisbon.jpg +0 -0
  81. package/templates/backend/consumer/apps/api/public/images/posts/ramen.jpg +0 -0
  82. package/templates/backend/consumer/apps/api/public/images/posts/run.jpg +0 -0
  83. package/templates/backend/consumer/apps/api/public/images/posts/sunset.jpg +0 -0
  84. package/templates/backend/consumer/apps/api/tsconfig.json +12 -2
  85. package/templates/chat/README.md +43 -21
  86. package/templates/chat/app/chat-client.tsx +495 -85
  87. package/templates/chat/app/error.tsx +1 -1
  88. package/templates/chat/app/globals.css +170 -54
  89. package/templates/chat/app/layout.tsx +12 -9
  90. package/templates/chat/app/not-found.tsx +4 -2
  91. package/templates/chat/app/page.tsx +9 -19
  92. package/templates/chat/app.ts +96 -17
  93. package/templates/chat/components/chat/avatar.tsx +45 -0
  94. package/templates/chat/components/chat/composer.tsx +110 -0
  95. package/templates/chat/components/chat/message-list.tsx +285 -0
  96. package/templates/chat/components/chat/sidebar.tsx +190 -0
  97. package/templates/chat/functions/joinChat.ts +83 -0
  98. package/templates/chat/functions/renameMember.ts +25 -0
  99. package/templates/chat/lib/chat.ts +188 -0
  100. package/templates/chat/lib/seed.ts +78 -0
  101. package/templates/chat/tests/chat.test.ts +159 -0
  102. package/templates/chat/tests/composer.test.tsx +54 -0
  103. package/templates/consumer/README.md +3 -3
  104. package/templates/consumer/app/globals.css +10 -3
  105. package/templates/consumer/app/icon.svg +1 -0
  106. package/templates/consumer/components/social/avatar.tsx +25 -11
  107. package/templates/consumer/functions/seedFeed.ts +3 -3
  108. package/templates/consumer/lib/seed.ts +43 -85
  109. package/templates/consumer/public/favicon.ico +0 -0
  110. package/templates/consumer/public/images/posts/arch.jpg +0 -0
  111. package/templates/consumer/public/images/posts/coffee.jpg +0 -0
  112. package/templates/consumer/public/images/posts/dog.jpg +0 -0
  113. package/templates/consumer/public/images/posts/lisbon.jpg +0 -0
  114. package/templates/consumer/public/images/posts/ramen.jpg +0 -0
  115. package/templates/consumer/public/images/posts/run.jpg +0 -0
  116. package/templates/consumer/public/images/posts/sunset.jpg +0 -0
  117. package/templates/consumer/tests/seed.test.ts +39 -0
  118. package/templates/creator/.env.example +7 -0
  119. package/templates/creator/README.md +24 -5
  120. package/templates/creator/app/(marketing)/newsletter-signup.tsx +33 -12
  121. package/templates/creator/app/auth-form.tsx +7 -7
  122. package/templates/creator/app/dashboard/dashboard-client.tsx +137 -100
  123. package/templates/creator/app/dashboard/page.tsx +18 -18
  124. package/templates/creator/app/icon.svg +2 -0
  125. package/templates/creator/app/login/page.tsx +14 -14
  126. package/templates/creator/app.ts +4 -4
  127. package/templates/creator/functions/seedDemo.ts +32 -0
  128. package/templates/creator/functions/subscribe.ts +4 -15
  129. package/templates/creator/lib/demo.ts +94 -0
  130. package/templates/creator/lib/site.config.ts +15 -10
  131. package/templates/creator/lib/stats.ts +35 -0
  132. package/templates/creator/public/favicon.ico +0 -0
  133. package/templates/creator/tests/counter.test.ts +92 -0
  134. package/templates/creator/tests/live-count.test.tsx +45 -0
  135. package/templates/crm/README.md +22 -1
  136. package/templates/crm/app/apple-icon.png +0 -0
  137. package/templates/crm/app/companies/companies-view.tsx +32 -14
  138. package/templates/crm/app/companies/page.tsx +2 -1
  139. package/templates/crm/app/contacts/contacts-view.tsx +34 -10
  140. package/templates/crm/app/contacts/page.tsx +2 -1
  141. package/templates/crm/app/deals/[id]/deal-view.tsx +137 -98
  142. package/templates/crm/app/deals/[id]/page.tsx +2 -1
  143. package/templates/crm/app/globals.css +29 -14
  144. package/templates/crm/app/icon.svg +1 -0
  145. package/templates/crm/app/layout.tsx +7 -5
  146. package/templates/crm/app/page.tsx +2 -1
  147. package/templates/crm/app/pipeline-view.tsx +5 -5
  148. package/templates/crm/app/workspace.tsx +29 -17
  149. package/templates/crm/app.ts +5 -5
  150. package/templates/crm/components/activity-timeline.tsx +14 -4
  151. package/templates/crm/components/app-shell.tsx +94 -0
  152. package/templates/crm/components/auth-form.tsx +35 -5
  153. package/templates/crm/components/avatar.tsx +5 -1
  154. package/templates/crm/components/board-skeleton.tsx +5 -5
  155. package/templates/crm/components/brand-mark.tsx +25 -0
  156. package/templates/crm/components/command-palette.tsx +17 -17
  157. package/templates/crm/components/data-table.tsx +97 -36
  158. package/templates/crm/components/deal-card.tsx +23 -27
  159. package/templates/crm/components/filter-tabs.tsx +101 -0
  160. package/templates/crm/components/metrics-bar.tsx +6 -6
  161. package/templates/crm/components/nav.tsx +15 -0
  162. package/templates/crm/components/page-header.tsx +43 -7
  163. package/templates/crm/components/pipeline-board.tsx +16 -10
  164. package/templates/crm/components/require-auth.tsx +24 -26
  165. package/templates/crm/components/sidebar.tsx +54 -49
  166. package/templates/crm/components/stage-badge.tsx +4 -2
  167. package/templates/crm/components/ui/button.tsx +5 -4
  168. package/templates/crm/components/ui/dialog.tsx +1 -1
  169. package/templates/crm/components/ui/input.tsx +1 -1
  170. package/templates/crm/components/ui/select.tsx +1 -1
  171. package/templates/crm/components/ui/textarea.tsx +1 -1
  172. package/templates/crm/functions/seedWorkspace.ts +32 -17
  173. package/templates/crm/lib/brand.ts +9 -0
  174. package/templates/crm/lib/demo-team.ts +51 -0
  175. package/templates/crm/lib/pipeline.ts +37 -0
  176. package/templates/crm/lib/search.ts +4 -3
  177. package/templates/crm/lib/seed.ts +53 -8
  178. package/templates/crm/public/favicon.ico +0 -0
  179. package/templates/crm/tests/components.test.tsx +5 -5
  180. package/templates/crm/tests/demo-team.test.ts +18 -0
  181. package/templates/crm/tests/pipeline.test.ts +59 -3
  182. package/templates/directory/.env.example +7 -0
  183. package/templates/directory/README.md +22 -3
  184. package/templates/directory/app/(marketing)/directory-browse.tsx +30 -12
  185. package/templates/directory/app/(marketing)/page.tsx +6 -4
  186. package/templates/directory/app/auth-form.tsx +4 -4
  187. package/templates/directory/app/dashboard/dashboard-client.tsx +177 -90
  188. package/templates/directory/app/dashboard/page.tsx +16 -14
  189. package/templates/directory/app/icon.svg +4 -0
  190. package/templates/directory/app/login/page.tsx +16 -15
  191. package/templates/directory/functions/seedDemo.ts +43 -0
  192. package/templates/directory/lib/demo.ts +145 -0
  193. package/templates/directory/lib/directory.ts +27 -0
  194. package/templates/directory/lib/site.config.ts +21 -16
  195. package/templates/directory/public/favicon.ico +0 -0
  196. package/templates/directory/tests/directory.test.ts +46 -0
  197. package/templates/expo/consumer/apps/expo/App.tsx +360 -288
  198. package/templates/expo/consumer/apps/expo/app.json +9 -1
  199. package/templates/expo/consumer/apps/expo/package.json +2 -1
  200. package/templates/helpdesk/README.md +21 -0
  201. package/templates/helpdesk/app/apple-icon.png +0 -0
  202. package/templates/helpdesk/app/customers/customers-view.tsx +23 -7
  203. package/templates/helpdesk/app/customers/page.tsx +2 -1
  204. package/templates/helpdesk/app/globals.css +29 -14
  205. package/templates/helpdesk/app/icon.svg +1 -0
  206. package/templates/helpdesk/app/inbox-view.tsx +66 -47
  207. package/templates/helpdesk/app/layout.tsx +7 -5
  208. package/templates/helpdesk/app/page.tsx +4 -7
  209. package/templates/helpdesk/app/tickets/[id]/page.tsx +2 -1
  210. package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +134 -97
  211. package/templates/helpdesk/app/workspace.tsx +33 -22
  212. package/templates/helpdesk/app.ts +2 -2
  213. package/templates/helpdesk/components/app-shell.tsx +94 -0
  214. package/templates/helpdesk/components/auth-form.tsx +35 -5
  215. package/templates/helpdesk/components/avatar.tsx +5 -1
  216. package/templates/helpdesk/components/brand-mark.tsx +28 -0
  217. package/templates/helpdesk/components/command-palette.tsx +17 -17
  218. package/templates/helpdesk/components/data-table.tsx +97 -36
  219. package/templates/helpdesk/components/filter-tabs.tsx +101 -0
  220. package/templates/helpdesk/components/nav.tsx +14 -0
  221. package/templates/helpdesk/components/page-header.tsx +43 -7
  222. package/templates/helpdesk/components/priority-badge.tsx +49 -6
  223. package/templates/helpdesk/components/require-auth.tsx +24 -26
  224. package/templates/helpdesk/components/sidebar.tsx +54 -48
  225. package/templates/helpdesk/components/sla-indicator.tsx +5 -8
  226. package/templates/helpdesk/components/thread.tsx +7 -7
  227. package/templates/helpdesk/components/ticket-list.tsx +79 -19
  228. package/templates/helpdesk/components/ui/button.tsx +5 -4
  229. package/templates/helpdesk/components/ui/dialog.tsx +1 -1
  230. package/templates/helpdesk/components/ui/input.tsx +1 -1
  231. package/templates/helpdesk/components/ui/select.tsx +1 -1
  232. package/templates/helpdesk/components/ui/textarea.tsx +1 -1
  233. package/templates/helpdesk/functions/seedWorkspace.ts +25 -15
  234. package/templates/helpdesk/lib/brand.ts +9 -0
  235. package/templates/helpdesk/lib/demo-team.ts +51 -0
  236. package/templates/helpdesk/lib/search.ts +4 -3
  237. package/templates/helpdesk/lib/seed.ts +43 -2
  238. package/templates/helpdesk/lib/tickets.ts +68 -20
  239. package/templates/helpdesk/public/favicon.ico +0 -0
  240. package/templates/helpdesk/tests/components.test.tsx +10 -0
  241. package/templates/helpdesk/tests/demo-team.test.ts +18 -0
  242. package/templates/helpdesk/tests/tickets.test.ts +70 -0
  243. package/templates/imessage/.env.example +49 -0
  244. package/templates/imessage/AGENTS.md +153 -0
  245. package/templates/imessage/CLAUDE.md +7 -0
  246. package/templates/imessage/README.md +187 -0
  247. package/templates/imessage/app/console.tsx +382 -0
  248. package/templates/imessage/app/contacts/page.tsx +13 -0
  249. package/templates/imessage/app/error.tsx +16 -0
  250. package/templates/imessage/app/gate.tsx +31 -0
  251. package/templates/imessage/app/globals.css +183 -0
  252. package/templates/imessage/app/layout.tsx +17 -0
  253. package/templates/imessage/app/login/login-form.tsx +115 -0
  254. package/templates/imessage/app/login/page.tsx +30 -0
  255. package/templates/imessage/app/not-found.tsx +13 -0
  256. package/templates/imessage/app/page.tsx +14 -0
  257. package/templates/imessage/app/robots.ts +6 -0
  258. package/templates/imessage/app/setup/page.tsx +13 -0
  259. package/templates/imessage/app/sign-out.tsx +20 -0
  260. package/templates/imessage/app.ts +299 -0
  261. package/templates/imessage/bunfig +4 -0
  262. package/templates/imessage/components/avatar.tsx +46 -0
  263. package/templates/imessage/components/contact-panel.tsx +263 -0
  264. package/templates/imessage/components/contacts-view.tsx +172 -0
  265. package/templates/imessage/components/conversation-list.tsx +129 -0
  266. package/templates/imessage/components/nav-rail.tsx +67 -0
  267. package/templates/imessage/components/setup-view.tsx +408 -0
  268. package/templates/imessage/components/thread.tsx +229 -0
  269. package/templates/imessage/components/ui/badge.tsx +37 -0
  270. package/templates/imessage/components/ui/button.tsx +56 -0
  271. package/templates/imessage/components/ui/card.tsx +90 -0
  272. package/templates/imessage/components/ui/dialog.tsx +110 -0
  273. package/templates/imessage/components/ui/input.tsx +30 -0
  274. package/templates/imessage/components/ui/label.tsx +21 -0
  275. package/templates/imessage/components/ui/select.tsx +39 -0
  276. package/templates/imessage/components/ui/table.tsx +124 -0
  277. package/templates/imessage/components/ui/textarea.tsx +24 -0
  278. package/templates/imessage/components.json +20 -0
  279. package/templates/imessage/functions/answerNow.ts +34 -0
  280. package/templates/imessage/functions/assistant.ts +96 -0
  281. package/templates/imessage/functions/beginTurn.ts +101 -0
  282. package/templates/imessage/functions/cancelReminder.ts +13 -0
  283. package/templates/imessage/functions/claimOutbound.ts +44 -0
  284. package/templates/imessage/functions/clearDemo.ts +18 -0
  285. package/templates/imessage/functions/deleteNote.ts +11 -0
  286. package/templates/imessage/functions/deliverOutbound.ts +55 -0
  287. package/templates/imessage/functions/finishTurn.ts +84 -0
  288. package/templates/imessage/functions/fireReminder.ts +42 -0
  289. package/templates/imessage/functions/ingestInbound.ts +22 -0
  290. package/templates/imessage/functions/ownerAccount.ts +13 -0
  291. package/templates/imessage/functions/processTurn.ts +74 -0
  292. package/templates/imessage/functions/relayApply.ts +102 -0
  293. package/templates/imessage/functions/relaySync.ts +63 -0
  294. package/templates/imessage/functions/releaseOutbound.ts +18 -0
  295. package/templates/imessage/functions/resolveTurn.ts +25 -0
  296. package/templates/imessage/functions/saveContact.ts +27 -0
  297. package/templates/imessage/functions/seedDemo.ts +70 -0
  298. package/templates/imessage/functions/sendOwnerMessage.ts +28 -0
  299. package/templates/imessage/functions/sendblueWebhook.ts +49 -0
  300. package/templates/imessage/functions/setContactStatus.ts +17 -0
  301. package/templates/imessage/functions/settleOutbound.ts +35 -0
  302. package/templates/imessage/functions/setupStatus.ts +33 -0
  303. package/templates/imessage/functions/sweep.ts +58 -0
  304. package/templates/imessage/functions/toolCancelReminder.ts +17 -0
  305. package/templates/imessage/functions/toolForgetNote.ts +15 -0
  306. package/templates/imessage/functions/toolListNotes.ts +16 -0
  307. package/templates/imessage/functions/toolListReminders.ts +17 -0
  308. package/templates/imessage/functions/toolSaveNote.ts +22 -0
  309. package/templates/imessage/functions/toolSetReminder.ts +55 -0
  310. package/templates/imessage/functions/updateSettings.ts +48 -0
  311. package/templates/imessage/gitignore +11 -0
  312. package/templates/imessage/lib/agent-call.ts +251 -0
  313. package/templates/imessage/lib/assistant.ts +74 -0
  314. package/templates/imessage/lib/chunk.ts +135 -0
  315. package/templates/imessage/lib/demo.ts +68 -0
  316. package/templates/imessage/lib/format.ts +170 -0
  317. package/templates/imessage/lib/gate.ts +104 -0
  318. package/templates/imessage/lib/handles.ts +67 -0
  319. package/templates/imessage/lib/provider.ts +38 -0
  320. package/templates/imessage/lib/relay-protocol.ts +95 -0
  321. package/templates/imessage/lib/reminders.ts +41 -0
  322. package/templates/imessage/lib/secrets.ts +59 -0
  323. package/templates/imessage/lib/sendblue.ts +173 -0
  324. package/templates/imessage/lib/store.ts +407 -0
  325. package/templates/imessage/lib/transport.ts +135 -0
  326. package/templates/imessage/lib/utils.ts +10 -0
  327. package/templates/imessage/package.json +40 -0
  328. package/templates/imessage/relay/.env.example +14 -0
  329. package/templates/imessage/relay/applescript.ts +70 -0
  330. package/templates/imessage/relay/chatdb.ts +165 -0
  331. package/templates/imessage/relay/client.ts +41 -0
  332. package/templates/imessage/relay/main.ts +137 -0
  333. package/templates/imessage/relay/state.ts +35 -0
  334. package/templates/imessage/tests/agent-call.test.ts +112 -0
  335. package/templates/imessage/tests/applescript.test.ts +88 -0
  336. package/templates/imessage/tests/chatdb.test.ts +139 -0
  337. package/templates/imessage/tests/chunk.test.ts +61 -0
  338. package/templates/imessage/tests/fake-ctx.ts +88 -0
  339. package/templates/imessage/tests/ingest.test.ts +141 -0
  340. package/templates/imessage/tests/lib.test.ts +99 -0
  341. package/templates/imessage/tests/review-fixes.test.ts +138 -0
  342. package/templates/imessage/tests/secrets.test.ts +36 -0
  343. package/templates/imessage/tests/sendblue.test.ts +143 -0
  344. package/templates/imessage/tests/setup.ts +5 -0
  345. package/templates/imessage/tsconfig.json +18 -0
  346. package/templates/inventory/README.md +21 -0
  347. package/templates/inventory/app/apple-icon.png +0 -0
  348. package/templates/inventory/app/globals.css +29 -14
  349. package/templates/inventory/app/icon.svg +1 -0
  350. package/templates/inventory/app/layout.tsx +7 -5
  351. package/templates/inventory/app/movements/movements-view.tsx +32 -10
  352. package/templates/inventory/app/movements/page.tsx +2 -1
  353. package/templates/inventory/app/page.tsx +4 -6
  354. package/templates/inventory/app/products/[id]/page.tsx +2 -1
  355. package/templates/inventory/app/products/[id]/product-view.tsx +55 -25
  356. package/templates/inventory/app/products-view.tsx +74 -33
  357. package/templates/inventory/app/workspace.tsx +34 -16
  358. package/templates/inventory/app.ts +10 -2
  359. package/templates/inventory/components/app-shell.tsx +94 -0
  360. package/templates/inventory/components/auth-form.tsx +35 -5
  361. package/templates/inventory/components/avatar.tsx +5 -1
  362. package/templates/inventory/components/brand-mark.tsx +25 -0
  363. package/templates/inventory/components/category-tile.tsx +49 -0
  364. package/templates/inventory/components/command-palette.tsx +17 -17
  365. package/templates/inventory/components/data-table.tsx +97 -36
  366. package/templates/inventory/components/filter-tabs.tsx +101 -0
  367. package/templates/inventory/components/nav.tsx +14 -0
  368. package/templates/inventory/components/page-header.tsx +43 -7
  369. package/templates/inventory/components/require-auth.tsx +24 -26
  370. package/templates/inventory/components/sidebar.tsx +54 -48
  371. package/templates/inventory/components/stock-level.tsx +30 -14
  372. package/templates/inventory/components/summary-bar.tsx +8 -15
  373. package/templates/inventory/components/ui/button.tsx +5 -4
  374. package/templates/inventory/components/ui/dialog.tsx +1 -1
  375. package/templates/inventory/components/ui/input.tsx +1 -1
  376. package/templates/inventory/components/ui/select.tsx +1 -1
  377. package/templates/inventory/components/ui/textarea.tsx +1 -1
  378. package/templates/inventory/functions/seedWorkspace.ts +18 -8
  379. package/templates/inventory/lib/brand.ts +9 -0
  380. package/templates/inventory/lib/demo-team.ts +51 -0
  381. package/templates/inventory/lib/search.ts +4 -3
  382. package/templates/inventory/lib/seed.ts +29 -2
  383. package/templates/inventory/lib/stock.ts +32 -0
  384. package/templates/inventory/public/favicon.ico +0 -0
  385. package/templates/inventory/tests/demo-team.test.ts +18 -0
  386. package/templates/inventory/tests/stock.test.ts +29 -0
  387. package/templates/invoices/README.md +16 -0
  388. package/templates/invoices/app/apple-icon.png +0 -0
  389. package/templates/invoices/app/clients/clients-view.tsx +18 -6
  390. package/templates/invoices/app/clients/page.tsx +2 -1
  391. package/templates/invoices/app/globals.css +29 -14
  392. package/templates/invoices/app/icon.svg +1 -0
  393. package/templates/invoices/app/invoices/[id]/invoice-view.tsx +72 -48
  394. package/templates/invoices/app/invoices/[id]/page.tsx +2 -1
  395. package/templates/invoices/app/invoices-view.tsx +81 -32
  396. package/templates/invoices/app/layout.tsx +7 -5
  397. package/templates/invoices/app/page.tsx +4 -6
  398. package/templates/invoices/app/workspace.tsx +26 -16
  399. package/templates/invoices/app.ts +2 -2
  400. package/templates/invoices/components/app-shell.tsx +94 -0
  401. package/templates/invoices/components/auth-form.tsx +35 -5
  402. package/templates/invoices/components/avatar.tsx +5 -1
  403. package/templates/invoices/components/brand-mark.tsx +26 -0
  404. package/templates/invoices/components/command-palette.tsx +17 -17
  405. package/templates/invoices/components/data-table.tsx +97 -36
  406. package/templates/invoices/components/filter-tabs.tsx +101 -0
  407. package/templates/invoices/components/line-items.tsx +19 -14
  408. package/templates/invoices/components/nav.tsx +14 -0
  409. package/templates/invoices/components/page-header.tsx +43 -7
  410. package/templates/invoices/components/require-auth.tsx +24 -26
  411. package/templates/invoices/components/sidebar.tsx +54 -48
  412. package/templates/invoices/components/status-badge.tsx +7 -7
  413. package/templates/invoices/components/summary-bar.tsx +5 -5
  414. package/templates/invoices/components/ui/button.tsx +5 -4
  415. package/templates/invoices/components/ui/dialog.tsx +1 -1
  416. package/templates/invoices/components/ui/input.tsx +1 -1
  417. package/templates/invoices/components/ui/select.tsx +1 -1
  418. package/templates/invoices/components/ui/textarea.tsx +1 -1
  419. package/templates/invoices/functions/recordPayment.ts +3 -0
  420. package/templates/invoices/lib/billing.ts +40 -0
  421. package/templates/invoices/lib/brand.ts +9 -0
  422. package/templates/invoices/lib/search.ts +4 -3
  423. package/templates/invoices/public/favicon.ico +0 -0
  424. package/templates/invoices/tests/billing.test.ts +27 -0
  425. package/templates/ios/consumer/apps/ios/Package.swift +4 -3
  426. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ActivityView.swift +107 -0
  427. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AppModel.swift +171 -0
  428. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/AuthView.swift +182 -0
  429. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/CommentsView.swift +136 -0
  430. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ComposeView.swift +152 -0
  431. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ExploreView.swift +117 -0
  432. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/FeedView.swift +175 -159
  433. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/MainTabView.swift +54 -0
  434. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Media.swift +271 -0
  435. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Models.swift +101 -4
  436. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/PostCard.swift +245 -0
  437. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileEditorView.swift +208 -0
  438. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileView.swift +183 -0
  439. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/RootView.swift +25 -31
  440. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/SocialStore.swift +285 -0
  441. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/StoryViewer.swift +155 -0
  442. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/Theme.swift +82 -0
  443. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/__APP_NAME_PASCAL__App.swift +4 -47
  444. package/templates/ios/consumer/apps/ios/project.yml +13 -9
  445. package/templates/local-service/.env.example +7 -0
  446. package/templates/local-service/README.md +26 -2
  447. package/templates/local-service/app/(marketing)/booking-widget.tsx +48 -9
  448. package/templates/local-service/app/(marketing)/page.tsx +66 -42
  449. package/templates/local-service/app/auth-form.tsx +8 -8
  450. package/templates/local-service/app/dashboard/dashboard-client.tsx +119 -92
  451. package/templates/local-service/app/dashboard/page.tsx +14 -14
  452. package/templates/local-service/app/icon.svg +2 -0
  453. package/templates/local-service/app/login/page.tsx +15 -15
  454. package/templates/local-service/functions/seedDemo.ts +49 -0
  455. package/templates/local-service/lib/demo.ts +135 -0
  456. package/templates/local-service/lib/hours.ts +44 -0
  457. package/templates/local-service/lib/site.config.ts +8 -3
  458. package/templates/local-service/lib/slots.ts +3 -0
  459. package/templates/local-service/public/favicon.ico +0 -0
  460. package/templates/local-service/tests/booking.test.ts +104 -0
  461. package/templates/marketplace/README.md +23 -15
  462. package/templates/marketplace/app/globals.css +172 -114
  463. package/templates/marketplace/app/layout.tsx +75 -30
  464. package/templates/marketplace/app/listing/[id]/page.tsx +210 -123
  465. package/templates/marketplace/app/me/page.tsx +4 -4
  466. package/templates/marketplace/app/page.tsx +221 -298
  467. package/templates/marketplace/app/sell/page.tsx +8 -11
  468. package/templates/marketplace/app.ts +32 -7
  469. package/templates/marketplace/client/ActivityFeed.tsx +311 -0
  470. package/templates/marketplace/client/AuthNav.tsx +24 -16
  471. package/templates/marketplace/client/ListingCard.tsx +118 -0
  472. package/templates/marketplace/client/LiveGrid.tsx +92 -0
  473. package/templates/marketplace/client/LiveListingStatus.tsx +32 -24
  474. package/templates/marketplace/client/LoginCard.tsx +114 -136
  475. package/templates/marketplace/client/MarketProvider.tsx +3 -5
  476. package/templates/marketplace/client/MyMarket.tsx +371 -185
  477. package/templates/marketplace/client/OfferPanel.tsx +421 -287
  478. package/templates/marketplace/client/SellForm.tsx +271 -192
  479. package/templates/marketplace/client/ThemeToggle.tsx +1 -0
  480. package/templates/marketplace/client/WatchButton.tsx +16 -16
  481. package/templates/marketplace/client/market.ts +61 -14
  482. package/templates/marketplace/functions/seedMarket.ts +143 -36
  483. package/templates/marketplace/functions/seedOffers.ts +101 -0
  484. package/templates/marketplace/lib/catalog.ts +72 -1
  485. package/templates/marketplace/public/images/listings/ceramic-lamp-640.webp +0 -0
  486. package/templates/marketplace/public/images/listings/ceramic-lamp.webp +0 -0
  487. package/templates/marketplace/public/images/listings/duffel-640.webp +0 -0
  488. package/templates/marketplace/public/images/listings/duffel.webp +0 -0
  489. package/templates/marketplace/public/images/listings/film-camera-640.webp +0 -0
  490. package/templates/marketplace/public/images/listings/film-camera.webp +0 -0
  491. package/templates/marketplace/public/images/listings/lounge-chair-640.webp +0 -0
  492. package/templates/marketplace/public/images/listings/lounge-chair.webp +0 -0
  493. package/templates/marketplace/public/images/listings/road-bike-640.webp +0 -0
  494. package/templates/marketplace/public/images/listings/road-bike.webp +0 -0
  495. package/templates/marketplace/public/images/listings/slipper-chair-640.webp +0 -0
  496. package/templates/marketplace/public/images/listings/slipper-chair.webp +0 -0
  497. package/templates/marketplace/public/images/listings/turntable-640.webp +0 -0
  498. package/templates/marketplace/public/images/listings/turntable.webp +0 -0
  499. package/templates/marketplace/public/images/listings/wardrobe-640.webp +0 -0
  500. package/templates/marketplace/public/images/listings/wardrobe.webp +0 -0
  501. package/templates/marketplace/public/images/listings/watch-640.webp +0 -0
  502. package/templates/marketplace/public/images/listings/watch.webp +0 -0
  503. package/templates/marketplace/public/images/listings/weekender-bag-640.webp +0 -0
  504. package/templates/marketplace/public/images/listings/weekender-bag.webp +0 -0
  505. package/templates/marketplace/tests/example.test.ts +64 -2
  506. package/templates/marketplace/tests/listing-card.test.tsx +61 -0
  507. package/templates/projects/README.md +24 -3
  508. package/templates/projects/app/apple-icon.png +0 -0
  509. package/templates/projects/app/clients/clients-view.tsx +20 -6
  510. package/templates/projects/app/clients/page.tsx +2 -1
  511. package/templates/projects/app/globals.css +29 -14
  512. package/templates/projects/app/icon.svg +1 -0
  513. package/templates/projects/app/layout.tsx +7 -5
  514. package/templates/projects/app/page.tsx +3 -5
  515. package/templates/projects/app/projects/[id]/page.tsx +2 -1
  516. package/templates/projects/app/projects/[id]/project-view.tsx +45 -30
  517. package/templates/projects/app/projects-view.tsx +105 -52
  518. package/templates/projects/app/workspace.tsx +41 -20
  519. package/templates/projects/app.ts +2 -2
  520. package/templates/projects/components/app-shell.tsx +94 -0
  521. package/templates/projects/components/auth-form.tsx +35 -5
  522. package/templates/projects/components/avatar.tsx +5 -1
  523. package/templates/projects/components/board-skeleton.tsx +3 -3
  524. package/templates/projects/components/brand-mark.tsx +25 -0
  525. package/templates/projects/components/command-palette.tsx +17 -17
  526. package/templates/projects/components/data-table.tsx +97 -36
  527. package/templates/projects/components/filter-tabs.tsx +101 -0
  528. package/templates/projects/components/meter.tsx +48 -0
  529. package/templates/projects/components/nav.tsx +14 -0
  530. package/templates/projects/components/page-header.tsx +43 -7
  531. package/templates/projects/components/require-auth.tsx +24 -26
  532. package/templates/projects/components/sidebar.tsx +54 -48
  533. package/templates/projects/components/summary-bar.tsx +49 -0
  534. package/templates/projects/components/task-board.tsx +10 -9
  535. package/templates/projects/components/time-dialog.tsx +29 -5
  536. package/templates/projects/components/ui/button.tsx +5 -4
  537. package/templates/projects/components/ui/dialog.tsx +1 -1
  538. package/templates/projects/components/ui/input.tsx +1 -1
  539. package/templates/projects/components/ui/select.tsx +1 -1
  540. package/templates/projects/components/ui/textarea.tsx +1 -1
  541. package/templates/projects/functions/seedWorkspace.ts +26 -12
  542. package/templates/projects/lib/brand.ts +9 -0
  543. package/templates/projects/lib/demo-team.ts +51 -0
  544. package/templates/projects/lib/search.ts +4 -3
  545. package/templates/projects/lib/seed.ts +35 -1
  546. package/templates/projects/lib/work.ts +63 -0
  547. package/templates/projects/public/favicon.ico +0 -0
  548. package/templates/projects/tests/demo-team.test.ts +18 -0
  549. package/templates/projects/tests/work.test.ts +65 -0
  550. package/templates/restaurant/.env.example +7 -0
  551. package/templates/restaurant/README.md +29 -3
  552. package/templates/restaurant/app/(marketing)/layout.tsx +4 -2
  553. package/templates/restaurant/app/(marketing)/page.tsx +47 -23
  554. package/templates/restaurant/app/(marketing)/reservation-widget.tsx +37 -7
  555. package/templates/restaurant/app/auth-form.tsx +8 -8
  556. package/templates/restaurant/app/dashboard/dashboard-client.tsx +241 -109
  557. package/templates/restaurant/app/dashboard/page.tsx +14 -14
  558. package/templates/restaurant/app/icon.svg +4 -0
  559. package/templates/restaurant/app/login/page.tsx +15 -15
  560. package/templates/restaurant/functions/seedDemo.ts +44 -0
  561. package/templates/restaurant/lib/demo.ts +144 -0
  562. package/templates/restaurant/lib/hours.ts +45 -0
  563. package/templates/restaurant/lib/site.config.ts +3 -3
  564. package/templates/restaurant/public/favicon.ico +0 -0
  565. package/templates/restaurant/tests/reservations.test.ts +67 -0
  566. package/templates/saas/README.md +54 -35
  567. package/templates/saas/app/(auth)/auth-form.tsx +25 -23
  568. package/templates/saas/app/(auth)/forgot-password/page.tsx +1 -1
  569. package/templates/saas/app/(auth)/layout.tsx +27 -33
  570. package/templates/saas/app/(auth)/login/page.tsx +1 -1
  571. package/templates/saas/app/(auth)/reset-password/page.tsx +1 -1
  572. package/templates/saas/app/(auth)/signup/page.tsx +1 -1
  573. package/templates/saas/app/(marketing)/layout.tsx +127 -253
  574. package/templates/saas/app/(marketing)/page.tsx +133 -372
  575. package/templates/saas/app/(marketing)/pricing/page.tsx +21 -19
  576. package/templates/saas/app/(marketing)/products/[slug]/page.tsx +40 -95
  577. package/templates/saas/app/dashboard/board-client.tsx +416 -0
  578. package/templates/saas/app/dashboard/dashboard-client.tsx +30 -615
  579. package/templates/saas/app/dashboard/layout.tsx +27 -13
  580. package/templates/saas/app/dashboard/overview-client.tsx +332 -0
  581. package/templates/saas/app/dashboard/page.tsx +29 -35
  582. package/templates/saas/app/dashboard/projects/[id]/page.tsx +29 -0
  583. package/templates/saas/app/dashboard/projects/page.tsx +11 -7
  584. package/templates/saas/app/dashboard/projects-client.tsx +398 -0
  585. package/templates/saas/app/globals.css +5 -4
  586. package/templates/saas/app/icon.svg +1 -0
  587. package/templates/saas/app/onboarding/onboarding-client.tsx +93 -26
  588. package/templates/saas/app/onboarding/page.tsx +6 -2
  589. package/templates/saas/app.ts +68 -13
  590. package/templates/saas/components/dashboard-shell.tsx +212 -132
  591. package/templates/saas/components/marketing.tsx +137 -274
  592. package/templates/saas/components/pricing-table.tsx +12 -11
  593. package/templates/saas/components/setup-checklist.tsx +31 -29
  594. package/templates/saas/components/task-ui.tsx +199 -0
  595. package/templates/saas/functions/createProject.ts +7 -13
  596. package/templates/saas/functions/createTask.ts +67 -0
  597. package/templates/saas/functions/deleteProject.ts +17 -0
  598. package/templates/saas/functions/deleteTask.ts +14 -0
  599. package/templates/saas/functions/seedWorkspace.ts +46 -0
  600. package/templates/saas/functions/setProjectStatus.ts +41 -0
  601. package/templates/saas/functions/updateProfile.ts +13 -0
  602. package/templates/saas/functions/updateProject.ts +4 -5
  603. package/templates/saas/functions/updateTask.ts +88 -0
  604. package/templates/saas/lib/members.ts +53 -0
  605. package/templates/saas/lib/names.ts +53 -0
  606. package/templates/saas/lib/plans.ts +11 -0
  607. package/templates/saas/lib/sample-workspace.ts +117 -0
  608. package/templates/saas/lib/site.config.ts +200 -463
  609. package/templates/saas/lib/tasks.ts +101 -0
  610. package/templates/saas/lib/use-live.ts +25 -0
  611. package/templates/saas/public/favicon.ico +0 -0
  612. package/templates/saas/public/screenshots/board.png +0 -0
  613. package/templates/saas/public/screenshots/members.png +0 -0
  614. package/templates/saas/public/screenshots/overview.png +0 -0
  615. package/templates/saas/public/screenshots/projects.png +0 -0
  616. package/templates/saas/scripts/capture-screenshots.mjs +156 -0
  617. package/templates/saas/tests/names.test.ts +35 -0
  618. package/templates/saas/tests/plans.test.ts +12 -0
  619. package/templates/saas/tests/sample-workspace.test.ts +46 -0
  620. package/templates/saas/tests/tasks.test.ts +81 -0
  621. package/templates/shop/README.md +25 -13
  622. package/templates/shop/app/(marketing)/layout.tsx +94 -67
  623. package/templates/shop/app/(marketing)/not-found.tsx +13 -12
  624. package/templates/shop/app/(marketing)/page.tsx +120 -59
  625. package/templates/shop/app/(marketing)/shop-client.tsx +364 -237
  626. package/templates/shop/app/(marketing)/success/page.tsx +75 -22
  627. package/templates/shop/app/globals.css +94 -1
  628. package/templates/shop/app/layout.tsx +4 -4
  629. package/templates/shop/app/login/page.tsx +4 -4
  630. package/templates/shop/app.ts +18 -6
  631. package/templates/shop/components/cart-button.tsx +33 -0
  632. package/templates/shop/components/marketing.tsx +17 -87
  633. package/templates/shop/components/shelf-count.tsx +36 -0
  634. package/templates/shop/functions/checkout.ts +17 -1
  635. package/templates/shop/lib/cart-store.ts +57 -0
  636. package/templates/shop/lib/shelf-store.ts +35 -0
  637. package/templates/shop/lib/shop.ts +27 -0
  638. package/templates/shop/lib/site.config.ts +108 -98
  639. package/templates/shop/public/images/hero.jpg +0 -0
  640. package/templates/shop/public/images/products/cedar-smoke.jpg +0 -0
  641. package/templates/shop/public/images/products/fig-leaf.jpg +0 -0
  642. package/templates/shop/public/images/products/linen.jpg +0 -0
  643. package/templates/shop/public/images/products/sea-salt.jpg +0 -0
  644. package/templates/shop/public/images/products/tobacco-oak.jpg +0 -0
  645. package/templates/shop/tests/shop.test.ts +64 -0
  646. package/templates/waitlist/.env.example +7 -0
  647. package/templates/waitlist/README.md +29 -13
  648. package/templates/waitlist/app/(marketing)/page.tsx +9 -7
  649. package/templates/waitlist/app/(marketing)/waitlist-hero.tsx +64 -31
  650. package/templates/waitlist/app/auth-form.tsx +8 -8
  651. package/templates/waitlist/app/dashboard/dashboard-client.tsx +132 -95
  652. package/templates/waitlist/app/dashboard/page.tsx +17 -16
  653. package/templates/waitlist/app/icon.svg +2 -0
  654. package/templates/waitlist/app/login/page.tsx +14 -14
  655. package/templates/waitlist/app.ts +6 -6
  656. package/templates/waitlist/functions/joinWaitlist.ts +6 -18
  657. package/templates/waitlist/functions/seedDemo.ts +32 -0
  658. package/templates/waitlist/lib/demo.ts +94 -0
  659. package/templates/waitlist/lib/launch.ts +19 -0
  660. package/templates/waitlist/lib/site.config.ts +14 -9
  661. package/templates/waitlist/lib/stats.ts +35 -0
  662. package/templates/waitlist/public/favicon.ico +0 -0
  663. package/templates/waitlist/tests/counter.test.ts +107 -0
  664. package/templates/waitlist/tests/live-count.test.tsx +48 -0
  665. package/templates/ai-chat/tests/example.test.ts +0 -12
  666. package/templates/backend/consumer/apps/api/functions/feed.ts +0 -57
  667. package/templates/backend/consumer/apps/api/functions/profilePosts.ts +0 -17
  668. package/templates/backend/consumer/apps/api/functions/toggleLike.ts +0 -48
  669. package/templates/backend/consumer/apps/api/schema.ts +0 -143
  670. package/templates/chat/functions/_keep.ts +0 -13
  671. package/templates/chat/tests/example.test.ts +0 -12
  672. package/templates/ios/consumer/apps/ios/Sources/__APP_NAME_PASCAL__/ProfileSetupView.swift +0 -68
  673. package/templates/marketplace/client/LiveTicker.tsx +0 -116
  674. package/templates/marketplace/client/ScrollToListingsLink.tsx +0 -29
  675. package/templates/marketplace/public/images/marketplace-hero.webp +0 -0
@@ -0,0 +1,170 @@
1
+ // Pure display helpers for the dashboard. No React, no data access.
2
+
3
+ export interface ThreadMessage {
4
+ id: string;
5
+ direction: "in" | "out";
6
+ kind: string;
7
+ text: string;
8
+ status: string;
9
+ detail?: string | null;
10
+ createdAt: string;
11
+ }
12
+
13
+ export function initials(name: string | null | undefined, handle: string): string {
14
+ const source = (name ?? "").trim();
15
+ if (source) {
16
+ const parts = source.split(/\s+/).filter(Boolean);
17
+ const letters = parts.length > 1 ? parts[0][0] + parts[parts.length - 1][0] : parts[0].slice(0, 2);
18
+ return letters.toUpperCase();
19
+ }
20
+ if (handle.includes("@")) return handle[0].toUpperCase();
21
+ return handle.slice(-2);
22
+ }
23
+
24
+ /** "3:04 PM" today, "Yesterday", weekday within a week, else "Sep 3". */
25
+ export function listTime(iso: string | null | undefined, now = new Date()): string {
26
+ if (!iso) return "";
27
+ const d = new Date(iso);
28
+ if (Number.isNaN(d.getTime())) return "";
29
+ const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
30
+ const days = Math.round((startOfDay(now) - startOfDay(d)) / 86_400_000);
31
+ if (days <= 0) return d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
32
+ if (days === 1) return "Yesterday";
33
+ if (days < 7) return d.toLocaleDateString("en-US", { weekday: "long" });
34
+ return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
35
+ }
36
+
37
+ /** Separator label between bubbles, iMessage style: "Today 3:04 PM". */
38
+ export function separatorLabel(iso: string, now = new Date()): string {
39
+ const d = new Date(iso);
40
+ const time = d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
41
+ const day = listTime(iso, now);
42
+ return day.includes(":") ? `Today ${time}` : `${day} ${time}`;
43
+ }
44
+
45
+ /** "just now", "2 min ago", "3 h ago", or a date. */
46
+ export function ago(iso: string | null | undefined, now = new Date()): string {
47
+ if (!iso) return "never";
48
+ const ms = now.getTime() - new Date(iso).getTime();
49
+ if (!Number.isFinite(ms)) return "never";
50
+ if (ms < 45_000) return "just now";
51
+ if (ms < 3_600_000) return `${Math.round(ms / 60_000)} min ago`;
52
+ if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)} h ago`;
53
+ return new Date(iso).toLocaleDateString("en-US", { month: "short", day: "numeric" });
54
+ }
55
+
56
+ export interface ThreadItem {
57
+ message: ThreadMessage;
58
+ /** Show a time separator above this bubble. */
59
+ separator: string | null;
60
+ /** Last bubble in a run from the same side: gets the tail. */
61
+ tail: boolean;
62
+ /** Caption under the bubble (status), or null. */
63
+ caption: string | null;
64
+ captionTone: "muted" | "warn" | "error";
65
+ }
66
+
67
+ const GAP_FOR_SEPARATOR_MS = 60 * 60 * 1000;
68
+
69
+ /** Caption for a message whose status the owner should see. */
70
+ export function statusCaption(m: ThreadMessage, isLastOutbound: boolean): Pick<ThreadItem, "caption" | "captionTone"> {
71
+ if (m.direction === "in") {
72
+ switch (m.status) {
73
+ case "awaiting_setup":
74
+ return { caption: "Not answered yet: waiting for setup", captionTone: "warn" };
75
+ case "rate_limited":
76
+ return { caption: "Not answered: hourly limit reached", captionTone: "warn" };
77
+ case "failed":
78
+ return { caption: `Reply failed${m.detail ? `: ${m.detail}` : ""}`, captionTone: "error" };
79
+ case "ignored":
80
+ if (m.detail === "unknown_sender") return { caption: "Not answered: not on your allowlist", captionTone: "muted" };
81
+ if (m.detail === "blocked") return { caption: "Not answered: blocked", captionTone: "muted" };
82
+ if (m.detail === "opt_out") return { caption: "Texted STOP: opted out", captionTone: "muted" };
83
+ if (m.detail === "opt_in") return { caption: "Texted START: opted back in", captionTone: "muted" };
84
+ if (m.detail === "opted_out") return { caption: "Not answered: opted out", captionTone: "muted" };
85
+ if (m.detail === "paused") return { caption: "Not answered: assistant paused", captionTone: "muted" };
86
+ return { caption: "Not answered", captionTone: "muted" };
87
+ default:
88
+ return { caption: null, captionTone: "muted" };
89
+ }
90
+ }
91
+ const who = m.kind === "owner" ? "Sent by you" : m.kind === "reminder" ? "Reminder" : m.kind === "auto_reply" ? "Automatic reply" : null;
92
+ switch (m.status) {
93
+ case "queued":
94
+ case "leased":
95
+ return { caption: "Sending…", captionTone: "muted" };
96
+ case "dry_run":
97
+ return { caption: "Dry run: not sent", captionTone: "warn" };
98
+ case "failed":
99
+ return { caption: `Not delivered${m.detail ? `: ${m.detail}` : ""}`, captionTone: "error" };
100
+ default:
101
+ if (isLastOutbound) return { caption: who ? `${who} · Delivered` : "Delivered", captionTone: "muted" };
102
+ return { caption: who, captionTone: "muted" };
103
+ }
104
+ }
105
+
106
+ export function buildThread(messages: ThreadMessage[]): ThreadItem[] {
107
+ const sorted = [...messages].sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0));
108
+ let lastOutboundIndex = -1;
109
+ sorted.forEach((m, i) => {
110
+ if (m.direction === "out") lastOutboundIndex = i;
111
+ });
112
+ return sorted.map((m, i) => {
113
+ const prev = sorted[i - 1];
114
+ const next = sorted[i + 1];
115
+ const gap = prev ? new Date(m.createdAt).getTime() - new Date(prev.createdAt).getTime() : Infinity;
116
+ const { caption, captionTone } = statusCaption(m, i === lastOutboundIndex);
117
+ return {
118
+ message: m,
119
+ separator: gap > GAP_FOR_SEPARATOR_MS ? separatorLabel(m.createdAt) : null,
120
+ tail:
121
+ !next ||
122
+ next.direction !== m.direction ||
123
+ new Date(next.createdAt).getTime() - new Date(m.createdAt).getTime() > GAP_FOR_SEPARATOR_MS ||
124
+ caption !== null,
125
+ caption,
126
+ captionTone,
127
+ };
128
+ });
129
+ }
130
+
131
+ /**
132
+ * The refusal in a tool result, if any: an is_error result, or a JSON body
133
+ * with an `error` field. Returns null when the tool succeeded.
134
+ */
135
+ export function toolRefusal(content: unknown, isError: boolean): string | null {
136
+ const text = typeof content === "string" ? content : JSON.stringify(content ?? "");
137
+ if (isError) return text.slice(0, 160);
138
+ try {
139
+ const parsed = JSON.parse(text) as { error?: unknown };
140
+ if (parsed && typeof parsed === "object" && typeof parsed.error === "string") return parsed.error;
141
+ } catch {
142
+ // Plain-text results are successes.
143
+ }
144
+ return null;
145
+ }
146
+
147
+ /** Human summary of an agent tool call for the activity list. */
148
+ export function describeToolCall(name: string, input: Record<string, unknown>): string {
149
+ switch (name) {
150
+ case "remember_note":
151
+ return `Saved a note: ${String(input.text ?? "")}`;
152
+ case "recall_notes":
153
+ return "Read saved notes";
154
+ case "forget_note":
155
+ return "Deleted a note";
156
+ case "set_reminder": {
157
+ const when = typeof input.when === "string" ? new Date(input.when) : null;
158
+ const at = when && !Number.isNaN(when.getTime())
159
+ ? when.toLocaleString("en-US", { weekday: "short", hour: "numeric", minute: "2-digit" })
160
+ : "later";
161
+ return `Scheduled a reminder for ${at}`;
162
+ }
163
+ case "list_reminders":
164
+ return "Checked upcoming reminders";
165
+ case "cancel_reminder":
166
+ return "Cancelled a reminder";
167
+ default:
168
+ return `Used ${name}`;
169
+ }
170
+ }
@@ -0,0 +1,104 @@
1
+ // Who the assistant answers. Every inbound message goes through `decideInbound`
2
+ // before anything is scheduled, so the assistant never becomes a relay for
3
+ // whoever happens to text the number.
4
+ //
5
+ // allowed contact → answer (subject to the per-contact rate limit)
6
+ // blocked contact → store, never answer
7
+ // unknown sender → store as a request; optionally send ONE fixed reply
8
+ // STOP / START → opt out / back in; applies to every contact
9
+
10
+ export type ContactStatus = "allowed" | "blocked" | "unknown";
11
+ export type UnknownSenderPolicy = "ignore" | "reply_once";
12
+
13
+ export interface GateSettings {
14
+ unknownSenderPolicy: UnknownSenderPolicy;
15
+ /** Inbound messages per contact per rolling hour that get an answer. */
16
+ rateLimitPerHour: number;
17
+ /** When true the assistant answers nobody. Messages are still stored. */
18
+ paused: boolean;
19
+ }
20
+
21
+ export interface GateContact {
22
+ status: ContactStatus;
23
+ optedOut: boolean;
24
+ /** Set once the one-time reply to an unknown sender has been sent. */
25
+ autoReplySentAt: string | null;
26
+ }
27
+
28
+ export type InboundDecision =
29
+ | { action: "answer" }
30
+ | { action: "auto_reply" }
31
+ | { action: "opt_out" }
32
+ | { action: "opt_in" }
33
+ | {
34
+ action: "ignore";
35
+ reason: "blocked" | "unknown_sender" | "opted_out" | "rate_limited" | "paused";
36
+ };
37
+
38
+ const STOP_WORDS = new Set(["stop", "stopall", "unsubscribe", "cancel", "end", "quit"]);
39
+ const START_WORDS = new Set(["start", "unstop", "yes"]);
40
+
41
+ function keyword(text: string): string {
42
+ return text.trim().toLowerCase().replace(/[.!\s]+$/g, "");
43
+ }
44
+
45
+ export function isStopKeyword(text: string): boolean {
46
+ return STOP_WORDS.has(keyword(text));
47
+ }
48
+
49
+ export function isStartKeyword(text: string): boolean {
50
+ return START_WORDS.has(keyword(text));
51
+ }
52
+
53
+ export function decideInbound(
54
+ contact: GateContact,
55
+ text: string,
56
+ settings: GateSettings,
57
+ answeredInLastHour: number,
58
+ ): InboundDecision {
59
+ if (isStopKeyword(text)) return { action: "opt_out" };
60
+ if (contact.optedOut) {
61
+ return isStartKeyword(text) ? { action: "opt_in" } : { action: "ignore", reason: "opted_out" };
62
+ }
63
+ if (contact.status === "blocked") return { action: "ignore", reason: "blocked" };
64
+ if (contact.status !== "allowed") {
65
+ if (settings.unknownSenderPolicy === "reply_once" && !contact.autoReplySentAt && !settings.paused) {
66
+ return { action: "auto_reply" };
67
+ }
68
+ return { action: "ignore", reason: "unknown_sender" };
69
+ }
70
+ if (settings.paused) return { action: "ignore", reason: "paused" };
71
+ if (answeredInLastHour >= Math.max(0, settings.rateLimitPerHour)) {
72
+ return { action: "ignore", reason: "rate_limited" };
73
+ }
74
+ return { action: "answer" };
75
+ }
76
+
77
+ /** Inbound rows stored per unknown sender per hour. Past this, retries and floods are dropped. */
78
+ export const UNKNOWN_SENDER_STORE_LIMIT = 20;
79
+
80
+ /** New unknown contacts created per rolling hour, across all senders. Past this, new senders are dropped. */
81
+ export const NEW_UNKNOWN_CONTACTS_PER_HOUR = 50;
82
+
83
+ /** Minimum time between two STOP (or two START) confirmations to one contact. */
84
+ export const KEYWORD_REPLY_INTERVAL_MS = 24 * 60 * 60 * 1000;
85
+
86
+ /** True when a keyword confirmation may be sent now, given when the last one went out. */
87
+ export function mayConfirmKeyword(lastSentAt: string | null, now: Date): boolean {
88
+ if (!lastSentAt) return true;
89
+ const last = Date.parse(lastSentAt);
90
+ return !Number.isFinite(last) || now.getTime() - last >= KEYWORD_REPLY_INTERVAL_MS;
91
+ }
92
+
93
+ export const DEFAULT_SETTINGS = {
94
+ ownerName: "",
95
+ timezone: "America/Chicago",
96
+ unknownSenderPolicy: "ignore" as UnknownSenderPolicy,
97
+ unknownSenderReply:
98
+ "Hi, this number is an automated assistant that only replies to people on its contact list. Your message was passed along.",
99
+ rateLimitPerHour: 30,
100
+ paused: false,
101
+ };
102
+
103
+ export const OPT_OUT_REPLY = "You won't get more messages from this number. Text START to resume.";
104
+ export const OPT_IN_REPLY = "You're back on. Text STOP any time to stop messages.";
@@ -0,0 +1,67 @@
1
+ // iMessage handles: a phone number in E.164 form or an email address (Apple ID).
2
+ // Every handle that enters the database or leaves through a transport goes
3
+ // through `normalizeHandle`, so one person has exactly one Contact row no matter
4
+ // how a transport formats the number.
5
+
6
+ export type HandleKind = "phone" | "email";
7
+
8
+ export interface NormalizedHandle {
9
+ handle: string;
10
+ kind: HandleKind;
11
+ }
12
+
13
+ const EMAIL_RE = /^[a-z0-9._%+-]{1,64}@[a-z0-9.-]{1,253}\.[a-z]{2,24}$/;
14
+ const E164_RE = /^\+[1-9][0-9]{6,14}$/;
15
+
16
+ /**
17
+ * Normalize a raw handle. Returns null for anything that is not a plausible
18
+ * phone number or email, including group-chat identifiers (`chat123…`).
19
+ *
20
+ * Phone rules: strip spaces, dashes, dots and parentheses. A leading `+` keeps
21
+ * the country code as written. Ten bare digits get `defaultCountryCode`
22
+ * (default "1", North America). Eleven digits starting with that code get a `+`.
23
+ */
24
+ export function normalizeHandle(
25
+ raw: string | null | undefined,
26
+ defaultCountryCode = "1",
27
+ ): NormalizedHandle | null {
28
+ if (typeof raw !== "string") return null;
29
+ let value = raw.trim();
30
+ if (value === "" || value.length > 320) return null;
31
+ // Some transports prefix the service: "tel:+1555…", "mailto:a@b.c", "iMessage;-;+1555…".
32
+ value = value.replace(/^(tel:|mailto:|sms:|imessage:)/i, "");
33
+ const semi = value.lastIndexOf(";");
34
+ if (semi !== -1) value = value.slice(semi + 1);
35
+
36
+ if (value.includes("@")) {
37
+ const email = value.toLowerCase();
38
+ return EMAIL_RE.test(email) ? { handle: email, kind: "email" } : null;
39
+ }
40
+
41
+ const hasPlus = value.startsWith("+");
42
+ if (!/^\+?[0-9\s().-]+$/.test(value)) return null;
43
+ const digits = value.replace(/[^0-9]/g, "");
44
+ let e164: string;
45
+ if (hasPlus) {
46
+ e164 = `+${digits}`;
47
+ } else if (digits.length === 10) {
48
+ e164 = `+${defaultCountryCode}${digits}`;
49
+ } else if (digits.length === 11 && digits.startsWith(defaultCountryCode)) {
50
+ e164 = `+${digits}`;
51
+ } else {
52
+ return null;
53
+ }
54
+ return E164_RE.test(e164) ? { handle: e164, kind: "phone" } : null;
55
+ }
56
+
57
+ /** True when `value` is already a normalized handle. */
58
+ export function isNormalizedHandle(value: string): boolean {
59
+ return E164_RE.test(value) || EMAIL_RE.test(value);
60
+ }
61
+
62
+ /** "+15551234567" → "(555) 123-4567" for North American numbers; others unchanged. */
63
+ export function formatHandle(handle: string): string {
64
+ const m = handle.match(/^\+1(\d{3})(\d{3})(\d{4})$/);
65
+ if (m) return `(${m[1]}) ${m[2]}-${m[3]}`;
66
+ return handle;
67
+ }
@@ -0,0 +1,38 @@
1
+ // Whether the server has a model provider configured, derived from the same
2
+ // environment variables Pylon reads for `ctx.llm`:
3
+ //
4
+ // PYLON_LLM_PROVIDER (or PYLON_AI_PROVIDER) = "anthropic" | "openai"
5
+ // ANTHROPIC_API_KEY / OPENAI_API_KEY (or PYLON_AI_API_KEY)
6
+ //
7
+ // With no provider set, an ANTHROPIC_API_KEY selects Anthropic and an
8
+ // OPENAI_API_KEY selects OpenAI. The result carries no key material.
9
+
10
+ export type ProviderName = "anthropic" | "openai";
11
+
12
+ export interface ProviderStatus {
13
+ configured: boolean;
14
+ provider: ProviderName | null;
15
+ }
16
+
17
+ type Env = Record<string, string | undefined>;
18
+
19
+ function present(env: Env, key: string): boolean {
20
+ const v = env[key];
21
+ return typeof v === "string" && v.trim().length > 0;
22
+ }
23
+
24
+ export function providerStatus(env: Env): ProviderStatus {
25
+ const explicit = (env.PYLON_LLM_PROVIDER || env.PYLON_AI_PROVIDER || "").trim();
26
+ if (explicit === "anthropic") {
27
+ const ok = present(env, "ANTHROPIC_API_KEY") || present(env, "PYLON_AI_API_KEY");
28
+ return { configured: ok, provider: "anthropic" };
29
+ }
30
+ if (explicit === "openai") {
31
+ const ok = present(env, "OPENAI_API_KEY") || present(env, "PYLON_AI_API_KEY");
32
+ return { configured: ok, provider: "openai" };
33
+ }
34
+ if (explicit !== "") return { configured: false, provider: null };
35
+ if (present(env, "ANTHROPIC_API_KEY")) return { configured: true, provider: "anthropic" };
36
+ if (present(env, "OPENAI_API_KEY")) return { configured: true, provider: "openai" };
37
+ return { configured: false, provider: null };
38
+ }
@@ -0,0 +1,95 @@
1
+ // The wire contract between the Mac relay (relay/main.ts) and the server
2
+ // (functions/relaySync.ts). One endpoint, one round trip per poll:
3
+ //
4
+ // relay → server POST /api/fn/relaySync
5
+ // Authorization: Bearer <RELAY_TOKEN>
6
+ // { relayVersion, host, inbound: RelayInboundItem[], acks: RelayAck[] }
7
+ // server → relay { accepted: string[], outbound: RelayOutboundItem[] }
8
+ //
9
+ // `accepted` lists the inbound guids the server stored (or had already
10
+ // stored); the relay advances its chat.db watermark only past those.
11
+ // `outbound` is the next batch of replies to send; each is leased to the relay
12
+ // for OUTBOUND_LEASE_MS and returns to the queue when no ack arrives in time.
13
+
14
+ import { normalizeHandle } from "./handles";
15
+ import type { InboundMessage, MessageTransport, SendResult } from "./transport";
16
+
17
+ export const RELAY_PROTOCOL_VERSION = 1;
18
+ export const MAX_INBOUND_PER_SYNC = 100;
19
+ export const MAX_OUTBOUND_PER_SYNC = 20;
20
+ export const MAX_TEXT_CHARS = 20_000;
21
+ export const OUTBOUND_LEASE_MS = 60_000;
22
+ /** A relay reply leased this many times without an ack is marked failed. */
23
+ export const MAX_RELAY_ATTEMPTS = 5;
24
+
25
+ export interface RelayInboundItem {
26
+ /** chat.db message.guid — globally unique per message. */
27
+ guid: string;
28
+ handle: string;
29
+ text: string;
30
+ sentAt: string;
31
+ service: string;
32
+ }
33
+
34
+ export interface RelayOutboundItem {
35
+ id: string;
36
+ handle: string;
37
+ text: string;
38
+ }
39
+
40
+ export interface RelayAck {
41
+ id: string;
42
+ ok: boolean;
43
+ error?: string;
44
+ /** The relay runs with RELAY_DRY_RUN and did not hand the message to Messages.app. */
45
+ dryRun?: boolean;
46
+ }
47
+
48
+ export interface RelaySyncRequest {
49
+ relayVersion: string;
50
+ host: string;
51
+ inbound: RelayInboundItem[];
52
+ acks: RelayAck[];
53
+ }
54
+
55
+ export interface RelaySyncResponse {
56
+ accepted: string[];
57
+ outbound: RelayOutboundItem[];
58
+ }
59
+
60
+ const GUID_RE = /^[A-Za-z0-9:._-]{1,128}$/;
61
+
62
+ export function parseRelayInbound(item: unknown): { message: InboundMessage } | { skip: string } {
63
+ if (typeof item !== "object" || item === null) return { skip: "not an object" };
64
+ const it = item as Partial<RelayInboundItem>;
65
+ if (typeof it.guid !== "string" || !GUID_RE.test(it.guid)) return { skip: "bad guid" };
66
+ const sender = normalizeHandle(typeof it.handle === "string" ? it.handle : "");
67
+ if (!sender) return { skip: "unrecognized sender" };
68
+ const text = typeof it.text === "string" ? it.text.slice(0, MAX_TEXT_CHARS) : "";
69
+ if (text.trim() === "") return { skip: "empty message" };
70
+ const sentAtMs = typeof it.sentAt === "string" ? Date.parse(it.sentAt) : Number.NaN;
71
+ return {
72
+ message: {
73
+ externalId: `relay:${it.guid}`,
74
+ handle: sender.handle,
75
+ text,
76
+ sentAt: new Date(Number.isFinite(sentAtMs) ? sentAtMs : Date.now()).toISOString(),
77
+ service: typeof it.service === "string" && it.service !== "" ? it.service.slice(0, 32) : "iMessage",
78
+ },
79
+ };
80
+ }
81
+
82
+ /**
83
+ * The relay adapter. Sending does not talk to the Mac: the outbound Message
84
+ * row is already stored with status "queued", and the relay collects it on its
85
+ * next sync. `send` therefore only reports that.
86
+ */
87
+ export function relayTransport(dryRun: boolean): MessageTransport {
88
+ return {
89
+ name: "relay",
90
+ normalizeInbound: parseRelayInbound,
91
+ async send(): Promise<SendResult> {
92
+ return dryRun ? { status: "dry_run" } : { status: "queued" };
93
+ },
94
+ };
95
+ }
@@ -0,0 +1,41 @@
1
+ // Bounds for reminders the assistant schedules. The model picks the time; the
2
+ // server decides whether it is acceptable.
3
+
4
+ export const MIN_REMINDER_LEAD_MS = 5 * 60_000;
5
+ export const MAX_REMINDER_HORIZON_MS = 366 * 24 * 60 * 60 * 1000;
6
+ export const MAX_OPEN_REMINDERS_PER_CONTACT = 20;
7
+ /** Reminders the assistant may create for one contact in a rolling day, sent or not. */
8
+ export const MAX_REMINDERS_PER_CONTACT_PER_DAY = 10;
9
+ export const MAX_REMINDER_CHARS = 500;
10
+ export const MAX_NOTE_CHARS = 500;
11
+ export const MAX_NOTES_PER_CONTACT = 100;
12
+
13
+ export type ReminderTimeCheck = { ok: true; dueAt: string; dueMs: number } | { ok: false; error: string };
14
+
15
+ /**
16
+ * Validate a reminder time. Requires an explicit offset or `Z` so "9am" is
17
+ * never silently read in the server's timezone.
18
+ */
19
+ export function checkReminderTime(when: string, now: Date): ReminderTimeCheck {
20
+ const trimmed = when.trim();
21
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})$/.test(trimmed)) {
22
+ return {
23
+ ok: false,
24
+ error: "Use an ISO 8601 time with a UTC offset, for example 2026-10-03T09:00:00-05:00.",
25
+ };
26
+ }
27
+ const ms = Date.parse(trimmed);
28
+ if (!Number.isFinite(ms)) return { ok: false, error: "That time could not be parsed." };
29
+ if (ms < now.getTime() + MIN_REMINDER_LEAD_MS) {
30
+ return { ok: false, error: "The reminder time must be at least five minutes in the future." };
31
+ }
32
+ if (ms > now.getTime() + MAX_REMINDER_HORIZON_MS) {
33
+ return { ok: false, error: "Reminders can be at most one year out." };
34
+ }
35
+ return { ok: true, dueAt: new Date(ms).toISOString(), dueMs: ms };
36
+ }
37
+
38
+ /** Collapse whitespace and cap length for text the model wants to store. */
39
+ export function cleanStoredText(text: string, max: number): string {
40
+ return text.replace(/\s+/g, " ").trim().slice(0, max);
41
+ }
@@ -0,0 +1,59 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+
3
+ // Shared-secret checks for the two inbound doors (the Sendblue webhook and the
4
+ // Mac relay). Both compare a secret the caller presents against one in the
5
+ // server environment.
6
+
7
+ /**
8
+ * Constant-time string comparison. Both sides are hashed to 32 bytes first so
9
+ * the comparison takes the same time whatever the lengths are, and a length
10
+ * mismatch is not observable.
11
+ */
12
+ export function constantTimeEqual(presented: string, expected: string): boolean {
13
+ const a = createHash("sha256").update(presented, "utf8").digest();
14
+ const b = createHash("sha256").update(expected, "utf8").digest();
15
+ return timingSafeEqual(a, b) && presented.length === expected.length;
16
+ }
17
+
18
+ /** Secrets shorter than this are rejected as configuration, not accepted. */
19
+ export const MIN_SECRET_LENGTH = 24;
20
+
21
+ export type SecretCheck =
22
+ | { ok: true }
23
+ | { ok: false; reason: "not_configured" | "missing" | "invalid" };
24
+
25
+ /**
26
+ * Check a presented secret against the configured one. An unset or too-short
27
+ * configured secret fails closed with `not_configured`: an empty secret must
28
+ * never mean "accept anything".
29
+ */
30
+ export function checkSecret(
31
+ presented: string | null | undefined,
32
+ configured: string | null | undefined,
33
+ ): SecretCheck {
34
+ const expected = (configured ?? "").trim();
35
+ if (expected.length < MIN_SECRET_LENGTH) return { ok: false, reason: "not_configured" };
36
+ const got = (presented ?? "").trim();
37
+ if (got === "") return { ok: false, reason: "missing" };
38
+ return constantTimeEqual(got, expected) ? { ok: true } : { ok: false, reason: "invalid" };
39
+ }
40
+
41
+ /** Pull the token out of an `Authorization: Bearer <token>` header value. */
42
+ export function bearerToken(header: string | null | undefined): string | null {
43
+ if (typeof header !== "string") return null;
44
+ const m = header.match(/^\s*Bearer\s+(\S+)\s*$/i);
45
+ return m ? m[1] : null;
46
+ }
47
+
48
+ /** Case-insensitive header read. The runtime lowercases names; this also covers fixtures. */
49
+ export function headerValue(
50
+ headers: Record<string, string> | null | undefined,
51
+ name: string,
52
+ ): string | null {
53
+ if (!headers) return null;
54
+ const want = name.toLowerCase();
55
+ for (const [k, v] of Object.entries(headers)) {
56
+ if (k.toLowerCase() === want) return v;
57
+ }
58
+ return null;
59
+ }