@helloleo/plugins 0.2.21 → 0.2.23

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 (340) hide show
  1. package/dist/index.js +73 -42
  2. package/package.json +3 -4
  3. package/dist/templates/tanstack-router-2026-06-15/components.json +0 -22
  4. package/dist/templates/tanstack-router-2026-06-15/index.html +0 -20
  5. package/dist/templates/tanstack-router-2026-06-15/package.json +0 -90
  6. package/dist/templates/tanstack-router-2026-06-15/src/components/error-component.tsx +0 -50
  7. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/accordion.tsx +0 -51
  8. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/alert-dialog.tsx +0 -115
  9. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/alert.tsx +0 -49
  10. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/aspect-ratio.tsx +0 -5
  11. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/avatar.tsx +0 -47
  12. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/badge.tsx +0 -32
  13. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/breadcrumb.tsx +0 -101
  14. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/button.tsx +0 -49
  15. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/calendar.tsx +0 -177
  16. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/card.tsx +0 -55
  17. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/carousel.tsx +0 -240
  18. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/chart.tsx +0 -331
  19. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/checkbox.tsx +0 -26
  20. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/collapsible.tsx +0 -11
  21. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/command.tsx +0 -143
  22. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/context-menu.tsx +0 -187
  23. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/dialog.tsx +0 -104
  24. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/drawer.tsx +0 -98
  25. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/dropdown-menu.tsx +0 -188
  26. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/form.tsx +0 -171
  27. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/hover-card.tsx +0 -27
  28. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/input-otp.tsx +0 -69
  29. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/input.tsx +0 -22
  30. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/label.tsx +0 -21
  31. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/menubar.tsx +0 -229
  32. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/navigation-menu.tsx +0 -120
  33. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/pagination.tsx +0 -98
  34. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/popover.tsx +0 -31
  35. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/progress.tsx +0 -25
  36. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/radio-group.tsx +0 -36
  37. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/resizable.tsx +0 -37
  38. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/scroll-area.tsx +0 -44
  39. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/select.tsx +0 -152
  40. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/separator.tsx +0 -24
  41. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sheet.tsx +0 -122
  42. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sidebar.tsx +0 -744
  43. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/skeleton.tsx +0 -7
  44. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/slider.tsx +0 -23
  45. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/sonner.tsx +0 -23
  46. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/switch.tsx +0 -27
  47. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/table.tsx +0 -94
  48. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/tabs.tsx +0 -53
  49. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/textarea.tsx +0 -21
  50. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/toggle-group.tsx +0 -57
  51. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/toggle.tsx +0 -42
  52. package/dist/templates/tanstack-router-2026-06-15/src/components/ui/tooltip.tsx +0 -32
  53. package/dist/templates/tanstack-router-2026-06-15/src/hooks/use-mobile.tsx +0 -19
  54. package/dist/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +0 -123
  55. package/dist/templates/tanstack-router-2026-06-15/src/lib/utils.ts +0 -6
  56. package/dist/templates/tanstack-router-2026-06-15/src/main.tsx +0 -37
  57. package/dist/templates/tanstack-router-2026-06-15/src/routeTree.gen.ts +0 -59
  58. package/dist/templates/tanstack-router-2026-06-15/src/routes/README.md +0 -85
  59. package/dist/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +0 -18
  60. package/dist/templates/tanstack-router-2026-06-15/src/routes/index.tsx +0 -18
  61. package/dist/templates/tanstack-router-2026-06-15/src/styles.css +0 -214
  62. package/dist/templates/tanstack-router-2026-06-15/tsconfig.json +0 -27
  63. package/dist/templates/tanstack-router-2026-06-15/vite.config.ts +0 -23
  64. package/dist/templates/tanstack-start-2026-06-18/components.json +0 -22
  65. package/dist/templates/tanstack-start-2026-06-18/drizzle.config.ts +0 -15
  66. package/dist/templates/tanstack-start-2026-06-18/package.json +0 -104
  67. package/dist/templates/tanstack-start-2026-06-18/src/components/error-component.tsx +0 -50
  68. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/accordion.tsx +0 -51
  69. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/alert-dialog.tsx +0 -115
  70. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/alert.tsx +0 -49
  71. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/aspect-ratio.tsx +0 -5
  72. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/avatar.tsx +0 -47
  73. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/badge.tsx +0 -32
  74. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/breadcrumb.tsx +0 -101
  75. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/button.tsx +0 -49
  76. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/calendar.tsx +0 -177
  77. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/card.tsx +0 -55
  78. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/carousel.tsx +0 -240
  79. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/chart.tsx +0 -371
  80. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/checkbox.tsx +0 -26
  81. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/collapsible.tsx +0 -11
  82. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/command.tsx +0 -143
  83. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/context-menu.tsx +0 -187
  84. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/dialog.tsx +0 -104
  85. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/drawer.tsx +0 -98
  86. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/dropdown-menu.tsx +0 -188
  87. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/form.tsx +0 -171
  88. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/hover-card.tsx +0 -27
  89. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/input-otp.tsx +0 -69
  90. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/input.tsx +0 -22
  91. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/label.tsx +0 -21
  92. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/menubar.tsx +0 -229
  93. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/navigation-menu.tsx +0 -120
  94. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/pagination.tsx +0 -98
  95. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/popover.tsx +0 -31
  96. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/progress.tsx +0 -25
  97. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/radio-group.tsx +0 -36
  98. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/resizable.tsx +0 -37
  99. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/scroll-area.tsx +0 -44
  100. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/select.tsx +0 -152
  101. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/separator.tsx +0 -24
  102. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/sheet.tsx +0 -122
  103. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/sidebar.tsx +0 -744
  104. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/skeleton.tsx +0 -7
  105. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/slider.tsx +0 -23
  106. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/sonner.tsx +0 -23
  107. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/switch.tsx +0 -27
  108. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/table.tsx +0 -94
  109. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/tabs.tsx +0 -53
  110. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/textarea.tsx +0 -21
  111. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/toggle-group.tsx +0 -57
  112. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/toggle.tsx +0 -42
  113. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/tooltip.tsx +0 -32
  114. package/dist/templates/tanstack-start-2026-06-18/src/db/index.ts +0 -9
  115. package/dist/templates/tanstack-start-2026-06-18/src/db/schema.ts +0 -18
  116. package/dist/templates/tanstack-start-2026-06-18/src/global.d.ts +0 -3
  117. package/dist/templates/tanstack-start-2026-06-18/src/hooks/use-mobile.tsx +0 -19
  118. package/dist/templates/tanstack-start-2026-06-18/src/lib/proxy.ts +0 -119
  119. package/dist/templates/tanstack-start-2026-06-18/src/lib/utils.ts +0 -6
  120. package/dist/templates/tanstack-start-2026-06-18/src/routeTree.gen.ts +0 -86
  121. package/dist/templates/tanstack-start-2026-06-18/src/router.tsx +0 -37
  122. package/dist/templates/tanstack-start-2026-06-18/src/routes/README.md +0 -94
  123. package/dist/templates/tanstack-start-2026-06-18/src/routes/__root.tsx +0 -65
  124. package/dist/templates/tanstack-start-2026-06-18/src/routes/api/health.ts +0 -81
  125. package/dist/templates/tanstack-start-2026-06-18/src/routes/index.tsx +0 -33
  126. package/dist/templates/tanstack-start-2026-06-18/src/server.ts +0 -32
  127. package/dist/templates/tanstack-start-2026-06-18/src/styles.css +0 -214
  128. package/dist/templates/tanstack-start-2026-06-18/tsconfig.json +0 -27
  129. package/dist/templates/tanstack-start-2026-06-18/vite.config.ts +0 -17
  130. package/src/index.ts +0 -8
  131. package/src/template-render.ts +0 -53
  132. package/src/templates/tanstack-router-2026-06-15/components.json +0 -22
  133. package/src/templates/tanstack-router-2026-06-15/index.html +0 -20
  134. package/src/templates/tanstack-router-2026-06-15/package.json +0 -90
  135. package/src/templates/tanstack-router-2026-06-15/src/components/error-component.tsx +0 -50
  136. package/src/templates/tanstack-router-2026-06-15/src/components/ui/accordion.tsx +0 -51
  137. package/src/templates/tanstack-router-2026-06-15/src/components/ui/alert-dialog.tsx +0 -115
  138. package/src/templates/tanstack-router-2026-06-15/src/components/ui/alert.tsx +0 -49
  139. package/src/templates/tanstack-router-2026-06-15/src/components/ui/aspect-ratio.tsx +0 -5
  140. package/src/templates/tanstack-router-2026-06-15/src/components/ui/avatar.tsx +0 -47
  141. package/src/templates/tanstack-router-2026-06-15/src/components/ui/badge.tsx +0 -32
  142. package/src/templates/tanstack-router-2026-06-15/src/components/ui/breadcrumb.tsx +0 -101
  143. package/src/templates/tanstack-router-2026-06-15/src/components/ui/button.tsx +0 -49
  144. package/src/templates/tanstack-router-2026-06-15/src/components/ui/calendar.tsx +0 -177
  145. package/src/templates/tanstack-router-2026-06-15/src/components/ui/card.tsx +0 -55
  146. package/src/templates/tanstack-router-2026-06-15/src/components/ui/carousel.tsx +0 -240
  147. package/src/templates/tanstack-router-2026-06-15/src/components/ui/chart.tsx +0 -331
  148. package/src/templates/tanstack-router-2026-06-15/src/components/ui/checkbox.tsx +0 -26
  149. package/src/templates/tanstack-router-2026-06-15/src/components/ui/collapsible.tsx +0 -11
  150. package/src/templates/tanstack-router-2026-06-15/src/components/ui/command.tsx +0 -143
  151. package/src/templates/tanstack-router-2026-06-15/src/components/ui/context-menu.tsx +0 -187
  152. package/src/templates/tanstack-router-2026-06-15/src/components/ui/dialog.tsx +0 -104
  153. package/src/templates/tanstack-router-2026-06-15/src/components/ui/drawer.tsx +0 -98
  154. package/src/templates/tanstack-router-2026-06-15/src/components/ui/dropdown-menu.tsx +0 -188
  155. package/src/templates/tanstack-router-2026-06-15/src/components/ui/form.tsx +0 -171
  156. package/src/templates/tanstack-router-2026-06-15/src/components/ui/hover-card.tsx +0 -27
  157. package/src/templates/tanstack-router-2026-06-15/src/components/ui/input-otp.tsx +0 -69
  158. package/src/templates/tanstack-router-2026-06-15/src/components/ui/input.tsx +0 -22
  159. package/src/templates/tanstack-router-2026-06-15/src/components/ui/label.tsx +0 -21
  160. package/src/templates/tanstack-router-2026-06-15/src/components/ui/menubar.tsx +0 -229
  161. package/src/templates/tanstack-router-2026-06-15/src/components/ui/navigation-menu.tsx +0 -120
  162. package/src/templates/tanstack-router-2026-06-15/src/components/ui/pagination.tsx +0 -98
  163. package/src/templates/tanstack-router-2026-06-15/src/components/ui/popover.tsx +0 -31
  164. package/src/templates/tanstack-router-2026-06-15/src/components/ui/progress.tsx +0 -25
  165. package/src/templates/tanstack-router-2026-06-15/src/components/ui/radio-group.tsx +0 -36
  166. package/src/templates/tanstack-router-2026-06-15/src/components/ui/resizable.tsx +0 -37
  167. package/src/templates/tanstack-router-2026-06-15/src/components/ui/scroll-area.tsx +0 -44
  168. package/src/templates/tanstack-router-2026-06-15/src/components/ui/select.tsx +0 -152
  169. package/src/templates/tanstack-router-2026-06-15/src/components/ui/separator.tsx +0 -24
  170. package/src/templates/tanstack-router-2026-06-15/src/components/ui/sheet.tsx +0 -122
  171. package/src/templates/tanstack-router-2026-06-15/src/components/ui/sidebar.tsx +0 -744
  172. package/src/templates/tanstack-router-2026-06-15/src/components/ui/skeleton.tsx +0 -7
  173. package/src/templates/tanstack-router-2026-06-15/src/components/ui/slider.tsx +0 -23
  174. package/src/templates/tanstack-router-2026-06-15/src/components/ui/sonner.tsx +0 -23
  175. package/src/templates/tanstack-router-2026-06-15/src/components/ui/switch.tsx +0 -27
  176. package/src/templates/tanstack-router-2026-06-15/src/components/ui/table.tsx +0 -94
  177. package/src/templates/tanstack-router-2026-06-15/src/components/ui/tabs.tsx +0 -53
  178. package/src/templates/tanstack-router-2026-06-15/src/components/ui/textarea.tsx +0 -21
  179. package/src/templates/tanstack-router-2026-06-15/src/components/ui/toggle-group.tsx +0 -57
  180. package/src/templates/tanstack-router-2026-06-15/src/components/ui/toggle.tsx +0 -42
  181. package/src/templates/tanstack-router-2026-06-15/src/components/ui/tooltip.tsx +0 -32
  182. package/src/templates/tanstack-router-2026-06-15/src/hooks/use-mobile.tsx +0 -19
  183. package/src/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +0 -123
  184. package/src/templates/tanstack-router-2026-06-15/src/lib/utils.ts +0 -6
  185. package/src/templates/tanstack-router-2026-06-15/src/main.tsx +0 -37
  186. package/src/templates/tanstack-router-2026-06-15/src/routeTree.gen.ts +0 -59
  187. package/src/templates/tanstack-router-2026-06-15/src/routes/README.md +0 -85
  188. package/src/templates/tanstack-router-2026-06-15/src/routes/__root.tsx +0 -18
  189. package/src/templates/tanstack-router-2026-06-15/src/routes/index.tsx +0 -18
  190. package/src/templates/tanstack-router-2026-06-15/src/styles.css +0 -214
  191. package/src/templates/tanstack-router-2026-06-15/tsconfig.json +0 -27
  192. package/src/templates/tanstack-router-2026-06-15/vite.config.ts +0 -23
  193. package/src/templates/tanstack-start-2026-06-18/components.json +0 -22
  194. package/src/templates/tanstack-start-2026-06-18/drizzle.config.ts +0 -15
  195. package/src/templates/tanstack-start-2026-06-18/package.json +0 -104
  196. package/src/templates/tanstack-start-2026-06-18/src/components/error-component.tsx +0 -50
  197. package/src/templates/tanstack-start-2026-06-18/src/components/ui/accordion.tsx +0 -51
  198. package/src/templates/tanstack-start-2026-06-18/src/components/ui/alert-dialog.tsx +0 -115
  199. package/src/templates/tanstack-start-2026-06-18/src/components/ui/alert.tsx +0 -49
  200. package/src/templates/tanstack-start-2026-06-18/src/components/ui/aspect-ratio.tsx +0 -5
  201. package/src/templates/tanstack-start-2026-06-18/src/components/ui/avatar.tsx +0 -47
  202. package/src/templates/tanstack-start-2026-06-18/src/components/ui/badge.tsx +0 -32
  203. package/src/templates/tanstack-start-2026-06-18/src/components/ui/breadcrumb.tsx +0 -101
  204. package/src/templates/tanstack-start-2026-06-18/src/components/ui/button.tsx +0 -49
  205. package/src/templates/tanstack-start-2026-06-18/src/components/ui/calendar.tsx +0 -177
  206. package/src/templates/tanstack-start-2026-06-18/src/components/ui/card.tsx +0 -55
  207. package/src/templates/tanstack-start-2026-06-18/src/components/ui/carousel.tsx +0 -240
  208. package/src/templates/tanstack-start-2026-06-18/src/components/ui/chart.tsx +0 -371
  209. package/src/templates/tanstack-start-2026-06-18/src/components/ui/checkbox.tsx +0 -26
  210. package/src/templates/tanstack-start-2026-06-18/src/components/ui/collapsible.tsx +0 -11
  211. package/src/templates/tanstack-start-2026-06-18/src/components/ui/command.tsx +0 -143
  212. package/src/templates/tanstack-start-2026-06-18/src/components/ui/context-menu.tsx +0 -187
  213. package/src/templates/tanstack-start-2026-06-18/src/components/ui/dialog.tsx +0 -104
  214. package/src/templates/tanstack-start-2026-06-18/src/components/ui/drawer.tsx +0 -98
  215. package/src/templates/tanstack-start-2026-06-18/src/components/ui/dropdown-menu.tsx +0 -188
  216. package/src/templates/tanstack-start-2026-06-18/src/components/ui/form.tsx +0 -171
  217. package/src/templates/tanstack-start-2026-06-18/src/components/ui/hover-card.tsx +0 -27
  218. package/src/templates/tanstack-start-2026-06-18/src/components/ui/input-otp.tsx +0 -69
  219. package/src/templates/tanstack-start-2026-06-18/src/components/ui/input.tsx +0 -22
  220. package/src/templates/tanstack-start-2026-06-18/src/components/ui/label.tsx +0 -21
  221. package/src/templates/tanstack-start-2026-06-18/src/components/ui/menubar.tsx +0 -229
  222. package/src/templates/tanstack-start-2026-06-18/src/components/ui/navigation-menu.tsx +0 -120
  223. package/src/templates/tanstack-start-2026-06-18/src/components/ui/pagination.tsx +0 -98
  224. package/src/templates/tanstack-start-2026-06-18/src/components/ui/popover.tsx +0 -31
  225. package/src/templates/tanstack-start-2026-06-18/src/components/ui/progress.tsx +0 -25
  226. package/src/templates/tanstack-start-2026-06-18/src/components/ui/radio-group.tsx +0 -36
  227. package/src/templates/tanstack-start-2026-06-18/src/components/ui/resizable.tsx +0 -37
  228. package/src/templates/tanstack-start-2026-06-18/src/components/ui/scroll-area.tsx +0 -44
  229. package/src/templates/tanstack-start-2026-06-18/src/components/ui/select.tsx +0 -152
  230. package/src/templates/tanstack-start-2026-06-18/src/components/ui/separator.tsx +0 -24
  231. package/src/templates/tanstack-start-2026-06-18/src/components/ui/sheet.tsx +0 -122
  232. package/src/templates/tanstack-start-2026-06-18/src/components/ui/sidebar.tsx +0 -744
  233. package/src/templates/tanstack-start-2026-06-18/src/components/ui/skeleton.tsx +0 -7
  234. package/src/templates/tanstack-start-2026-06-18/src/components/ui/slider.tsx +0 -23
  235. package/src/templates/tanstack-start-2026-06-18/src/components/ui/sonner.tsx +0 -23
  236. package/src/templates/tanstack-start-2026-06-18/src/components/ui/switch.tsx +0 -27
  237. package/src/templates/tanstack-start-2026-06-18/src/components/ui/table.tsx +0 -94
  238. package/src/templates/tanstack-start-2026-06-18/src/components/ui/tabs.tsx +0 -53
  239. package/src/templates/tanstack-start-2026-06-18/src/components/ui/textarea.tsx +0 -21
  240. package/src/templates/tanstack-start-2026-06-18/src/components/ui/toggle-group.tsx +0 -57
  241. package/src/templates/tanstack-start-2026-06-18/src/components/ui/toggle.tsx +0 -42
  242. package/src/templates/tanstack-start-2026-06-18/src/components/ui/tooltip.tsx +0 -32
  243. package/src/templates/tanstack-start-2026-06-18/src/db/index.ts +0 -9
  244. package/src/templates/tanstack-start-2026-06-18/src/db/schema.ts +0 -18
  245. package/src/templates/tanstack-start-2026-06-18/src/global.d.ts +0 -3
  246. package/src/templates/tanstack-start-2026-06-18/src/hooks/use-mobile.tsx +0 -19
  247. package/src/templates/tanstack-start-2026-06-18/src/lib/proxy.ts +0 -119
  248. package/src/templates/tanstack-start-2026-06-18/src/lib/utils.ts +0 -6
  249. package/src/templates/tanstack-start-2026-06-18/src/routeTree.gen.ts +0 -86
  250. package/src/templates/tanstack-start-2026-06-18/src/router.tsx +0 -37
  251. package/src/templates/tanstack-start-2026-06-18/src/routes/README.md +0 -94
  252. package/src/templates/tanstack-start-2026-06-18/src/routes/__root.tsx +0 -65
  253. package/src/templates/tanstack-start-2026-06-18/src/routes/api/health.ts +0 -81
  254. package/src/templates/tanstack-start-2026-06-18/src/routes/index.tsx +0 -33
  255. package/src/templates/tanstack-start-2026-06-18/src/server.ts +0 -32
  256. package/src/templates/tanstack-start-2026-06-18/src/styles.css +0 -214
  257. package/src/templates/tanstack-start-2026-06-18/tsconfig.json +0 -27
  258. package/src/templates/tanstack-start-2026-06-18/vite.config.ts +0 -17
  259. package/src/templates/tanstack-start-2026-07-30/components.json +0 -22
  260. package/src/templates/tanstack-start-2026-07-30/drizzle/meta/_journal.json +0 -1
  261. package/src/templates/tanstack-start-2026-07-30/drizzle.config.ts +0 -18
  262. package/src/templates/tanstack-start-2026-07-30/package.json +0 -103
  263. package/src/templates/tanstack-start-2026-07-30/src/components/error-component.tsx +0 -50
  264. package/src/templates/tanstack-start-2026-07-30/src/components/ui/accordion.tsx +0 -51
  265. package/src/templates/tanstack-start-2026-07-30/src/components/ui/alert-dialog.tsx +0 -115
  266. package/src/templates/tanstack-start-2026-07-30/src/components/ui/alert.tsx +0 -49
  267. package/src/templates/tanstack-start-2026-07-30/src/components/ui/aspect-ratio.tsx +0 -5
  268. package/src/templates/tanstack-start-2026-07-30/src/components/ui/avatar.tsx +0 -47
  269. package/src/templates/tanstack-start-2026-07-30/src/components/ui/badge.tsx +0 -32
  270. package/src/templates/tanstack-start-2026-07-30/src/components/ui/breadcrumb.tsx +0 -101
  271. package/src/templates/tanstack-start-2026-07-30/src/components/ui/button.tsx +0 -49
  272. package/src/templates/tanstack-start-2026-07-30/src/components/ui/calendar.tsx +0 -177
  273. package/src/templates/tanstack-start-2026-07-30/src/components/ui/card.tsx +0 -55
  274. package/src/templates/tanstack-start-2026-07-30/src/components/ui/carousel.tsx +0 -240
  275. package/src/templates/tanstack-start-2026-07-30/src/components/ui/chart.tsx +0 -371
  276. package/src/templates/tanstack-start-2026-07-30/src/components/ui/checkbox.tsx +0 -26
  277. package/src/templates/tanstack-start-2026-07-30/src/components/ui/collapsible.tsx +0 -11
  278. package/src/templates/tanstack-start-2026-07-30/src/components/ui/command.tsx +0 -143
  279. package/src/templates/tanstack-start-2026-07-30/src/components/ui/context-menu.tsx +0 -187
  280. package/src/templates/tanstack-start-2026-07-30/src/components/ui/dialog.tsx +0 -104
  281. package/src/templates/tanstack-start-2026-07-30/src/components/ui/drawer.tsx +0 -98
  282. package/src/templates/tanstack-start-2026-07-30/src/components/ui/dropdown-menu.tsx +0 -188
  283. package/src/templates/tanstack-start-2026-07-30/src/components/ui/form.tsx +0 -171
  284. package/src/templates/tanstack-start-2026-07-30/src/components/ui/hover-card.tsx +0 -27
  285. package/src/templates/tanstack-start-2026-07-30/src/components/ui/input-otp.tsx +0 -69
  286. package/src/templates/tanstack-start-2026-07-30/src/components/ui/input.tsx +0 -22
  287. package/src/templates/tanstack-start-2026-07-30/src/components/ui/label.tsx +0 -21
  288. package/src/templates/tanstack-start-2026-07-30/src/components/ui/menubar.tsx +0 -229
  289. package/src/templates/tanstack-start-2026-07-30/src/components/ui/navigation-menu.tsx +0 -120
  290. package/src/templates/tanstack-start-2026-07-30/src/components/ui/pagination.tsx +0 -98
  291. package/src/templates/tanstack-start-2026-07-30/src/components/ui/popover.tsx +0 -31
  292. package/src/templates/tanstack-start-2026-07-30/src/components/ui/progress.tsx +0 -25
  293. package/src/templates/tanstack-start-2026-07-30/src/components/ui/radio-group.tsx +0 -36
  294. package/src/templates/tanstack-start-2026-07-30/src/components/ui/resizable.tsx +0 -37
  295. package/src/templates/tanstack-start-2026-07-30/src/components/ui/scroll-area.tsx +0 -44
  296. package/src/templates/tanstack-start-2026-07-30/src/components/ui/select.tsx +0 -152
  297. package/src/templates/tanstack-start-2026-07-30/src/components/ui/separator.tsx +0 -24
  298. package/src/templates/tanstack-start-2026-07-30/src/components/ui/sheet.tsx +0 -122
  299. package/src/templates/tanstack-start-2026-07-30/src/components/ui/sidebar.tsx +0 -744
  300. package/src/templates/tanstack-start-2026-07-30/src/components/ui/skeleton.tsx +0 -7
  301. package/src/templates/tanstack-start-2026-07-30/src/components/ui/slider.tsx +0 -23
  302. package/src/templates/tanstack-start-2026-07-30/src/components/ui/sonner.tsx +0 -23
  303. package/src/templates/tanstack-start-2026-07-30/src/components/ui/switch.tsx +0 -27
  304. package/src/templates/tanstack-start-2026-07-30/src/components/ui/table.tsx +0 -94
  305. package/src/templates/tanstack-start-2026-07-30/src/components/ui/tabs.tsx +0 -53
  306. package/src/templates/tanstack-start-2026-07-30/src/components/ui/textarea.tsx +0 -21
  307. package/src/templates/tanstack-start-2026-07-30/src/components/ui/toggle-group.tsx +0 -57
  308. package/src/templates/tanstack-start-2026-07-30/src/components/ui/toggle.tsx +0 -42
  309. package/src/templates/tanstack-start-2026-07-30/src/components/ui/tooltip.tsx +0 -32
  310. package/src/templates/tanstack-start-2026-07-30/src/db/index.ts +0 -9
  311. package/src/templates/tanstack-start-2026-07-30/src/db/schema.ts +0 -34
  312. package/src/templates/tanstack-start-2026-07-30/src/global.d.ts +0 -3
  313. package/src/templates/tanstack-start-2026-07-30/src/hooks/use-mobile.tsx +0 -19
  314. package/src/templates/tanstack-start-2026-07-30/src/lib/kv.ts +0 -37
  315. package/src/templates/tanstack-start-2026-07-30/src/lib/proxy.ts +0 -111
  316. package/src/templates/tanstack-start-2026-07-30/src/lib/storage.ts +0 -45
  317. package/src/templates/tanstack-start-2026-07-30/src/lib/utils.ts +0 -6
  318. package/src/templates/tanstack-start-2026-07-30/src/routeTree.gen.ts +0 -86
  319. package/src/templates/tanstack-start-2026-07-30/src/router.tsx +0 -37
  320. package/src/templates/tanstack-start-2026-07-30/src/routes/README.md +0 -94
  321. package/src/templates/tanstack-start-2026-07-30/src/routes/__root.tsx +0 -65
  322. package/src/templates/tanstack-start-2026-07-30/src/routes/api/health.ts +0 -83
  323. package/src/templates/tanstack-start-2026-07-30/src/routes/index.tsx +0 -33
  324. package/src/templates/tanstack-start-2026-07-30/src/server.ts +0 -32
  325. package/src/templates/tanstack-start-2026-07-30/src/styles.css +0 -214
  326. package/src/templates/tanstack-start-2026-07-30/tsconfig.json +0 -27
  327. package/src/templates/tanstack-start-2026-07-30/vite.config.ts +0 -17
  328. package/src/tools/create-plan.ts +0 -61
  329. package/src/tools/inspect-http.ts +0 -276
  330. package/src/tools/list-plans.ts +0 -85
  331. package/src/tools/modify-plan.ts +0 -79
  332. package/src/tools/remove.ts +0 -53
  333. package/src/tools/scaffold.ts +0 -213
  334. package/src/tools/setup-authentication/auth-files.ts +0 -144
  335. package/src/tools/setup-authentication/constants.ts +0 -19
  336. package/src/tools/setup-authentication/db-schema.ts +0 -96
  337. package/src/tools/setup-authentication/index.ts +0 -148
  338. package/src/tools/setup-authentication/package-json.ts +0 -17
  339. package/src/tools/setup-authentication/pages.ts +0 -241
  340. package/src/tools/setup-netsuite.ts +0 -1404
@@ -1,1404 +0,0 @@
1
- import { mkdir, readFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import type { Plugin } from "@opencode-ai/plugin";
4
- import { tool } from "@opencode-ai/plugin";
5
-
6
- /**
7
- * NetSuite runtime OAuth (end-user LOGIN). Two variants, picked automatically
8
- * from the project's scaffold -- never from env:
9
- *
10
- * - tanstack-start (SSR): the app IS the backend. Emits server routes
11
- * (/auth/netsuite/* + /api/me), netsuite_flows + netsuite_sessions drizzle
12
- * tables, and a same-origin client. The redirect URI registered on
13
- * Integration Record #2 must be the APP's own /auth/netsuite/callback
14
- * (read it from GET /auth/netsuite/config).
15
- * - tanstack-router (SPA): client-only files; the OAuth backend is the
16
- * per-project HelloLeo Cloud Worker (VITE_WORKER_URL), sign-in runs in
17
- * a popup + postMessage so it works inside the HelloLeo preview iframe.
18
- * The wizard registers the Worker's fixed /auth/netsuite/callback as the
19
- * ONE redirect URI on Integration Record #2 (provision-first, see webapp
20
- * netsuiteRuntimeSetup).
21
- *
22
- * Both variants share the same component names (AuthLayout / LoginPage /
23
- * SignInWithNetSuite) and the same wiring step: wrap the root route's
24
- * <Outlet /> with <AuthLayout>. LoginPage is a deliberately bare placeholder
25
- * in both -- Leo designs it per app.
26
- *
27
- * This login uses NetSuite Integration Record #2 (scope: rest_webservices),
28
- * a SEPARATE record from the MCP one (scope: mcp) used only to BUILD the app.
29
- *
30
- * Called AFTER `scaffold`, never instead of it.
31
- */
32
-
33
- /**
34
- * Scaffold id recorded by the scaffold tool in .leo/project.json, or null if
35
- * the file is missing/unreadable (pre-project.json workspaces).
36
- */
37
- async function readScaffoldId(dir: string): Promise<string | null> {
38
- try {
39
- const meta = await Bun.file(path.join(dir, ".leo", "project.json")).json();
40
- return typeof meta?.scaffold === "string" ? meta.scaffold : null;
41
- } catch {
42
- return null;
43
- }
44
- }
45
-
46
- /**
47
- * Is this project a tanstack-start (SSR) app? Primary signal: the scaffold id
48
- * in .leo/project.json. Fallback for pre-project.json workspaces: only the
49
- * start scaffold depends on @tanstack/react-start -- the router SPA ships
50
- * @tanstack/react-router alone.
51
- */
52
- async function isStartScaffold(dir: string): Promise<boolean> {
53
- const scaffoldId = await readScaffoldId(dir);
54
- if (scaffoldId) return scaffoldId.startsWith("tanstack-start");
55
- try {
56
- const pkg = await Bun.file(path.join(dir, "package.json")).json();
57
- return Boolean(
58
- pkg?.dependencies?.["@tanstack/react-start"] ??
59
- pkg?.devDependencies?.["@tanstack/react-start"],
60
- );
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- // --- emitted: src/db/schema.ts (appended) --------------------------------
67
- function schemaAppend(): string {
68
- return `
69
- // --- NetSuite OAuth (added by setup_netsuite) ---
70
- // Short-lived PKCE + return-to state between /auth/netsuite/start and its
71
- // callback. Single-use: the callback deletes the row after reading it.
72
- export const netsuiteFlows = sqliteTable('netsuite_flows', {
73
- flowId: text('flow_id').primaryKey(),
74
- codeVerifier: text('code_verifier').notNull(),
75
- returnTo: text('return_to').notNull(),
76
- createdAt: integer('created_at')
77
- .notNull()
78
- .default(sql\`(unixepoch())\`),
79
- })
80
-
81
- // Opaque session id -> NetSuite tokens. Tokens stay server-side; the browser
82
- // only ever holds the sid (sent as a bearer).
83
- export const netsuiteSessions = sqliteTable('netsuite_sessions', {
84
- sid: text('sid').primaryKey(),
85
- accountId: text('account_id').notNull(),
86
- accessToken: text('access_token').notNull(),
87
- refreshToken: text('refresh_token'),
88
- expiresAt: integer('expires_at'),
89
- userId: text('user_id'),
90
- email: text('email'),
91
- createdAt: integer('created_at')
92
- .notNull()
93
- .default(sql\`(unixepoch())\`),
94
- })
95
- `;
96
- }
97
-
98
- // --- emitted: src/lib/netsuite-server.ts ---------------------------------
99
- function netsuiteServerTs(): string {
100
- return `/*
101
- * NetSuite runtime OAuth -- server side (TanStack Start server routes).
102
- *
103
- * Runs wherever the app runs: \`vite dev\` (workerd) in the Fly preview machine
104
- * in dev, and the published Cloudflare Worker in prod. There is no separate
105
- * backend Worker -- these helpers back the /auth/netsuite/* + /api/me routes.
106
- *
107
- * Config is read from the Cloudflare env binding via @helloleo/runtime.
108
- * Set NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET (secret optional for a
109
- * PKCE public client) and optional PUBLIC_APP_URL as HelloLeo Cloud secrets
110
- * (prod) or dev vars (preview). NETSUITE_BROKER_CALLBACK (written by the
111
- * HelloLeo wizard) switches the redirect URI to the shared OAuth broker so
112
- * one URI serves every app/environment. They never reach the browser.
113
- */
114
- import { eq } from 'drizzle-orm'
115
- import { env as runtimeEnv } from '@helloleo/runtime'
116
- import { db } from '#/db'
117
- import { netsuiteSessions } from '#/db/schema'
118
-
119
- interface NsEnv {
120
- NETSUITE_ACCOUNT_ID?: string
121
- NETSUITE_CLIENT_ID?: string
122
- NETSUITE_CLIENT_SECRET?: string
123
- NETSUITE_BROKER_CALLBACK?: string
124
- PUBLIC_APP_URL?: string
125
- HELLOLEO_URL?: string
126
- }
127
- const nsEnv = runtimeEnv as unknown as NsEnv
128
-
129
- // Runtime record scope. The app signs end-users in for REST/SuiteQL.
130
- export const SCOPE = 'rest_webservices'
131
- const REFRESH_LEEWAY_SECONDS = 60
132
-
133
- export function requireConfig() {
134
- const accountId = nsEnv.NETSUITE_ACCOUNT_ID
135
- const clientId = nsEnv.NETSUITE_CLIENT_ID
136
- if (!accountId || !clientId) {
137
- throw new Error(
138
- 'NetSuite not configured: set NETSUITE_ACCOUNT_ID and NETSUITE_CLIENT_ID (and NETSUITE_CLIENT_SECRET for a confidential client) as HelloLeo Cloud secrets / dev vars.',
139
- )
140
- }
141
- return { accountId, clientId, clientSecret: nsEnv.NETSUITE_CLIENT_SECRET || '' }
142
- }
143
-
144
- // Account id -> DNS host form: lowercase, underscores to hyphens.
145
- function host(accountId: string) {
146
- return accountId.trim().toLowerCase().replace(/_/g, '-')
147
- }
148
- export function authorizeUrl(accountId: string) {
149
- return \`https://\${host(accountId)}.app.netsuite.com/app/login/oauth2/authorize.nl\`
150
- }
151
- export function tokenUrl(accountId: string) {
152
- return \`https://\${host(accountId)}.suitetalk.api.netsuite.com/services/rest/auth/oauth2/v1/token\`
153
- }
154
- export function restBase(accountId: string) {
155
- return \`https://\${host(accountId)}.suitetalk.api.netsuite.com\`
156
- }
157
-
158
- function stripTrailingSlash(s: string) {
159
- return s.endsWith('/') ? s.slice(0, -1) : s
160
- }
161
-
162
- // Public origin of THIS app, used to build the redirect URI. Prefer an explicit
163
- // PUBLIC_APP_URL; else derive from the request (honouring the preview proxy's
164
- // forwarded headers). Must match a redirect URI registered on the NetSuite
165
- // Integration Record exactly.
166
- export function appOrigin(request: Request) {
167
- // 1) Explicit override wins (set by the platform per environment).
168
- if (nsEnv.PUBLIC_APP_URL) return stripTrailingSlash(nsEnv.PUBLIC_APP_URL)
169
- // 2) HELLOLEO_URL is injected on every deploy (the app's canonical origin,
170
- // a bare host with no scheme), so it's set even when the NetSuite
171
- // wizard's PUBLIC_APP_URL pin wasn't.
172
- if (nsEnv.HELLOLEO_URL) return \`https://\${stripTrailingSlash(nsEnv.HELLOLEO_URL)}\`
173
- // 3) Preview/prod proxies rewrite Host, so trust the forwarded/original host
174
- // they set (HelloLeo's nginx sends X-Original-Host) — that's the public URL
175
- // the browser loaded, not an internal upstream host.
176
- const url = new URL(request.url)
177
- const proto = request.headers.get('x-forwarded-proto') || url.protocol.replace(':', '')
178
- const hostHeader =
179
- request.headers.get('x-forwarded-host') ||
180
- request.headers.get('x-original-host') ||
181
- url.host
182
- return \`\${proto}://\${hostHeader}\`
183
- }
184
- /**
185
- * HelloLeo's shared OAuth broker (integration-proxy Worker). When
186
- * NETSUITE_BROKER_CALLBACK is set (the platform writes it to the project env),
187
- * customers register that ONE stable URI on their Integration Record instead
188
- * of one URI per app/environment: the authorize redirect goes to the broker,
189
- * the broker 302s the callback back to THIS app (looked up by the state
190
- * nonce), and the token exchange passes the same broker URI. Unset (self-host
191
- * / standalone), the app falls back to its own origin's callback.
192
- */
193
- export function brokerCallback(): string | null {
194
- const raw = nsEnv.NETSUITE_BROKER_CALLBACK
195
- if (!raw) return null
196
- try {
197
- const u = new URL(raw)
198
- return u.protocol === 'https:' ? u.toString() : null
199
- } catch {
200
- return null
201
- }
202
- }
203
-
204
- export function redirectUri(request: Request) {
205
- return brokerCallback() ?? \`\${appOrigin(request)}/auth/netsuite/callback\`
206
- }
207
-
208
- /**
209
- * Tell the broker where to send this flow's callback: nonce -> this app's
210
- * origin. MUST succeed before redirecting to NetSuite, otherwise the callback
211
- * dead-ends at the broker. No-op when the broker isn't configured.
212
- */
213
- export async function registerWithBroker(request: Request, flowId: string): Promise<void> {
214
- const callback = brokerCallback()
215
- if (!callback) return
216
- const registerUrl = new URL('/auth/netsuite/register', callback)
217
- const res = await fetch(registerUrl.toString(), {
218
- method: 'POST',
219
- headers: { 'Content-Type': 'application/json' },
220
- body: JSON.stringify({ nonce: flowId, target: appOrigin(request) }),
221
- })
222
- if (!res.ok) {
223
- throw new Error(\`NetSuite broker registration failed (HTTP \${res.status}) -- cannot start sign-in\`)
224
- }
225
- }
226
-
227
- // Coerce ?return_to= to our OWN origin. The callback appends the session bearer
228
- // as a #nssid=... fragment; an off-origin return_to would leak that live
229
- // credential to an attacker-controlled page (open-redirect -> token theft).
230
- export function safeReturnTo(request: Request, raw: string | null): string {
231
- const origin = appOrigin(request)
232
- if (!raw) return origin
233
- try {
234
- const u = new URL(raw, origin)
235
- return u.origin === origin ? u.toString() : origin
236
- } catch {
237
- return origin
238
- }
239
- }
240
-
241
- // --- PKCE (Web Crypto: works on workerd and Node) ---
242
- function b64url(bytes: Uint8Array) {
243
- let s = ''
244
- for (const b of bytes) s += String.fromCharCode(b)
245
- return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')
246
- }
247
- export async function pkce() {
248
- const verifier = b64url(crypto.getRandomValues(new Uint8Array(48)))
249
- const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
250
- const challenge = b64url(new Uint8Array(digest))
251
- return { verifier, challenge }
252
- }
253
-
254
- interface TokenResponse {
255
- access_token?: string
256
- refresh_token?: string
257
- expires_in?: number
258
- error?: string
259
- error_description?: string
260
- }
261
-
262
- // Confidential client -> HTTP Basic. Public (PKCE-only) -> client_id in body.
263
- function authHeaders(clientId: string, clientSecret: string, params: Record<string, string>) {
264
- const headers: Record<string, string> = { 'Content-Type': 'application/x-www-form-urlencoded' }
265
- if (clientSecret) headers.Authorization = 'Basic ' + btoa(\`\${clientId}:\${clientSecret}\`)
266
- else params.client_id = clientId
267
- return headers
268
- }
269
-
270
- // POST the token endpoint and parse tolerantly. NetSuite returns JSON for both
271
- // success and OAuth errors, but a gateway (502/504) can return HTML -- never let
272
- // res.json() throw into the route handler (that would surface as an opaque 500
273
- // instead of a clean "token exchange failed").
274
- async function postToken(
275
- accountId: string,
276
- params: Record<string, string>,
277
- headers: Record<string, string>,
278
- ): Promise<TokenResponse> {
279
- let res: Response
280
- try {
281
- res = await fetch(tokenUrl(accountId), { method: 'POST', headers, body: new URLSearchParams(params) })
282
- } catch (e) {
283
- return { error: 'network_error', error_description: String(e) }
284
- }
285
- const text = await res.text()
286
- let data: TokenResponse = {}
287
- try {
288
- data = JSON.parse(text) as TokenResponse
289
- } catch {
290
- // non-JSON body (e.g. gateway HTML) -- leave data empty, fall through
291
- }
292
- if (!res.ok && !data.error) {
293
- data = { error: 'token_request_failed', error_description: 'HTTP ' + res.status }
294
- }
295
- return data
296
- }
297
-
298
- export async function exchangeCode(request: Request, code: string, verifier: string) {
299
- const { accountId, clientId, clientSecret } = requireConfig()
300
- const params: Record<string, string> = {
301
- grant_type: 'authorization_code',
302
- code,
303
- redirect_uri: redirectUri(request),
304
- code_verifier: verifier,
305
- }
306
- return postToken(accountId, params, authHeaders(clientId, clientSecret, params))
307
- }
308
-
309
- async function refreshToken(refresh: string) {
310
- const { accountId, clientId, clientSecret } = requireConfig()
311
- const params: Record<string, string> = { grant_type: 'refresh_token', refresh_token: refresh }
312
- return postToken(accountId, params, authHeaders(clientId, clientSecret, params))
313
- }
314
-
315
- export interface NsSession {
316
- sid: string
317
- accountId: string
318
- accessToken: string
319
- refreshToken: string | null
320
- expiresAt: number | null
321
- userId: string | null
322
- email: string | null
323
- }
324
-
325
- // Load a session by sid, transparently refreshing the access token when it's
326
- // within the leeway of expiry. Returns null if unknown or unrefreshable.
327
- export async function getSession(sid: string): Promise<NsSession | null> {
328
- if (!sid) return null
329
- const rows = await db.select().from(netsuiteSessions).where(eq(netsuiteSessions.sid, sid)).limit(1)
330
- const s = rows[0]
331
- if (!s) return null
332
- const now = Math.floor(Date.now() / 1000)
333
- // A missing expiry is treated as EXPIRED (force a refresh) rather than trusting
334
- // the token forever -- NetSuite normally sends expires_in, so this is defensive.
335
- const expired = s.expiresAt == null ? true : now + REFRESH_LEEWAY_SECONDS >= s.expiresAt
336
- if (expired) {
337
- // Token expired (or about to). It MUST refresh; if it can't, the session is
338
- // dead — delete it and return null so /api/me reports signed-out and the user
339
- // re-auths, rather than a "logged in" app whose every NetSuite call 401s.
340
- if (!s.refreshToken) { await deleteSession(sid); return null }
341
- const tok = await refreshToken(s.refreshToken)
342
- if (!tok.access_token) { await deleteSession(sid); return null }
343
- const expiresAt = now + (tok.expires_in || 3600)
344
- const refreshTok = tok.refresh_token ?? s.refreshToken
345
- await db
346
- .update(netsuiteSessions)
347
- .set({ accessToken: tok.access_token, refreshToken: refreshTok, expiresAt })
348
- .where(eq(netsuiteSessions.sid, sid))
349
- return { ...s, accessToken: tok.access_token, refreshToken: refreshTok, expiresAt }
350
- }
351
- return s
352
- }
353
-
354
- export async function deleteSession(sid: string) {
355
- if (sid) await db.delete(netsuiteSessions).where(eq(netsuiteSessions.sid, sid))
356
- }
357
-
358
- export function bearer(request: Request) {
359
- const h = request.headers.get('authorization') || ''
360
- return h.startsWith('Bearer ') ? h.slice(7) : ''
361
- }
362
-
363
- /**
364
- * Call NetSuite REST / SuiteQL for the signed-in session. Use this from your
365
- * data routes (src/routes/api/*). \`path\` is relative to the account's
366
- * suitetalk host, e.g. '/services/rest/query/v1/suiteql'. Returns the raw
367
- * fetch Response (401 if the sid is unknown). Access token is auto-refreshed.
368
- */
369
- export async function netsuiteFetch(sid: string, path: string, init: RequestInit = {}): Promise<Response> {
370
- const session = await getSession(sid)
371
- if (!session) return new Response(JSON.stringify({ error: 'unauthorized' }), { status: 401 })
372
- const url = path.startsWith('http') ? path : \`\${restBase(session.accountId)}\${path}\`
373
- const headers = new Headers(init.headers)
374
- headers.set('Authorization', \`Bearer \${session.accessToken}\`)
375
- if (init.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json')
376
- return fetch(url, { ...init, headers })
377
- }
378
- `;
379
- }
380
-
381
- // --- emitted: src/routes/auth/netsuite/start.ts --------------------------
382
- function startRouteTs(): string {
383
- return `import { createFileRoute } from '@tanstack/react-router'
384
- import { db } from '#/db'
385
- import { netsuiteFlows } from '#/db/schema'
386
- import {
387
- authorizeUrl,
388
- pkce,
389
- redirectUri,
390
- registerWithBroker,
391
- requireConfig,
392
- safeReturnTo,
393
- SCOPE,
394
- } from '#/lib/netsuite-server'
395
-
396
- // GET /auth/netsuite/start -- begin the NetSuite OAuth (PKCE) flow.
397
- export const Route = createFileRoute('/auth/netsuite/start')({
398
- server: {
399
- handlers: {
400
- GET: async ({ request }) => {
401
- const { accountId, clientId } = requireConfig()
402
- const url = new URL(request.url)
403
- // Same-origin only: the callback returns the bearer in a URL fragment.
404
- const returnTo = safeReturnTo(request, url.searchParams.get('return_to'))
405
-
406
- const { verifier, challenge } = await pkce()
407
- const flowId = crypto.randomUUID()
408
- // With the shared broker, NetSuite redirects to the broker's fixed
409
- // callback and the broker forwards here -- it must know this flow's
410
- // target before the user leaves for NetSuite.
411
- await registerWithBroker(request, flowId)
412
- await db.insert(netsuiteFlows).values({ flowId, codeVerifier: verifier, returnTo })
413
-
414
- const authorize = new URL(authorizeUrl(accountId))
415
- authorize.searchParams.set('response_type', 'code')
416
- authorize.searchParams.set('client_id', clientId)
417
- authorize.searchParams.set('redirect_uri', redirectUri(request))
418
- authorize.searchParams.set('scope', SCOPE)
419
- authorize.searchParams.set('state', flowId)
420
- authorize.searchParams.set('code_challenge', challenge)
421
- authorize.searchParams.set('code_challenge_method', 'S256')
422
-
423
- return new Response(null, { status: 302, headers: { Location: authorize.toString() } })
424
- },
425
- },
426
- },
427
- })
428
- `;
429
- }
430
-
431
- // --- emitted: src/routes/auth/netsuite/callback.ts -----------------------
432
- function callbackRouteTs(): string {
433
- return `import { createFileRoute } from '@tanstack/react-router'
434
- import { eq } from 'drizzle-orm'
435
- import { db } from '#/db'
436
- import { netsuiteFlows, netsuiteSessions } from '#/db/schema'
437
- import { exchangeCode, requireConfig } from '#/lib/netsuite-server'
438
-
439
- // GET /auth/netsuite/callback -- exchange the code, mint a session, hand the
440
- // bearer back to the SPA via the URL fragment (#nssid=...). The fragment
441
- // survives the HelloLeo preview iframe, where third-party cookies are dropped.
442
- export const Route = createFileRoute('/auth/netsuite/callback')({
443
- server: {
444
- handlers: {
445
- GET: async ({ request }) => {
446
- const url = new URL(request.url)
447
- const err = url.searchParams.get('error')
448
- if (err) return new Response(\`NetSuite authorization failed: \${err}\`, { status: 400 })
449
-
450
- const code = url.searchParams.get('code')
451
- const state = url.searchParams.get('state')
452
- if (!code || !state) return new Response('Missing code or state', { status: 400 })
453
-
454
- const rows = await db
455
- .select()
456
- .from(netsuiteFlows)
457
- .where(eq(netsuiteFlows.flowId, state))
458
- .limit(1)
459
- const flow = rows[0]
460
- if (!flow) return new Response('Unknown or expired flow -- start again', { status: 400 })
461
- // Single-use: burn the flow row immediately.
462
- await db.delete(netsuiteFlows).where(eq(netsuiteFlows.flowId, state))
463
- // Reject stale flows: the PKCE verifier must not stay valid indefinitely
464
- // for an abandoned or leaked authorize URL (createdAt is unixepoch seconds).
465
- const nowSec = Math.floor(Date.now() / 1000)
466
- if (flow.createdAt && nowSec - flow.createdAt > 600) {
467
- return new Response('Flow expired -- start again', { status: 400 })
468
- }
469
-
470
- const tok = await exchangeCode(request, code, flow.codeVerifier)
471
- if (!tok.access_token) {
472
- return new Response(
473
- \`Token exchange failed: \${tok.error_description || tok.error || 'unknown error'}\`,
474
- { status: 400 },
475
- )
476
- }
477
-
478
- const sid = crypto.randomUUID()
479
- const now = Math.floor(Date.now() / 1000)
480
- await db.insert(netsuiteSessions).values({
481
- sid,
482
- accountId: requireConfig().accountId,
483
- accessToken: tok.access_token,
484
- refreshToken: tok.refresh_token ?? null,
485
- expiresAt: now + (tok.expires_in || 3600),
486
- })
487
-
488
- const dest = new URL(flow.returnTo)
489
- dest.hash = \`nssid=\${sid}\`
490
- return new Response(null, { status: 302, headers: { Location: dest.toString() } })
491
- },
492
- },
493
- },
494
- })
495
- `;
496
- }
497
-
498
- // --- emitted: src/routes/auth/netsuite/logout.ts -------------------------
499
- function logoutRouteTs(): string {
500
- return `import { createFileRoute } from '@tanstack/react-router'
501
- import { bearer, deleteSession } from '#/lib/netsuite-server'
502
-
503
- // POST /auth/netsuite/logout -- destroy the current session.
504
- export const Route = createFileRoute('/auth/netsuite/logout')({
505
- server: {
506
- handlers: {
507
- POST: async ({ request }) => {
508
- await deleteSession(bearer(request))
509
- return Response.json({ ok: true })
510
- },
511
- },
512
- },
513
- })
514
- `;
515
- }
516
-
517
- // --- emitted: src/routes/api/me.ts ---------------------------------------
518
- function meRouteTs(): string {
519
- return `import { createFileRoute } from '@tanstack/react-router'
520
- import { bearer, getSession } from '#/lib/netsuite-server'
521
-
522
- // GET /api/me -- resolve the bearer sid to a session. The ONLY authoritative
523
- // auth check; the client must never trust sessionStorage on its own.
524
- export const Route = createFileRoute('/api/me')({
525
- server: {
526
- handlers: {
527
- GET: async ({ request }) => {
528
- const session = await getSession(bearer(request))
529
- if (!session) return Response.json({ authenticated: false })
530
- return Response.json({
531
- authenticated: true,
532
- account_id: session.accountId,
533
- })
534
- },
535
- },
536
- },
537
- })
538
- `;
539
- }
540
-
541
- // --- emitted: src/routes/auth/netsuite/config.ts -------------------------
542
- function configRouteTs(): string {
543
- return `import { createFileRoute } from '@tanstack/react-router'
544
- import { env as runtimeEnv } from '@helloleo/runtime'
545
- import { appOrigin, redirectUri } from '#/lib/netsuite-server'
546
-
547
- // GET /auth/netsuite/config -- single source of truth for the redirect URI.
548
- // Whatever this returns is EXACTLY what /auth/netsuite/start sends NetSuite, so
549
- // show the user THIS value to register on their Integration Record. Never
550
- // hardcode or guess a callback URL.
551
- export const Route = createFileRoute('/auth/netsuite/config')({
552
- server: {
553
- handlers: {
554
- GET: async ({ request }) => {
555
- const e = runtimeEnv as unknown as {
556
- NETSUITE_ACCOUNT_ID?: string
557
- NETSUITE_CLIENT_ID?: string
558
- }
559
- return Response.json({
560
- redirect_uri: redirectUri(request),
561
- origin: appOrigin(request),
562
- configured: Boolean(e.NETSUITE_ACCOUNT_ID && e.NETSUITE_CLIENT_ID),
563
- })
564
- },
565
- },
566
- },
567
- })
568
- `;
569
- }
570
-
571
- // --- emitted: src/lib/netsuite.ts (client) -------------------------------
572
- function netsuiteClientTs(): string {
573
- return `/*
574
- * NetSuite auth -- client (same-origin). The OAuth backend is THIS app's own
575
- * server routes (src/routes/auth/netsuite/* + src/routes/api/me.ts), so every
576
- * call is relative -- no separate Worker, no VITE_WORKER_URL.
577
- *
578
- * The browser only ever holds an opaque session id (sid), handed back in the
579
- * callback's URL fragment (#nssid=...) so it survives the HelloLeo preview
580
- * iframe (third-party cookies are partitioned/dropped there). The sid rides as
581
- * Authorization: Bearer on every call; NetSuite tokens stay server-side. The
582
- * only authoritative auth check is GET /api/me.
583
- *
584
- * NEVER use a password gate. NEVER trust sessionStorage as the auth state --
585
- * it's just a cache of the bearer.
586
- */
587
- const TOKEN_KEY = 'nssid'
588
-
589
- // Capture-and-strip #nssid=... as early as possible so URLs stay clean.
590
- ;(function bootstrapTokenFromHash() {
591
- if (typeof window === 'undefined') return
592
- const hash = window.location.hash.replace(/^#/, '')
593
- if (!hash) return
594
- const params = new URLSearchParams(hash)
595
- const sid = params.get(TOKEN_KEY)
596
- if (sid) {
597
- try {
598
- sessionStorage.setItem(TOKEN_KEY, sid)
599
- } catch {}
600
- params.delete(TOKEN_KEY)
601
- const rest = params.toString()
602
- window.history.replaceState(
603
- null,
604
- '',
605
- window.location.pathname + window.location.search + (rest ? '#' + rest : ''),
606
- )
607
- }
608
- })()
609
-
610
- export function getSessionToken(): string | null {
611
- try {
612
- return sessionStorage.getItem(TOKEN_KEY)
613
- } catch {
614
- return null
615
- }
616
- }
617
-
618
- export function signInWithNetSuite(returnTo: string = window.location.href) {
619
- const url = new URL('/auth/netsuite/start', window.location.origin)
620
- url.searchParams.set('return_to', returnTo)
621
- // The app renders inside the HelloLeo preview iframe; /start 302s to
622
- // NetSuite, whose OAuth page sends X-Frame-Options: DENY. Navigate the TOP
623
- // frame so the flow runs at top level and the callback lands there to read
624
- // #nssid=. Assign .href (NOT .assign(): reading .assign off a cross-origin
625
- // window.top.location throws, dropping us into the iframe fallback ->
626
- // "refused to connect").
627
- try {
628
- if (window.top && window.top !== window) {
629
- window.top.location.href = url.toString()
630
- return
631
- }
632
- } catch {
633
- /* top window unreachable -- fall through to same-frame navigation */
634
- }
635
- window.location.href = url.toString()
636
- }
637
-
638
- export interface NetSuiteSession {
639
- authenticated: boolean
640
- account_id?: string
641
- }
642
-
643
- export async function getNetSuiteSession(): Promise<NetSuiteSession> {
644
- const sid = getSessionToken()
645
- if (!sid) return { authenticated: false }
646
- const res = await fetch('/api/me', { headers: { Authorization: 'Bearer ' + sid } })
647
- if (!res.ok) return { authenticated: false }
648
- // res.json() is \`unknown\` under workers-types -- assert the /api/me shape.
649
- const data = (await res.json()) as NetSuiteSession
650
- return { ...data, authenticated: Boolean(data?.authenticated) }
651
- }
652
-
653
- export async function signOutNetSuite(): Promise<void> {
654
- const sid = getSessionToken()
655
- if (sid) {
656
- try {
657
- await fetch('/auth/netsuite/logout', {
658
- method: 'POST',
659
- headers: { Authorization: 'Bearer ' + sid },
660
- })
661
- } catch {}
662
- }
663
- try {
664
- sessionStorage.removeItem(TOKEN_KEY)
665
- } catch {}
666
- window.location.reload()
667
- }
668
-
669
- /**
670
- * Fetch YOUR data routes (src/routes/api/*) with the bearer attached. Use this
671
- * instead of bare fetch for authenticated calls.
672
- */
673
- export async function apiFetch(path: string, init: RequestInit = {}): Promise<Response> {
674
- const sid = getSessionToken()
675
- const headers = new Headers(init.headers)
676
- if (sid) headers.set('Authorization', 'Bearer ' + sid)
677
- return fetch(path, { ...init, headers })
678
- }
679
-
680
- /**
681
- * The exact redirect URI this app will send NetSuite, read from the server so
682
- * it can never drift from what /auth/netsuite/start actually uses. Show it to
683
- * whoever registers the Integration Record; never hardcode a callback URL.
684
- */
685
- export async function getNetSuiteRedirectUri(): Promise<string | null> {
686
- try {
687
- const res = await fetch('/auth/netsuite/config')
688
- if (!res.ok) return null
689
- const data = (await res.json()) as { redirect_uri?: string }
690
- return data?.redirect_uri ?? null
691
- } catch {
692
- return null
693
- }
694
- }
695
- `;
696
- }
697
-
698
- // --- emitted: src/lib/auth.tsx (client) ----------------------------------
699
- function startAuthTsx(): string {
700
- return `import * as React from 'react'
701
- import {
702
- getNetSuiteSession,
703
- signInWithNetSuite,
704
- signOutNetSuite,
705
- type NetSuiteSession,
706
- } from './netsuite'
707
-
708
- /**
709
- * NetSuite auth context. Holds the session (resolved client-side from /api/me
710
- * via getNetSuiteSession) and exposes sign-in / sign-out. /api/me is the only
711
- * authoritative check -- never trust sessionStorage directly.
712
- */
713
- export interface NetSuiteAuthContextValue {
714
- pending: boolean
715
- isAuthenticated: boolean
716
- session: NetSuiteSession | null
717
- signIn: () => void
718
- signOut: () => Promise<void>
719
- refresh: () => Promise<void>
720
- }
721
-
722
- const NetSuiteAuthContext = React.createContext<NetSuiteAuthContextValue | null>(null)
723
-
724
- export function NetSuiteAuthProvider({ children }: { children: React.ReactNode }) {
725
- const [session, setSession] = React.useState<NetSuiteSession | null>(null)
726
- // Start pending on BOTH server and client render so SSR and first paint match
727
- // (a spinner), avoiding a hydration mismatch. The effect (client-only)
728
- // resolves it.
729
- const [pending, setPending] = React.useState(true)
730
-
731
- const refresh = React.useCallback(async () => {
732
- setPending(true)
733
- try {
734
- setSession(await getNetSuiteSession())
735
- } finally {
736
- setPending(false)
737
- }
738
- }, [])
739
-
740
- React.useEffect(() => {
741
- refresh()
742
- }, [refresh])
743
-
744
- const value: NetSuiteAuthContextValue = {
745
- pending,
746
- isAuthenticated: Boolean(session?.authenticated),
747
- session,
748
- signIn: () => signInWithNetSuite(),
749
- signOut: signOutNetSuite,
750
- refresh,
751
- }
752
-
753
- return <NetSuiteAuthContext.Provider value={value}>{children}</NetSuiteAuthContext.Provider>
754
- }
755
-
756
- export function useNetSuiteAuth(): NetSuiteAuthContextValue {
757
- const ctx = React.useContext(NetSuiteAuthContext)
758
- if (!ctx) {
759
- throw new Error('useNetSuiteAuth must be used within <NetSuiteAuthProvider>')
760
- }
761
- return ctx
762
- }
763
- `;
764
- }
765
-
766
- // --- emitted: src/components/auth/AuthLayout.tsx --------------------------
767
- function startAuthLayoutTsx(): string {
768
- return `import type { ReactNode } from 'react'
769
- import { NetSuiteAuthProvider, useNetSuiteAuth } from '../../lib/auth'
770
- import { LoginPage } from './LoginPage'
771
-
772
- /**
773
- * NetSuite auth layout. The ONE manual wiring step: wrap the root route's
774
- * <Outlet /> with it in src/routes/__root.tsx:
775
- *
776
- * import { AuthLayout } from '../components/auth/AuthLayout'
777
- * ...
778
- * function RootComponent() {
779
- * return (
780
- * <RootDocument>
781
- * <AuthLayout>
782
- * <Outlet />
783
- * </AuthLayout>
784
- * </RootDocument>
785
- * )
786
- * }
787
- *
788
- * It provides auth context and gates rendering: a spinner while the session
789
- * resolves, <LoginPage /> for signed-out users, your routes once signed in.
790
- * NEVER swap this for a password gate -- real OAuth is the only login.
791
- */
792
- export function AuthLayout({ children }: { children: ReactNode }) {
793
- return (
794
- <NetSuiteAuthProvider>
795
- <Gate>{children}</Gate>
796
- </NetSuiteAuthProvider>
797
- )
798
- }
799
-
800
- function Gate({ children }: { children: ReactNode }) {
801
- const { pending, isAuthenticated } = useNetSuiteAuth()
802
-
803
- if (pending) {
804
- return (
805
- <div className="min-h-screen flex items-center justify-center bg-background">
806
- <div className="h-8 w-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
807
- </div>
808
- )
809
- }
810
-
811
- if (!isAuthenticated) return <LoginPage />
812
-
813
- return <>{children}</>
814
- }
815
- `;
816
- }
817
-
818
- // --- emitted: src/components/auth/LoginPage.tsx ---------------------------
819
- function startLoginPageTsx(): string {
820
- return `import { SignInWithNetSuite } from './SignInWithNetSuite'
821
-
822
- /**
823
- * Unauthenticated gate -- a deliberately bare placeholder. DESIGN THIS PAGE
824
- * to match the app you are building: its real name, its brand, its layout
825
- * language. Do NOT ship this default as-is.
826
- *
827
- * The only hard requirements:
828
- * - keep <SignInWithNetSuite /> as the ONLY call-to-action (it starts the
829
- * OAuth flow at /auth/netsuite/start)
830
- * - never add a password form -- real OAuth is the only login
831
- */
832
- export function LoginPage() {
833
- return (
834
- <main className="min-h-screen flex items-center justify-center bg-background text-foreground p-4">
835
- <div className="flex flex-col items-center gap-4 text-center">
836
- <h1 className="text-2xl font-semibold">Sign in</h1>
837
- <p className="text-sm text-muted-foreground">
838
- Connect your NetSuite account to continue.
839
- </p>
840
- <SignInWithNetSuite />
841
- </div>
842
- </main>
843
- )
844
- }
845
- `;
846
- }
847
-
848
- // --- emitted: src/components/auth/SignInWithNetSuite.tsx ------------------
849
- function startSignInButtonTsx(): string {
850
- return `import { signInWithNetSuite } from '../../lib/netsuite'
851
-
852
- export function SignInWithNetSuite() {
853
- return (
854
- <button
855
- onClick={() => signInWithNetSuite()}
856
- className="inline-flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
857
- >
858
- Sign in with NetSuite
859
- </button>
860
- )
861
- }
862
- `;
863
- }
864
-
865
- function spaNetsuiteTs(): string {
866
- return `/*
867
- * NetSuite OAuth client helper (bearer-token-in-fragment).
868
- *
869
- * Two NetSuite Integration Records back this:
870
- * 1. MCP (scope: mcp) -- used by HelloLeo at build time.
871
- * 2. Runtime (scope: rest_webservices) -- used here at sign-in time.
872
- * Runtime credentials live as HelloLeo Cloud secrets on the project's Worker
873
- * (env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET). They never reach
874
- * the browser. Cookies are NOT used -- frontend ↔ Worker are cross-site
875
- * (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
876
- * third-party cookies in the HelloLeo preview iframe.
877
- *
878
- * Flow:
879
- * 1. User clicks "Sign in with NetSuite".
880
- * 2. Browser navigates to the Worker /auth/netsuite/start.
881
- * 3. Worker redirects to NetSuite's OAuth UI with PKCE.
882
- * 4. NetSuite redirects back to the Worker /auth/netsuite/callback.
883
- * 5. Worker exchanges the code, mints a session id, then redirects to
884
- * <return_to>#nssid=<sid>. URL fragments stay in the browser only.
885
- * 6. This module reads the fragment on load, stashes the sid in
886
- * sessionStorage, and sends it as Authorization: Bearer on every
887
- * Worker call.
888
- *
889
- * NEVER use a hardcoded password gate. NEVER use sessionStorage as the
890
- * authoritative auth state -- the only acceptable check is /api/me on the
891
- * Worker. sessionStorage here is just a cache of the bearer token.
892
- */
893
-
894
- export const WORKER_URL = import.meta.env.VITE_WORKER_URL ?? '';
895
- const TOKEN_KEY = 'nssid';
896
-
897
- // Capture-and-strip the #nssid=... fragment as early as possible so URLs
898
- // shown to the user stay clean. Runs once at module load.
899
- (function bootstrapTokenFromHash() {
900
- if (typeof window === 'undefined') return;
901
- const hash = window.location.hash.replace(/^#/, '');
902
- if (!hash) return;
903
- const params = new URLSearchParams(hash);
904
- const sid = params.get(TOKEN_KEY);
905
- if (sid) {
906
- try { sessionStorage.setItem(TOKEN_KEY, sid); } catch {}
907
- params.delete(TOKEN_KEY);
908
- const remaining = params.toString();
909
- const newHash = remaining ? '#' + remaining : '';
910
- window.history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
911
- }
912
- })();
913
-
914
- export function getSessionToken(): string | null {
915
- try { return sessionStorage.getItem(TOKEN_KEY); } catch { return null; }
916
- }
917
-
918
- export function signInWithNetSuite(returnTo: string = window.location.href) {
919
- if (!WORKER_URL) {
920
- throw new Error(
921
- 'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
922
- );
923
- }
924
- const workerOrigin = new URL(WORKER_URL).origin;
925
- const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
926
- url.searchParams.set('return_to', returnTo);
927
- // popup=1 tells the Worker callback to postMessage the session id back to
928
- // THIS window (then close) instead of a #nssid= redirect.
929
- url.searchParams.set('popup', '1');
930
-
931
- // Open OAuth in a POPUP -- not the iframe, not the top frame. NetSuite's OAuth
932
- // page is X-Frame-Options: DENY (can't load in an iframe), and navigating the
933
- // TOP frame replaces the HelloLeo preview and lands the session in a DIFFERENT
934
- // browsing context -- so back in the preview iframe (its own partitioned
935
- // sessionStorage) you still see the login even after signing in. A popup keeps
936
- // the app + preview intact and hands the token back to this exact context.
937
- const popup = window.open(
938
- url.toString(),
939
- 'netsuite-oauth',
940
- 'width=600,height=760,menubar=no,toolbar=no',
941
- );
942
-
943
- if (!popup) {
944
- // Popup blocked -> fall back to top-frame navigation (works first-party / in
945
- // a standalone tab; the callback then uses the #nssid= redirect).
946
- try {
947
- if (window.top && window.top !== window) {
948
- window.top.location.href = url.toString();
949
- return;
950
- }
951
- } catch {
952
- /* top window not accessible */
953
- }
954
- window.location.href = url.toString();
955
- return;
956
- }
957
-
958
- const onMessage = (event: MessageEvent) => {
959
- if (event.origin !== workerOrigin) return;
960
- const data = event.data as { type?: string; nssid?: string } | null;
961
- if (!data || data.type !== 'netsuite-auth' || !data.nssid) return;
962
- try { sessionStorage.setItem(TOKEN_KEY, data.nssid); } catch {}
963
- window.removeEventListener('message', onMessage);
964
- try { popup.close(); } catch {}
965
- // Re-render as signed-in -- the auth provider re-checks /api/me on load.
966
- window.location.reload();
967
- };
968
- window.addEventListener('message', onMessage);
969
- }
970
-
971
- export interface NetSuiteSession {
972
- authenticated: boolean;
973
- account_id?: string;
974
- user_id?: string;
975
- email?: string;
976
- }
977
-
978
- export async function getNetSuiteSession(): Promise<NetSuiteSession> {
979
- if (!WORKER_URL) return { authenticated: false };
980
- const sid = getSessionToken();
981
- if (!sid) return { authenticated: false };
982
- const res = await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/api/me\`, {
983
- headers: { Authorization: 'Bearer ' + sid },
984
- });
985
- if (!res.ok) return { authenticated: false };
986
- const data = await res.json();
987
- return { authenticated: Boolean(data?.account_id), ...data };
988
- }
989
-
990
- export async function signOutNetSuite(): Promise<void> {
991
- if (!WORKER_URL) return;
992
- const sid = getSessionToken();
993
- if (sid) {
994
- try {
995
- await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/logout\`, {
996
- method: 'POST',
997
- headers: { Authorization: 'Bearer ' + sid },
998
- });
999
- } catch {}
1000
- }
1001
- try { sessionStorage.removeItem(TOKEN_KEY); } catch {}
1002
- window.location.reload();
1003
- }
1004
-
1005
- /**
1006
- * Helper for ALL other Worker calls (your dashboard endpoints, etc.).
1007
- * Always go through this instead of bare fetch -- it injects the bearer
1008
- * token and routes through the Worker so NetSuite tokens stay server-side.
1009
- */
1010
- export async function workerFetch(pathOrUrl: string, init: RequestInit = {}): Promise<Response> {
1011
- if (!WORKER_URL) throw new Error('VITE_WORKER_URL is not set');
1012
- const url = pathOrUrl.startsWith('http')
1013
- ? pathOrUrl
1014
- : \`\${WORKER_URL.replace(/\\/$/, '')}\${pathOrUrl.startsWith('/') ? '' : '/'}\${pathOrUrl}\`;
1015
- const sid = getSessionToken();
1016
- const headers = new Headers(init.headers ?? {});
1017
- if (sid) headers.set('Authorization', 'Bearer ' + sid);
1018
- return fetch(url, { ...init, headers });
1019
- }
1020
- `;
1021
- }
1022
-
1023
- function spaSignInButtonTsx(): string {
1024
- return `import { signInWithNetSuite } from '../../lib/netsuite';
1025
-
1026
- export function SignInWithNetSuite() {
1027
- return (
1028
- <button
1029
- onClick={() => signInWithNetSuite()}
1030
- className="inline-flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1031
- >
1032
- Sign in with NetSuite
1033
- </button>
1034
- );
1035
- }
1036
- `;
1037
- }
1038
-
1039
- function spaLoginPageTsx(): string {
1040
- return `import { SignInWithNetSuite } from './SignInWithNetSuite';
1041
-
1042
- /**
1043
- * Unauthenticated gate -- a deliberately bare placeholder. DESIGN THIS PAGE
1044
- * to match the app you are building: its real name, its brand, its layout
1045
- * language. Do NOT ship this default as-is.
1046
- *
1047
- * The only hard requirements:
1048
- * - keep <SignInWithNetSuite /> as the ONLY call-to-action (it starts the
1049
- * OAuth flow on the Worker's /auth/netsuite/start)
1050
- * - never add a password form -- real OAuth is the only login
1051
- */
1052
- export function LoginPage() {
1053
- return (
1054
- <main className="min-h-screen flex items-center justify-center bg-background text-foreground p-4">
1055
- <div className="flex flex-col items-center gap-4 text-center">
1056
- <h1 className="text-2xl font-semibold">Sign in</h1>
1057
- <p className="text-sm text-muted-foreground">
1058
- Connect your NetSuite account to continue.
1059
- </p>
1060
- <SignInWithNetSuite />
1061
- </div>
1062
- </main>
1063
- );
1064
- }
1065
- `;
1066
- }
1067
-
1068
- function spaAuthTsx(): string {
1069
- return `import * as React from 'react';
1070
- import {
1071
- getNetSuiteSession,
1072
- signInWithNetSuite,
1073
- signOutNetSuite,
1074
- type NetSuiteSession,
1075
- } from './netsuite';
1076
-
1077
- /**
1078
- * NetSuite auth context. Holds the current session (resolved from the Worker's
1079
- * /api/me via getNetSuiteSession) and exposes sign-in / sign-out. The only
1080
- * authoritative auth check is /api/me -- never trust sessionStorage directly.
1081
- */
1082
- export interface NetSuiteAuthContextValue {
1083
- pending: boolean;
1084
- isAuthenticated: boolean;
1085
- session: NetSuiteSession | null;
1086
- signIn: () => void;
1087
- signOut: () => Promise<void>;
1088
- refresh: () => Promise<void>;
1089
- }
1090
-
1091
- const NetSuiteAuthContext =
1092
- React.createContext<NetSuiteAuthContextValue | null>(null);
1093
-
1094
- export function NetSuiteAuthProvider({
1095
- children,
1096
- }: {
1097
- children: React.ReactNode;
1098
- }) {
1099
- const [session, setSession] = React.useState<NetSuiteSession | null>(null);
1100
- const [pending, setPending] = React.useState(true);
1101
-
1102
- const refresh = React.useCallback(async () => {
1103
- setPending(true);
1104
- try {
1105
- setSession(await getNetSuiteSession());
1106
- } finally {
1107
- setPending(false);
1108
- }
1109
- }, []);
1110
-
1111
- React.useEffect(() => {
1112
- refresh();
1113
- }, [refresh]);
1114
-
1115
- const value: NetSuiteAuthContextValue = {
1116
- pending,
1117
- isAuthenticated: Boolean(session?.authenticated),
1118
- session,
1119
- signIn: () => signInWithNetSuite(),
1120
- signOut: signOutNetSuite,
1121
- refresh,
1122
- };
1123
-
1124
- return (
1125
- <NetSuiteAuthContext.Provider value={value}>
1126
- {children}
1127
- </NetSuiteAuthContext.Provider>
1128
- );
1129
- }
1130
-
1131
- export function useNetSuiteAuth(): NetSuiteAuthContextValue {
1132
- const ctx = React.useContext(NetSuiteAuthContext);
1133
- if (!ctx) {
1134
- throw new Error('useNetSuiteAuth must be used within <NetSuiteAuthProvider>');
1135
- }
1136
- return ctx;
1137
- }
1138
- `;
1139
- }
1140
-
1141
- function spaAuthLayoutTsx(): string {
1142
- return `import type { ReactNode } from 'react';
1143
- import { NetSuiteAuthProvider, useNetSuiteAuth } from '../../lib/auth';
1144
- import { LoginPage } from './LoginPage';
1145
-
1146
- /**
1147
- * NetSuite auth layout. This is the ONLY wiring you need to add by hand --
1148
- * wrap the root route's <Outlet /> with it:
1149
- *
1150
- * // src/routes/__root.tsx
1151
- * import { AuthLayout } from '../components/auth/AuthLayout';
1152
- * ...
1153
- * function RootComponent() {
1154
- * return (
1155
- * <>
1156
- * <AuthLayout>
1157
- * <Outlet />
1158
- * </AuthLayout>
1159
- * { ...devtools... }
1160
- * </>
1161
- * );
1162
- * }
1163
- *
1164
- * It provides NetSuite auth context to the whole tree and gates rendering:
1165
- * while the session resolves it shows a spinner, unauthenticated users see
1166
- * <LoginPage />, authenticated users see your routes. Use useNetSuiteAuth()
1167
- * anywhere below it to read the session or sign out. NEVER swap this for a
1168
- * password gate -- real OAuth is the only supported login.
1169
- */
1170
- export function AuthLayout({ children }: { children: ReactNode }) {
1171
- return (
1172
- <NetSuiteAuthProvider>
1173
- <Gate>{children}</Gate>
1174
- </NetSuiteAuthProvider>
1175
- );
1176
- }
1177
-
1178
- function Gate({ children }: { children: ReactNode }) {
1179
- const { pending, isAuthenticated } = useNetSuiteAuth();
1180
-
1181
- if (pending) {
1182
- return (
1183
- <div className="min-h-screen flex items-center justify-center bg-background">
1184
- <div className="h-8 w-8 rounded-full border-2 border-primary border-t-transparent animate-spin" />
1185
- </div>
1186
- );
1187
- }
1188
-
1189
- if (!isAuthenticated) return <LoginPage />;
1190
-
1191
- return <>{children}</>;
1192
- }
1193
- `;
1194
- }
1195
-
1196
- // --- variant: tanstack-start (in-app server routes) ----------------------
1197
- async function executeStart(dir: string): Promise<string> {
1198
- await mkdir(path.join(dir, "src", "lib"), { recursive: true });
1199
- await mkdir(path.join(dir, "src", "components", "auth"), {
1200
- recursive: true,
1201
- });
1202
- await mkdir(path.join(dir, "src", "routes", "auth", "netsuite"), {
1203
- recursive: true,
1204
- });
1205
- await mkdir(path.join(dir, "src", "routes", "api"), { recursive: true });
1206
-
1207
- // Append the NetSuite tables to the drizzle schema (single source of
1208
- // truth; migrations are generated from it and applied automatically).
1209
- // Idempotent -- skip if already present. The 2026-07-30 start template
1210
- // ships schema.ts as comments only (no imports), so prepend the drizzle
1211
- // imports whenever the file has no real (uncommented) import of its own.
1212
- const schemaImports = `import { sql } from 'drizzle-orm'\nimport { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'\n`;
1213
- const schemaPath = path.join(dir, "src", "db", "schema.ts");
1214
- let schemaAppended = false;
1215
- try {
1216
- const current = await readFile(schemaPath, "utf8");
1217
- if (!current.includes("netsuite_flows")) {
1218
- const hasImports = /^import .*drizzle-orm/m.test(current);
1219
- await Bun.write(
1220
- schemaPath,
1221
- (hasImports ? current : schemaImports + current) + schemaAppend(),
1222
- );
1223
- schemaAppended = true;
1224
- }
1225
- } catch {
1226
- // No base schema.ts -- scaffold wasn't run. Write a minimal one.
1227
- await Bun.write(schemaPath, schemaImports + schemaAppend());
1228
- schemaAppended = true;
1229
- }
1230
-
1231
- const files: [string, string][] = [
1232
- [path.join("src", "lib", "netsuite-server.ts"), netsuiteServerTs()],
1233
- [
1234
- path.join("src", "routes", "auth", "netsuite", "start.ts"),
1235
- startRouteTs(),
1236
- ],
1237
- [
1238
- path.join("src", "routes", "auth", "netsuite", "callback.ts"),
1239
- callbackRouteTs(),
1240
- ],
1241
- [
1242
- path.join("src", "routes", "auth", "netsuite", "logout.ts"),
1243
- logoutRouteTs(),
1244
- ],
1245
- [
1246
- path.join("src", "routes", "auth", "netsuite", "config.ts"),
1247
- configRouteTs(),
1248
- ],
1249
- [path.join("src", "routes", "api", "me.ts"), meRouteTs()],
1250
- [path.join("src", "lib", "netsuite.ts"), netsuiteClientTs()],
1251
- [path.join("src", "lib", "auth.tsx"), startAuthTsx()],
1252
- [
1253
- path.join("src", "components", "auth", "AuthLayout.tsx"),
1254
- startAuthLayoutTsx(),
1255
- ],
1256
- [
1257
- path.join("src", "components", "auth", "LoginPage.tsx"),
1258
- startLoginPageTsx(),
1259
- ],
1260
- [
1261
- path.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
1262
- startSignInButtonTsx(),
1263
- ],
1264
- ];
1265
-
1266
- for (const [filePath, content] of files) {
1267
- await Bun.write(path.join(dir, filePath), content);
1268
- }
1269
-
1270
- return [
1271
- `Wrote ${files.length} NetSuite files${schemaAppended ? " + appended src/db/schema.ts" : ""}:`,
1272
- "DOCS -- the authoritative setup guide is https://docs.helloleo.dev/integrations/netsuite . When you tell the user what to do in NetSuite, follow and link these steps verbatim; do not invent steps.",
1273
- " - Record #2 (runtime): https://docs.helloleo.dev/integrations/netsuite#step-4-%E2%80%94-create-integration-record-2-runtime",
1274
- " - Send runtime creds to Leo: https://docs.helloleo.dev/integrations/netsuite#step-5-%E2%80%94-send-the-runtime-credentials-to-leo",
1275
- "",
1276
- " server (OAuth backend lives in the app -- no separate Worker):",
1277
- " - src/lib/netsuite-server.ts (env, PKCE, token exchange, session refresh, netsuiteFetch)",
1278
- " - src/routes/auth/netsuite/start.ts GET begin PKCE flow",
1279
- " - src/routes/auth/netsuite/callback.ts GET code -> tokens -> session",
1280
- " - src/routes/auth/netsuite/logout.ts POST destroy session",
1281
- " - src/routes/auth/netsuite/config.ts GET the exact redirect URI to register",
1282
- " - src/routes/api/me.ts GET bearer -> session (auth check)",
1283
- " - src/db/schema.ts += netsuite_flows + netsuite_sessions",
1284
- " client:",
1285
- " - src/lib/netsuite.ts (same-origin: signIn, session, apiFetch)",
1286
- " - src/lib/auth.tsx (NetSuiteAuthProvider + useNetSuiteAuth)",
1287
- " - src/components/auth/{AuthLayout,LoginPage,SignInWithNetSuite}.tsx",
1288
- "",
1289
- "WIRE IT UP (one manual step): in src/routes/__root.tsx, wrap <Outlet /> with <AuthLayout>:",
1290
- "",
1291
- " import { AuthLayout } from '../components/auth/AuthLayout'",
1292
- "",
1293
- " <AuthLayout>",
1294
- " <Outlet />",
1295
- " </AuthLayout>",
1296
- "",
1297
- "Then:",
1298
- "- DESIGN THE LOGIN PAGE: src/components/auth/LoginPage.tsx is a bare placeholder on purpose. Restyle it to match THIS app (name, brand, layout) as part of building the UI -- do not ship the default. Keep <SignInWithNetSuite /> as the only call-to-action; never add a password form.",
1299
- "- Build your dashboard inside <AuthLayout>. Read the session with useNetSuiteAuth().",
1300
- "- Data routes: add src/routes/api/*.ts and call netsuiteFetch(bearer(request), '/services/rest/query/v1/suiteql', {...}) from the handler.",
1301
- "- MIGRATIONS: schema.ts changed (netsuite_flows + netsuite_sessions). Migrations are generated and applied automatically; if the dev server complains about missing tables, run `bun run db:generate` once (never hand-edit files under drizzle/).",
1302
- "",
1303
- "TWO INTEGRATION RECORDS -- do not confuse them:",
1304
- "- Record #1 (scope: mcp): already connected, used ONLY to build the app (schema discovery). This login does NOT use it.",
1305
- "- Record #2 (scope: rest_webservices): THIS login uses it -- the end user signs into NetSuite through it, and the redirect URI is registered on THIS record. If it doesn't exist yet, tell the user to create it (a second Integration Record, scope rest_webservices only).",
1306
- "",
1307
- "CONFIG -- NETSUITE_ACCOUNT_ID, NETSUITE_CLIENT_ID, NETSUITE_CLIENT_SECRET (secret optional for PKCE public clients). Never hardcode; never put in code. Same var names in every environment, different SOURCE:",
1308
- "- DEV (Fly preview): read from the project .env. HelloLeo writes them there (the setup wizard, or the env tooling). In dev the app uses .env -- do NOT tell the user to use HelloLeo Cloud here.",
1309
- "- PROD (published Worker): the SAME names live as HelloLeo Cloud secrets; the publish step promotes them from .env onto the Worker automatically. Same code path (@helloleo/runtime env), just a different source per environment.",
1310
- "- PUBLIC_APP_URL / HELLOLEO_URL / NETSUITE_BROKER_CALLBACK are set by the platform per environment; don't hardcode them.",
1311
- "- REDIRECT URI -- never guess or hardcode it. When NETSUITE_BROKER_CALLBACK is set (the HelloLeo wizard writes it), the redirect URI is HelloLeo's shared OAuth broker: ONE stable URI the user registers ONCE on Integration Record #2 (scope: rest_webservices), valid for both the preview and the published app -- the broker routes each callback back to this app. Without it, the app falls back to its own origin's callback. Either way, read the exact value from GET /auth/netsuite/config (or getNetSuiteRedirectUri() on the client) and tell the user to register THAT.",
1312
- "- Never replace <AuthLayout> with a password gate. Real OAuth is the only supported login.",
1313
- ].join("\n");
1314
- }
1315
-
1316
- // --- variant: SPA + HelloLeo Cloud Worker --------------------------------
1317
- async function executeSpa(dir: string): Promise<string> {
1318
- await mkdir(path.join(dir, "src", "components", "auth"), {
1319
- recursive: true,
1320
- });
1321
-
1322
- // All additive -- nothing the base scaffold wrote is touched.
1323
- const nsFiles: [string, string][] = [
1324
- [path.join("src", "lib", "netsuite.ts"), spaNetsuiteTs()],
1325
- [path.join("src", "lib", "auth.tsx"), spaAuthTsx()],
1326
- [
1327
- path.join("src", "components", "auth", "AuthLayout.tsx"),
1328
- spaAuthLayoutTsx(),
1329
- ],
1330
- [
1331
- path.join("src", "components", "auth", "LoginPage.tsx"),
1332
- spaLoginPageTsx(),
1333
- ],
1334
- [
1335
- path.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
1336
- spaSignInButtonTsx(),
1337
- ],
1338
- ];
1339
-
1340
- for (const [filePath, content] of nsFiles) {
1341
- await Bun.write(path.join(dir, filePath), content);
1342
- }
1343
-
1344
- return [
1345
- `Wrote ${nsFiles.length} NetSuite OAuth files (all additive, nothing overwritten):`,
1346
- " - src/lib/netsuite.ts (OAuth client: signIn, session, workerFetch)",
1347
- " - src/lib/auth.tsx (NetSuiteAuthProvider and useNetSuiteAuth)",
1348
- " - src/components/auth/AuthLayout.tsx (provider and auth gate)",
1349
- " - src/components/auth/LoginPage.tsx (unauthenticated view)",
1350
- " - src/components/auth/SignInWithNetSuite.tsx (sign-in button)",
1351
- "",
1352
- "Notes:",
1353
- "- DESIGN THE LOGIN PAGE: src/components/auth/LoginPage.tsx is a bare placeholder on purpose. Restyle it to match THIS app (name, brand, layout) as part of building the UI -- do not ship the default. Keep <SignInWithNetSuite /> as the only call-to-action; never add a password form.",
1354
- "- AuthLayout shows LoginPage to signed-out users and renders the app's routes once they sign in with NetSuite.",
1355
- "- Read the session anywhere with useNetSuiteAuth() from src/lib/auth.tsx.",
1356
- "- Never replace AuthLayout with a password gate. Real OAuth is the only supported login.",
1357
- "- The auth worker (deployed via the cloudflare-backend MCP) serves /auth/netsuite/start, /auth/netsuite/callback, /api/me and /auth/netsuite/logout. It reads env.NETSUITE_ACCOUNT_ID, _CLIENT_ID and _CLIENT_SECRET. VITE_WORKER_URL must be set (HelloLeo Cloud seeds it).",
1358
- "- POPUP FLOW (required so login works inside the HelloLeo preview iframe): the client opens /auth/netsuite/start in a popup with ?popup=1. The worker MUST carry that flag through OAuth (persist it on the flow row in /start, read it back in /callback via state). In /auth/netsuite/callback, when the flow was popup, DO NOT redirect to return_to#nssid — instead return an HTML page whose script does: window.opener.postMessage({ type: 'netsuite-auth', nssid: SID }, '*'); window.close(); (fall back to the return_to#nssid redirect only when there is no opener / not a popup). Without this, the preview iframe (partitioned sessionStorage) keeps showing the login after sign-in.",
1359
- "",
1360
- "Next: in src/routes/__root.tsx, import AuthLayout and wrap <Outlet /> with it. Keep any existing providers.",
1361
- "",
1362
- " import { AuthLayout } from '../components/auth/AuthLayout'",
1363
- "",
1364
- " <AuthLayout>",
1365
- " <Outlet />",
1366
- " </AuthLayout>",
1367
- ].join("\n");
1368
- }
1369
-
1370
- export const setupNetsuite = tool({
1371
- description:
1372
- "Add NetSuite runtime OAuth (end-user LOGIN) to an existing project. " +
1373
- "Call scaffold first, then this. Works on both scaffold types and " +
1374
- "picks the variant automatically: on tanstack-start the app hosts " +
1375
- "its own OAuth routes (/auth/netsuite/* + /api/me, same-origin " +
1376
- "client); on tanstack-router (SPA) it writes the client + auth " +
1377
- "components and the OAuth backend is the project's HelloLeo Cloud " +
1378
- "Worker (VITE_WORKER_URL). This login uses NetSuite Integration " +
1379
- "Record #2 (scope: rest_webservices) -- a SEPARATE record from the " +
1380
- "MCP one (scope: mcp) that was used only to BUILD the app; do not " +
1381
- "confuse them. Real OAuth from day one -- never replace <AuthLayout> " +
1382
- "with a password placeholder. Setup guide (link the user to it): " +
1383
- "https://docs.helloleo.dev/integrations/netsuite",
1384
- args: {},
1385
- async execute(_args, context) {
1386
- const dir = context.directory;
1387
- // The variant follows the scaffold, never an env flag: a start app has
1388
- // a server (TanStack Start routes + D1) so it hosts the OAuth itself;
1389
- // the router SPA has no server, so the per-project HelloLeo Cloud
1390
- // Worker is the backend. The redirect URI registered on Integration
1391
- // Record #2 must match: the app's own /auth/netsuite/callback for
1392
- // start, the Worker's fixed callback for the SPA.
1393
- if (await isStartScaffold(dir)) {
1394
- return executeStart(dir);
1395
- }
1396
- return executeSpa(dir);
1397
- },
1398
- });
1399
-
1400
- export const SetupNetsuitePlugin: Plugin = async () => {
1401
- return {
1402
- tool: { setup_netsuite: setupNetsuite },
1403
- };
1404
- };