@shipit-ai/cli 1.166.1 → 1.166.2

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 (283) hide show
  1. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.d.ts.map +1 -1
  2. package/dist/packages/core/src/application/use-cases/features/create/create-feature.use-case.js +19 -2
  3. package/dist/packages/core/src/infrastructure/services/external/github-repository.service.d.ts.map +1 -1
  4. package/dist/packages/core/src/infrastructure/services/external/github-repository.service.js +14 -3
  5. package/dist/src/presentation/web/app/actions/deploy-repository.d.ts.map +1 -1
  6. package/dist/src/presentation/web/app/actions/deploy-repository.js +14 -7
  7. package/dist/src/presentation/web/app/actions/get-merge-review-data.d.ts.map +1 -1
  8. package/dist/src/presentation/web/app/actions/get-merge-review-data.js +66 -23
  9. package/dist/src/presentation/web/app/actions/open-folder.d.ts.map +1 -1
  10. package/dist/src/presentation/web/app/actions/open-folder.js +12 -4
  11. package/dist/src/presentation/web/app/actions/open-shell.d.ts.map +1 -1
  12. package/dist/src/presentation/web/app/actions/open-shell.js +46 -7
  13. package/dist/src/presentation/web/app/api/agent-events/route.d.ts.map +1 -1
  14. package/dist/src/presentation/web/app/api/agent-events/route.js +2 -6
  15. package/dist/src/presentation/web/app/api/attachments/upload-from-path/route.d.ts.map +1 -1
  16. package/dist/src/presentation/web/app/api/attachments/upload-from-path/route.js +21 -17
  17. package/dist/src/presentation/web/app/api/deployment-logs/route.d.ts.map +1 -1
  18. package/dist/src/presentation/web/app/api/deployment-logs/route.js +2 -6
  19. package/dist/src/presentation/web/app/api/directory/list/route.d.ts.map +1 -1
  20. package/dist/src/presentation/web/app/api/directory/list/route.js +39 -24
  21. package/dist/src/presentation/web/app/api/interactive/chat/[featureId]/stream/route.d.ts.map +1 -1
  22. package/dist/src/presentation/web/app/api/interactive/chat/[featureId]/stream/route.js +2 -6
  23. package/dist/src/presentation/web/app/api/interactive/sessions/[id]/stream/route.d.ts.map +1 -1
  24. package/dist/src/presentation/web/app/api/interactive/sessions/[id]/stream/route.js +2 -6
  25. package/dist/src/presentation/web/lib/path-sanitizers.d.ts +50 -0
  26. package/dist/src/presentation/web/lib/path-sanitizers.d.ts.map +1 -0
  27. package/dist/src/presentation/web/lib/path-sanitizers.js +136 -0
  28. package/dist/tsconfig.build.tsbuildinfo +1 -1
  29. package/package.json +6 -6
  30. package/web/.next/BUILD_ID +1 -1
  31. package/web/.next/build-manifest.json +3 -3
  32. package/web/.next/fallback-build-manifest.json +3 -3
  33. package/web/.next/prerender-manifest.json +3 -3
  34. package/web/.next/required-server-files.js +2 -2
  35. package/web/.next/required-server-files.json +2 -2
  36. package/web/.next/server/app/(dashboard)/@drawer/adopt/page/server-reference-manifest.json +29 -29
  37. package/web/.next/server/app/(dashboard)/@drawer/adopt/page.js +1 -1
  38. package/web/.next/server/app/(dashboard)/@drawer/adopt/page.js.nft.json +1 -1
  39. package/web/.next/server/app/(dashboard)/@drawer/adopt/page_client-reference-manifest.js +1 -1
  40. package/web/.next/server/app/(dashboard)/@drawer/chat/page/server-reference-manifest.json +27 -27
  41. package/web/.next/server/app/(dashboard)/@drawer/chat/page.js +1 -1
  42. package/web/.next/server/app/(dashboard)/@drawer/chat/page.js.nft.json +1 -1
  43. package/web/.next/server/app/(dashboard)/@drawer/chat/page_client-reference-manifest.js +1 -1
  44. package/web/.next/server/app/(dashboard)/@drawer/create/page/server-reference-manifest.json +30 -30
  45. package/web/.next/server/app/(dashboard)/@drawer/create/page.js +1 -1
  46. package/web/.next/server/app/(dashboard)/@drawer/create/page.js.nft.json +1 -1
  47. package/web/.next/server/app/(dashboard)/@drawer/create/page_client-reference-manifest.js +1 -1
  48. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page/server-reference-manifest.json +37 -37
  49. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page.js +3 -2
  50. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page.js.nft.json +1 -1
  51. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
  52. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page/server-reference-manifest.json +37 -37
  53. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js +3 -2
  54. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page.js.nft.json +1 -1
  55. package/web/.next/server/app/(dashboard)/@drawer/feature/[featureId]/page_client-reference-manifest.js +1 -1
  56. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/[tab]/page/server-reference-manifest.json +28 -28
  57. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/[tab]/page.js +1 -1
  58. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/[tab]/page.js.nft.json +1 -1
  59. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/[tab]/page_client-reference-manifest.js +1 -1
  60. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page/server-reference-manifest.json +28 -28
  61. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js +1 -1
  62. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page.js.nft.json +1 -1
  63. package/web/.next/server/app/(dashboard)/@drawer/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  64. package/web/.next/server/app/(dashboard)/chat/page/server-reference-manifest.json +27 -27
  65. package/web/.next/server/app/(dashboard)/chat/page.js +1 -1
  66. package/web/.next/server/app/(dashboard)/chat/page.js.nft.json +1 -1
  67. package/web/.next/server/app/(dashboard)/chat/page_client-reference-manifest.js +1 -1
  68. package/web/.next/server/app/(dashboard)/create/page/server-reference-manifest.json +30 -30
  69. package/web/.next/server/app/(dashboard)/create/page.js +1 -1
  70. package/web/.next/server/app/(dashboard)/create/page.js.nft.json +1 -1
  71. package/web/.next/server/app/(dashboard)/create/page_client-reference-manifest.js +1 -1
  72. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page/server-reference-manifest.json +37 -37
  73. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page.js +3 -2
  74. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page.js.nft.json +1 -1
  75. package/web/.next/server/app/(dashboard)/feature/[featureId]/[tab]/page_client-reference-manifest.js +1 -1
  76. package/web/.next/server/app/(dashboard)/feature/[featureId]/page/server-reference-manifest.json +37 -37
  77. package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js +3 -2
  78. package/web/.next/server/app/(dashboard)/feature/[featureId]/page.js.nft.json +1 -1
  79. package/web/.next/server/app/(dashboard)/feature/[featureId]/page_client-reference-manifest.js +1 -1
  80. package/web/.next/server/app/(dashboard)/page/server-reference-manifest.json +27 -27
  81. package/web/.next/server/app/(dashboard)/page.js +1 -1
  82. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  83. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  84. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/[tab]/page/server-reference-manifest.json +28 -28
  85. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/[tab]/page.js +1 -1
  86. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/[tab]/page.js.nft.json +1 -1
  87. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/[tab]/page_client-reference-manifest.js +1 -1
  88. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page/server-reference-manifest.json +28 -28
  89. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js +1 -1
  90. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page.js.nft.json +1 -1
  91. package/web/.next/server/app/(dashboard)/repository/[repositoryId]/page_client-reference-manifest.js +1 -1
  92. package/web/.next/server/app/_global-error.html +1 -1
  93. package/web/.next/server/app/_global-error.rsc +1 -1
  94. package/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  95. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  96. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  97. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  98. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  99. package/web/.next/server/app/_not-found/page/server-reference-manifest.json +6 -6
  100. package/web/.next/server/app/_not-found/page.js +1 -1
  101. package/web/.next/server/app/_not-found/page.js.nft.json +1 -1
  102. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  103. package/web/.next/server/app/api/agent-events/route.js +2 -1
  104. package/web/.next/server/app/api/agent-events/route.js.nft.json +1 -1
  105. package/web/.next/server/app/api/attachments/preview/route.js +1 -1
  106. package/web/.next/server/app/api/attachments/preview/route.js.nft.json +1 -1
  107. package/web/.next/server/app/api/attachments/upload-from-path/route.js +1 -1
  108. package/web/.next/server/app/api/attachments/upload-from-path/route.js.nft.json +1 -1
  109. package/web/.next/server/app/api/deployment-logs/route.js +2 -1
  110. package/web/.next/server/app/api/deployment-logs/route.js.nft.json +1 -1
  111. package/web/.next/server/app/api/dialog/pick-files/route.js +1 -1
  112. package/web/.next/server/app/api/dialog/pick-files/route.js.nft.json +1 -1
  113. package/web/.next/server/app/api/directory/list/route.js +1 -1
  114. package/web/.next/server/app/api/directory/list/route.js.nft.json +1 -1
  115. package/web/.next/server/app/api/evidence/route.js +1 -1
  116. package/web/.next/server/app/api/evidence/route.js.nft.json +1 -1
  117. package/web/.next/server/app/api/graph-data/route.js +1 -1
  118. package/web/.next/server/app/api/graph-data/route.js.nft.json +1 -1
  119. package/web/.next/server/app/api/interactive/chat/[featureId]/messages/route.js +1 -1
  120. package/web/.next/server/app/api/interactive/chat/[featureId]/messages/route.js.nft.json +1 -1
  121. package/web/.next/server/app/api/interactive/chat/[featureId]/stream/route.js +2 -1
  122. package/web/.next/server/app/api/interactive/chat/[featureId]/stream/route.js.nft.json +1 -1
  123. package/web/.next/server/app/api/interactive/sessions/[id]/stream/route.js +2 -1
  124. package/web/.next/server/app/api/interactive/sessions/[id]/stream/route.js.nft.json +1 -1
  125. package/web/.next/server/app/api/sessions-batch/route.js +1 -1
  126. package/web/.next/server/app/api/sessions-batch/route.js.nft.json +1 -1
  127. package/web/.next/server/app/settings/page/server-reference-manifest.json +11 -11
  128. package/web/.next/server/app/settings/page.js +1 -1
  129. package/web/.next/server/app/settings/page.js.nft.json +1 -1
  130. package/web/.next/server/app/settings/page_client-reference-manifest.js +1 -1
  131. package/web/.next/server/app/skills/page/server-reference-manifest.json +11 -11
  132. package/web/.next/server/app/skills/page.js +1 -1
  133. package/web/.next/server/app/skills/page.js.nft.json +1 -1
  134. package/web/.next/server/app/skills/page_client-reference-manifest.js +1 -1
  135. package/web/.next/server/app/tools/page/server-reference-manifest.json +11 -11
  136. package/web/.next/server/app/tools/page.js +1 -1
  137. package/web/.next/server/app/tools/page.js.nft.json +1 -1
  138. package/web/.next/server/app/tools/page_client-reference-manifest.js +1 -1
  139. package/web/.next/server/app/version/page/server-reference-manifest.json +6 -6
  140. package/web/.next/server/app/version/page.js +1 -1
  141. package/web/.next/server/app/version/page.js.nft.json +1 -1
  142. package/web/.next/server/app/version/page_client-reference-manifest.js +1 -1
  143. package/web/.next/server/chunks/{[root-of-the-server]__02xmnal._.js → [root-of-the-server]__08cpfre._.js} +2 -2
  144. package/web/.next/server/chunks/[root-of-the-server]__0_-chcy._.js +3 -0
  145. package/web/.next/server/chunks/[root-of-the-server]__0_-chcy._.js.map +1 -0
  146. package/web/.next/server/chunks/[root-of-the-server]__0aft8l4._.js +9 -0
  147. package/web/.next/server/chunks/{[root-of-the-server]__0_6fhza._.js.map → [root-of-the-server]__0aft8l4._.js.map} +1 -1
  148. package/web/.next/server/chunks/[root-of-the-server]__0e9p7em._.js +3 -0
  149. package/web/.next/server/chunks/[root-of-the-server]__0e9p7em._.js.map +1 -0
  150. package/web/.next/server/chunks/{[root-of-the-server]__0.2exzi._.js → [root-of-the-server]__0gfvkg8._.js} +2 -2
  151. package/web/.next/server/chunks/{[root-of-the-server]__0ip_e1x._.js → [root-of-the-server]__0hcp97v._.js} +2 -2
  152. package/web/.next/server/chunks/{[root-of-the-server]__09118p2._.js → [root-of-the-server]__0iel39d._.js} +2 -2
  153. package/web/.next/server/chunks/[root-of-the-server]__0kc8ify._.js +12 -0
  154. package/web/.next/server/chunks/[root-of-the-server]__0kc8ify._.js.map +1 -0
  155. package/web/.next/server/chunks/[root-of-the-server]__0r5uk_8._.js +9 -0
  156. package/web/.next/server/chunks/[root-of-the-server]__0r5uk_8._.js.map +1 -0
  157. package/web/.next/server/chunks/[root-of-the-server]__0tb~wwk._.js +1 -1
  158. package/web/.next/server/chunks/{[root-of-the-server]__04jjtl_._.js → [root-of-the-server]__0u1jyv9._.js} +2 -2
  159. package/web/.next/server/chunks/{[root-of-the-server]__07suer1._.js → [root-of-the-server]__0zu_byw._.js} +2 -2
  160. package/web/.next/server/chunks/[root-of-the-server]__13e2_kk._.js +18 -0
  161. package/web/.next/server/chunks/[root-of-the-server]__13e2_kk._.js.map +1 -0
  162. package/web/.next/server/chunks/ssr/0j.8_web__next-internal_server_app_(dashboard)_@drawer_adopt_page_actions_00~eq5i.js +1 -1
  163. package/web/.next/server/chunks/ssr/0j.8_web__next-internal_server_app_(dashboard)_@drawer_adopt_page_actions_00~eq5i.js.map +1 -1
  164. package/web/.next/server/chunks/ssr/0j.8_web__next-internal_server_app_(dashboard)_@drawer_chat_page_actions_0979_c..js +1 -1
  165. package/web/.next/server/chunks/ssr/0j.8_web__next-internal_server_app_(dashboard)_@drawer_chat_page_actions_0979_c..js.map +1 -1
  166. package/web/.next/server/chunks/ssr/0j.8_web__next-internal_server_app_(dashboard)_chat_page_actions_0dqll_1.js +1 -1
  167. package/web/.next/server/chunks/ssr/0j.8_web__next-internal_server_app_(dashboard)_chat_page_actions_0dqll_1.js.map +1 -1
  168. package/web/.next/server/chunks/ssr/0j.8_web_components_common_control-center-drawer_create-drawer-client_tsx_0g70fc5._.js +1 -1
  169. package/web/.next/server/chunks/ssr/0j.8_web_components_common_control-center-drawer_create-drawer-client_tsx_0g70fc5._.js.map +1 -1
  170. package/web/.next/server/chunks/ssr/0j.8_web_components_common_control-center-drawer_feature-drawer-client_tsx_104cna.._.js +2 -2
  171. package/web/.next/server/chunks/ssr/0j.8_web_components_common_control-center-drawer_feature-drawer-client_tsx_104cna.._.js.map +1 -1
  172. package/web/.next/server/chunks/ssr/0ukq_presentation_web_components_features_settings_settings-page-client_tsx_0j1uius._.js +1 -1
  173. package/web/.next/server/chunks/ssr/0ukq_presentation_web_components_features_settings_settings-page-client_tsx_0j1uius._.js.map +1 -1
  174. package/web/.next/server/chunks/ssr/11y9_components_common_control-center-drawer_repository-drawer-client_tsx_09z.znp._.js +1 -1
  175. package/web/.next/server/chunks/ssr/11y9_components_common_control-center-drawer_repository-drawer-client_tsx_09z.znp._.js.map +1 -1
  176. package/web/.next/server/chunks/ssr/[root-of-the-server]__04nnbmc._.js +3 -0
  177. package/web/.next/server/chunks/ssr/[root-of-the-server]__04nnbmc._.js.map +1 -0
  178. package/web/.next/server/chunks/ssr/[root-of-the-server]__07740t6._.js +3 -0
  179. package/web/.next/server/chunks/ssr/[root-of-the-server]__07740t6._.js.map +1 -0
  180. package/web/.next/server/chunks/ssr/[root-of-the-server]__0l~puw4._.js +3 -0
  181. package/web/.next/server/chunks/ssr/[root-of-the-server]__0l~puw4._.js.map +1 -0
  182. package/web/.next/server/chunks/ssr/[root-of-the-server]__0o3qggc._.js +1 -1
  183. package/web/.next/server/chunks/ssr/[root-of-the-server]__0o3qggc._.js.map +1 -1
  184. package/web/.next/server/chunks/ssr/{[root-of-the-server]__0qh.wn.._.js → [root-of-the-server]__0q3-gz.._.js} +2 -2
  185. package/web/.next/server/chunks/ssr/[root-of-the-server]__0rv1gci._.js +1 -1
  186. package/web/.next/server/chunks/ssr/[root-of-the-server]__0vwjc_m._.js +3 -0
  187. package/web/.next/server/chunks/ssr/[root-of-the-server]__0vwjc_m._.js.map +1 -0
  188. package/web/.next/server/chunks/ssr/{[root-of-the-server]__12g8h3_._.js → [root-of-the-server]__0w4__yd._.js} +3 -3
  189. package/web/.next/server/chunks/ssr/_01mq~sm._.js +1 -1
  190. package/web/.next/server/chunks/ssr/_01mq~sm._.js.map +1 -1
  191. package/web/.next/server/chunks/ssr/_01sesw0._.js +1 -1
  192. package/web/.next/server/chunks/ssr/_01sesw0._.js.map +1 -1
  193. package/web/.next/server/chunks/ssr/{_0e4npv~._.js → _04rrcmm._.js} +2 -2
  194. package/web/.next/server/chunks/ssr/{_0e4npv~._.js.map → _04rrcmm._.js.map} +1 -1
  195. package/web/.next/server/chunks/ssr/{_0nvrqsj._.js → _0c497sr._.js} +2 -2
  196. package/web/.next/server/chunks/ssr/{_0nvrqsj._.js.map → _0c497sr._.js.map} +1 -1
  197. package/web/.next/server/chunks/ssr/{_0a-ddx-._.js → _0c741v_._.js} +2 -2
  198. package/web/.next/server/chunks/ssr/{_0a-ddx-._.js.map → _0c741v_._.js.map} +1 -1
  199. package/web/.next/server/chunks/ssr/_0jpbsh_._.js +1 -1
  200. package/web/.next/server/chunks/ssr/_0jpbsh_._.js.map +1 -1
  201. package/web/.next/server/chunks/ssr/_0vyfc4b._.js +1 -1
  202. package/web/.next/server/chunks/ssr/_0vyfc4b._.js.map +1 -1
  203. package/web/.next/server/chunks/ssr/_0w-_hww._.js +1 -1
  204. package/web/.next/server/chunks/ssr/_0w-_hww._.js.map +1 -1
  205. package/web/.next/server/chunks/ssr/_0~7lwu_._.js +1 -1
  206. package/web/.next/server/chunks/ssr/_0~7lwu_._.js.map +1 -1
  207. package/web/.next/server/chunks/ssr/_109n-y4._.js +1 -1
  208. package/web/.next/server/chunks/ssr/src_presentation_web_0.e4~xc._.js +1 -1
  209. package/web/.next/server/chunks/ssr/src_presentation_web_0.e4~xc._.js.map +1 -1
  210. package/web/.next/server/chunks/ssr/src_presentation_web_00dvh.m._.js +1 -1
  211. package/web/.next/server/chunks/ssr/src_presentation_web_00dvh.m._.js.map +1 -1
  212. package/web/.next/server/chunks/ssr/src_presentation_web_06b6~lt._.js +2 -2
  213. package/web/.next/server/chunks/ssr/src_presentation_web_06b6~lt._.js.map +1 -1
  214. package/web/.next/server/chunks/ssr/src_presentation_web_08fy2mf._.js +1 -1
  215. package/web/.next/server/chunks/ssr/src_presentation_web_08fy2mf._.js.map +1 -1
  216. package/web/.next/server/chunks/ssr/src_presentation_web_0f~udu1._.js +1 -1
  217. package/web/.next/server/chunks/ssr/src_presentation_web_0f~udu1._.js.map +1 -1
  218. package/web/.next/server/chunks/ssr/src_presentation_web_0qys821._.js +2 -2
  219. package/web/.next/server/chunks/ssr/src_presentation_web_0qys821._.js.map +1 -1
  220. package/web/.next/server/chunks/ssr/src_presentation_web_0q~dt0o._.js +1 -1
  221. package/web/.next/server/chunks/ssr/src_presentation_web_0q~dt0o._.js.map +1 -1
  222. package/web/.next/server/chunks/ssr/src_presentation_web_11jrkxt._.js +1 -1
  223. package/web/.next/server/chunks/ssr/src_presentation_web_11jrkxt._.js.map +1 -1
  224. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_(dashboard)_page_actions_1199d3x.js +1 -1
  225. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_(dashboard)_page_actions_1199d3x.js.map +1 -1
  226. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app__not-found_page_actions_0m2jqxx.js +1 -1
  227. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app__not-found_page_actions_0m2jqxx.js.map +1 -1
  228. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_version_page_actions_0krkh_0.js +1 -1
  229. package/web/.next/server/chunks/ssr/src_presentation_web__next-internal_server_app_version_page_actions_0krkh_0.js.map +1 -1
  230. package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_approve-feature_ts_0pjb_re._.js +3 -0
  231. package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_approve-feature_ts_0pjb_re._.js.map +1 -0
  232. package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_load-settings_ts_0b8f3pf._.js +1 -1
  233. package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_open-ide_ts_0w2wqvu._.js +1 -1
  234. package/web/.next/server/chunks/ssr/src_presentation_web_app_actions_open-ide_ts_0w2wqvu._.js.map +1 -1
  235. package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_0l3oxx9._.js +1 -1
  236. package/web/.next/server/chunks/ssr/src_presentation_web_components_features_control-center_0l3oxx9._.js.map +1 -1
  237. package/web/.next/server/middleware-build-manifest.js +3 -3
  238. package/web/.next/server/pages/500.html +1 -1
  239. package/web/.next/server/server-reference-manifest.js +1 -1
  240. package/web/.next/server/server-reference-manifest.json +49 -49
  241. package/web/.next/static/chunks/{0t.pzrmeoq6th.js → 0-fy~80ui.5os.js} +1 -1
  242. package/web/.next/static/chunks/{00dg6gti40.3i.js → 039ic1ygq-to3.js} +1 -1
  243. package/web/.next/static/chunks/{0ntgq3d_.m5el.js → 04xk1iouwcfcq.js} +3 -3
  244. package/web/.next/static/chunks/{0njrgvmyafrod.js → 07a4jt64wdipb.js} +1 -1
  245. package/web/.next/static/chunks/{14g1l3~6i5251.js → 07gx-h_y91lay.js} +1 -1
  246. package/web/.next/static/chunks/{0ist7260j__0m.js → 0_imq4rg3q.fe.js} +2 -2
  247. package/web/.next/static/chunks/{09dqgshddfxff.js → 0c_bi0dck80dt.js} +1 -1
  248. package/web/.next/static/chunks/{0awttldb-.7m..js → 0ex35-_jtxyjc.js} +1 -1
  249. package/web/.next/static/chunks/{0_c5~n__lz4ks.js → 0i084mozx131g.js} +1 -1
  250. package/web/.next/static/chunks/{0_--5mgqukm__.js → 0k~55i.ofbdeb.js} +1 -1
  251. package/web/.next/static/chunks/{0t8zwgaz.d1s5.js → 0oq-cvtg8rjjp.js} +1 -1
  252. package/web/.next/static/chunks/{0d-2jp.f._l2e.js → 0t_6hx6ul7umb.js} +1 -1
  253. package/web/.next/static/chunks/{0nk2r-18.7g6r.js → 0whez3wju~9ok.js} +1 -1
  254. package/web/.next/server/chunks/[root-of-the-server]__0-3b27b._.js +0 -9
  255. package/web/.next/server/chunks/[root-of-the-server]__0-3b27b._.js.map +0 -1
  256. package/web/.next/server/chunks/[root-of-the-server]__0_6fhza._.js +0 -9
  257. package/web/.next/server/chunks/[root-of-the-server]__0esdmru._.js +0 -12
  258. package/web/.next/server/chunks/[root-of-the-server]__0esdmru._.js.map +0 -1
  259. package/web/.next/server/chunks/[root-of-the-server]__0l1p8bx._.js +0 -3
  260. package/web/.next/server/chunks/[root-of-the-server]__0l1p8bx._.js.map +0 -1
  261. package/web/.next/server/chunks/[root-of-the-server]__0p~owgt._.js +0 -18
  262. package/web/.next/server/chunks/[root-of-the-server]__0p~owgt._.js.map +0 -1
  263. package/web/.next/server/chunks/[root-of-the-server]__0rru~m.._.js +0 -3
  264. package/web/.next/server/chunks/[root-of-the-server]__0rru~m.._.js.map +0 -1
  265. package/web/.next/server/chunks/ssr/[root-of-the-server]__045sv4b._.js +0 -3
  266. package/web/.next/server/chunks/ssr/[root-of-the-server]__045sv4b._.js.map +0 -1
  267. package/web/.next/server/chunks/ssr/[root-of-the-server]__0d_0_fp._.js +0 -3
  268. package/web/.next/server/chunks/ssr/[root-of-the-server]__0d_0_fp._.js.map +0 -1
  269. package/web/.next/server/chunks/ssr/[root-of-the-server]__0l4d7e.._.js +0 -3
  270. package/web/.next/server/chunks/ssr/[root-of-the-server]__0l4d7e.._.js.map +0 -1
  271. package/web/.next/server/chunks/ssr/[root-of-the-server]__0r32z03._.js +0 -3
  272. package/web/.next/server/chunks/ssr/[root-of-the-server]__0r32z03._.js.map +0 -1
  273. /package/web/.next/server/chunks/{[root-of-the-server]__02xmnal._.js.map → [root-of-the-server]__08cpfre._.js.map} +0 -0
  274. /package/web/.next/server/chunks/{[root-of-the-server]__0.2exzi._.js.map → [root-of-the-server]__0gfvkg8._.js.map} +0 -0
  275. /package/web/.next/server/chunks/{[root-of-the-server]__0ip_e1x._.js.map → [root-of-the-server]__0hcp97v._.js.map} +0 -0
  276. /package/web/.next/server/chunks/{[root-of-the-server]__09118p2._.js.map → [root-of-the-server]__0iel39d._.js.map} +0 -0
  277. /package/web/.next/server/chunks/{[root-of-the-server]__04jjtl_._.js.map → [root-of-the-server]__0u1jyv9._.js.map} +0 -0
  278. /package/web/.next/server/chunks/{[root-of-the-server]__07suer1._.js.map → [root-of-the-server]__0zu_byw._.js.map} +0 -0
  279. /package/web/.next/server/chunks/ssr/{[root-of-the-server]__0qh.wn.._.js.map → [root-of-the-server]__0q3-gz.._.js.map} +0 -0
  280. /package/web/.next/server/chunks/ssr/{[root-of-the-server]__12g8h3_._.js.map → [root-of-the-server]__0w4__yd._.js.map} +0 -0
  281. /package/web/.next/static/{ZpPnD_b687G9xVr2nzrds → ynyh_sSxbFA995FRvBUxs}/_buildManifest.js +0 -0
  282. /package/web/.next/static/{ZpPnD_b687G9xVr2nzrds → ynyh_sSxbFA995FRvBUxs}/_clientMiddlewareManifest.js +0 -0
  283. /package/web/.next/static/{ZpPnD_b687G9xVr2nzrds → ynyh_sSxbFA995FRvBUxs}/_ssgManifest.js +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"create-feature.use-case.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/application/use-cases/features/create/create-feature.use-case.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wCAAwC,CAAC;AAMtE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oEAAoE,CAAC;AAC7G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8DAA8D,CAAC;AACrG,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,iEAAiE,CAAC;AACnH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gEAAgE,CAAC;AAC1G,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8DAA8D,CAAC;AAC5G,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uEAAuE,CAAC;AACnH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4DAA4D,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2DAA2D,CAAC;AACjG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qEAAqE,CAAC;AAC/G,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,wEAAwE,CAAC;AAExH,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE9F,qBACa,oBAAoB;IAG7B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAE5B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAEhC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAE9B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAEhC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAElC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAE/B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAElC,OAAO,CAAC,QAAQ,CAAC,cAAc;IAE/B,OAAO,CAAC,QAAQ,CAAC,YAAY;gBAtBZ,WAAW,EAAE,kBAAkB,EAE/B,eAAe,EAAE,gBAAgB,EAEjC,YAAY,EAAE,2BAA2B,EAEzC,aAAa,EAAE,mBAAmB,EAElC,eAAe,EAAE,uBAAuB,EAExC,iBAAiB,EAAE,iBAAiB,EAEpC,YAAY,EAAE,YAAY,EAE1B,cAAc,EAAE,qBAAqB,EAErC,YAAY,EAAE,aAAa,EAE3B,iBAAiB,EAAE,yBAAyB,EAE5C,cAAc,EAAE,eAAe,EAE/B,YAAY,EAAE,mBAAmB;IAGpD;;;OAGG;IACG,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAMtE;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAyI1E;;;OAGG;IACG,kBAAkB,CACtB,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,kBAAkB,EACzB,WAAW,EAAE,OAAO,GACnB,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;CAoJ1D"}
1
+ {"version":3,"file":"create-feature.use-case.d.ts","sourceRoot":"","sources":["../../../../../../../../packages/core/src/application/use-cases/features/create/create-feature.use-case.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wCAAwC,CAAC;AAMtE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oEAAoE,CAAC;AAC7G,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8DAA8D,CAAC;AACrG,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,iEAAiE,CAAC;AACnH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gEAAgE,CAAC;AAC1G,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8DAA8D,CAAC;AAC5G,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uEAAuE,CAAC;AACnH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4DAA4D,CAAC;AAChG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2DAA2D,CAAC;AACjG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qEAAqE,CAAC;AAC/G,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,wEAAwE,CAAC;AAExH,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAE9F,qBACa,oBAAoB;IAG7B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAE5B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAEhC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAE9B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAEhC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAElC,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAE/B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAE7B,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAElC,OAAO,CAAC,QAAQ,CAAC,cAAc;IAE/B,OAAO,CAAC,QAAQ,CAAC,YAAY;gBAtBZ,WAAW,EAAE,kBAAkB,EAE/B,eAAe,EAAE,gBAAgB,EAEjC,YAAY,EAAE,2BAA2B,EAEzC,aAAa,EAAE,mBAAmB,EAElC,eAAe,EAAE,uBAAuB,EAExC,iBAAiB,EAAE,iBAAiB,EAEpC,YAAY,EAAE,YAAY,EAE1B,cAAc,EAAE,qBAAqB,EAErC,YAAY,EAAE,aAAa,EAE3B,iBAAiB,EAAE,yBAAyB,EAE5C,cAAc,EAAE,eAAe,EAE/B,YAAY,EAAE,mBAAmB;IAGpD;;;OAGG;IACG,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAMtE;;;;OAIG;IACG,YAAY,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAwJ1E;;;OAGG;IACG,kBAAkB,CACtB,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,kBAAkB,EACzB,WAAW,EAAE,OAAO,GACnB,OAAO,CAAC;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,OAAO,CAAA;KAAE,CAAC;CAoJ1D"}
@@ -116,8 +116,25 @@ let CreateFeatureUseCase = class CreateFeatureUseCase {
116
116
  }
117
117
  }
118
118
  }
119
- // Resolve or create repository entity for this path
120
- const normalizedPath = effectiveRepoPath.replace(/\\/g, '/').replace(/\/+$/, '') || effectiveRepoPath;
119
+ // Resolve or create repository entity for this path.
120
+ //
121
+ // Normalization: (1) convert backslashes to forward slashes,
122
+ // (2) strip trailing slashes.
123
+ //
124
+ // Step 2 is implemented as a while-loop specifically to work around a
125
+ // CodeQL js/polynomial-redos FALSE POSITIVE on the equivalent regex
126
+ // `/\/+$/`. That pattern is actually O(n) on V8 and all other modern
127
+ // regex engines (single character class, anchored to end, no alternation
128
+ // — there is no backtracking choice point). CodeQL's query is overly
129
+ // strict on quantifiers against tainted input and does not model this
130
+ // correctly. The loop produces identical output and suppresses the
131
+ // alert without requiring an inline dismissal comment.
132
+ let normalizedPath = effectiveRepoPath.replace(/\\/g, '/');
133
+ while (normalizedPath.length > 1 && normalizedPath.endsWith('/')) {
134
+ normalizedPath = normalizedPath.slice(0, -1);
135
+ }
136
+ if (!normalizedPath)
137
+ normalizedPath = effectiveRepoPath;
121
138
  let repository = await this.repositoryRepo.findByPath(normalizedPath);
122
139
  const now = new Date();
123
140
  if (!repository) {
@@ -1 +1 @@
1
- {"version":3,"file":"github-repository.service.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/infrastructure/services/external/github-repository.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EACV,wBAAwB,EACxB,UAAU,EACV,kBAAkB,EAClB,2BAA2B,EAC3B,YAAY,EACZ,eAAe,EAChB,MAAM,mFAAmF,CAAC;AAyB3F,qBACa,uBAAwB,YAAW,wBAAwB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,YAAY;IAErE,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB1B,oBAAoB,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAuClF,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IA2BlD,eAAe,CACnB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,IAAI,CAAC;IAgDhB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe;IA2CtC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAyB9C,mBAAmB;CAOlC"}
1
+ {"version":3,"file":"github-repository.service.d.ts","sourceRoot":"","sources":["../../../../../../../packages/core/src/infrastructure/services/external/github-repository.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EACV,wBAAwB,EACxB,UAAU,EACV,kBAAkB,EAClB,2BAA2B,EAC3B,YAAY,EACZ,eAAe,EAChB,MAAM,mFAAmF,CAAC;AAyB3F,qBACa,uBAAwB,YAAW,wBAAwB;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,YAAY;IAErE,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;IAyB1B,oBAAoB,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAkDlF,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC;IA2BlD,eAAe,CACnB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,IAAI,CAAC;IAgDhB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe;IA2CtC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAyB9C,mBAAmB;CAOlC"}
@@ -66,9 +66,20 @@ let GitHubRepositoryService = class GitHubRepositoryService {
66
66
  String(limit),
67
67
  ];
68
68
  if (options?.search) {
69
- // gh repo list does not have a --match flag; use jq to filter by name
70
- const escaped = options.search.replace(/"/g, '\\"');
71
- args.push('-q', `[.[] | select(.name | test("${escaped}"; "i"))]`);
69
+ // gh repo list does not have a --match flag; use jq to filter by name.
70
+ //
71
+ // Use a literal case-insensitive substring match via ascii_downcase +
72
+ // contains() instead of test() (which would treat the input as a
73
+ // regex). This eliminates both regex-injection and ReDoS risk from
74
+ // user-supplied metacharacters.
75
+ //
76
+ // The search string is JSON-encoded before embedding so that quotes,
77
+ // backslashes, newlines, and any other special character are fully
78
+ // escaped by the language runtime (not a hand-rolled single-char
79
+ // replace). JSON.stringify produces a string literal that is
80
+ // simultaneously valid JSON and a valid jq string literal.
81
+ const jqLiteral = JSON.stringify(options.search.toLowerCase());
82
+ args.push('-q', `[.[] | select((.name | ascii_downcase) | contains(${jqLiteral}))]`);
72
83
  }
73
84
  try {
74
85
  const { stdout } = await this.execFile('gh', args);
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-repository.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/web/app/actions/deploy-repository.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAK1E,wBAAsB,gBAAgB,CACpC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CAAE,CAAC,CAiCxE"}
1
+ {"version":3,"file":"deploy-repository.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/web/app/actions/deploy-repository.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAK1E,wBAAsB,gBAAgB,CACpC,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,eAAe,CAAA;CAAE,CAAC,CAuCxE"}
@@ -1,5 +1,5 @@
1
1
  'use server';
2
- import { existsSync } from 'node:fs';
2
+ import { realpathSync } from 'node:fs';
3
3
  import { isAbsolute } from 'node:path';
4
4
  import { resolve } from '../../lib/server-container.js';
5
5
  import { createDeploymentLogger } from '../../lib/core-utils.js';
@@ -12,12 +12,19 @@ export async function deployRepository(repositoryPath) {
12
12
  log.warn('rejected — not an absolute path');
13
13
  return { success: false, error: 'repositoryPath must be an absolute path' };
14
14
  }
15
+ // Resolve through realpath() up-front. Every subsequent use references
16
+ // the symlink-resolved absolute path, not the raw user input. This is the
17
+ // sanitizer CodeQL's js/path-injection analysis recognizes.
18
+ let resolvedPath;
15
19
  try {
16
- if (!existsSync(repositoryPath)) {
17
- log.warn(`directory does not exist: "${repositoryPath}"`);
18
- return { success: false, error: `Directory does not exist: ${repositoryPath}` };
19
- }
20
- if (isSameShipitAiInstance(repositoryPath)) {
20
+ resolvedPath = realpathSync(repositoryPath);
21
+ }
22
+ catch {
23
+ log.warn(`directory does not exist: "${repositoryPath}"`);
24
+ return { success: false, error: 'Directory does not exist' };
25
+ }
26
+ try {
27
+ if (isSameShipitAiInstance(resolvedPath)) {
21
28
  log.warn('rejected — target is the running ShipIT instance');
22
29
  return {
23
30
  success: false,
@@ -26,7 +33,7 @@ export async function deployRepository(repositoryPath) {
26
33
  }
27
34
  log.info('directory exists, calling deploymentService.start()');
28
35
  const deploymentService = resolve('IDeploymentService');
29
- deploymentService.start(repositoryPath, repositoryPath, 'repository');
36
+ deploymentService.start(resolvedPath, resolvedPath, 'repository');
30
37
  log.info('start() returned successfully — state=Booting');
31
38
  return { success: true, state: DeploymentState.Booting };
32
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"get-merge-review-data.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/web/app/actions/get-merge-review-data.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,sDAAsD,CAAC;AAI9D,KAAK,wBAAwB,GAAG,eAAe,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAqCpE,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CA4G7F"}
1
+ {"version":3,"file":"get-merge-review-data.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/web/app/actions/get-merge-review-data.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,eAAe,EAEhB,MAAM,sDAAsD,CAAC;AAI9D,KAAK,wBAAwB,GAAG,eAAe,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAwDpE,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAuI7F"}
@@ -1,12 +1,17 @@
1
1
  'use server';
2
2
  import { createHash } from 'node:crypto';
3
- import { readFileSync, existsSync } from 'node:fs';
3
+ import { readFileSync } from 'node:fs';
4
4
  import { basename, join, dirname } from 'node:path';
5
5
  import { resolve } from '../../lib/server-container.js';
6
+ import { realpathOrNull, isWithinRoot } from '../../lib/path-sanitizers.js';
6
7
  import { computeWorktreePath, getShipitAiHomeDir } from '../../lib/core-utils.js';
7
8
  /**
8
9
  * Compute the ShipIT evidence directory for a given repository and feature.
9
10
  * Path: ~/.shipit-ai/repos/<sha256-hash-prefix>/evidence/<featureId>/
11
+ *
12
+ * The sha256 hash of the repository path makes the resulting directory name
13
+ * deterministic and hex-only, neutralizing any path-injection risk from the
14
+ * repositoryPath input. The featureId is a UUID from the DB lookup.
10
15
  */
11
16
  function computeEvidenceDir(repositoryPath, featureId) {
12
17
  const repoHash = createHash('sha256').update(repositoryPath).digest('hex').slice(0, 16);
@@ -19,21 +24,34 @@ function computeEvidenceDir(repositoryPath, featureId) {
19
24
  * deleted so those paths no longer resolve. The evidence files were also saved
20
25
  * to the ShipIT evidence dir with the same filename, so we map relative paths
21
26
  * to absolute paths there.
27
+ *
28
+ * IMPORTANT: the returned paths must remain in the SAME form as the
29
+ * `/api/evidence` route expects (it uses `path.resolve` + `.startsWith`
30
+ * against the unresolved `SHIPIT_AI_HOME/repos` root). Do not pass
31
+ * realpath-resolved paths here, because on macOS `SHIPIT_AI_HOME=/tmp/...`
32
+ * resolves to `/private/tmp/...` and the evidence route's prefix check
33
+ * would reject the realpath'd form. Basename-only containment (strip any
34
+ * directory traversal via `basename()` then `join()` with the known-safe
35
+ * `evidenceDir`) is sufficient sanitization for this taint source because
36
+ * `basename()` cannot return a path-traversal string.
22
37
  */
23
38
  function normalizeEvidencePaths(evidence, evidenceDir) {
24
39
  return evidence.map((e) => {
40
+ // If the manifest path is absolute and already present on disk, keep
41
+ // it verbatim — this preserves the original reference and matches the
42
+ // pre-fix behavior for already-migrated evidence. We do NOT realpath
43
+ // the result because the evidence route does not realpath its input,
44
+ // and mismatching normalization forms would cause 404s.
25
45
  if (e.relativePath.startsWith('/')) {
26
- // Already absolute — check if the file exists; if not, try the evidence dir
27
- if (existsSync(e.relativePath))
28
- return e;
29
- const fallback = join(evidenceDir, basename(e.relativePath)).replace(/\\/g, '/');
30
- if (existsSync(fallback))
31
- return { ...e, relativePath: fallback };
32
46
  return e;
33
47
  }
34
- // Relative path — resolve to evidence dir using the filename
35
- const absolutePath = join(evidenceDir, basename(e.relativePath)).replace(/\\/g, '/');
36
- return { ...e, relativePath: absolutePath };
48
+ // Relative path — map to evidenceDir using basename() only. `basename`
49
+ // strips any directory components including `..` sequences, so the
50
+ // joined result is guaranteed to live directly inside evidenceDir
51
+ // regardless of what the manifest file contained.
52
+ const safeName = basename(e.relativePath);
53
+ const target = join(evidenceDir, safeName).replace(/\\/g, '/');
54
+ return { ...e, relativePath: target };
37
55
  });
38
56
  }
39
57
  export async function getMergeReviewData(featureId) {
@@ -88,19 +106,44 @@ export async function getMergeReviewData(featureId) {
88
106
  : null;
89
107
  if (evidenceDir) {
90
108
  try {
91
- const manifestPath = join(evidenceDir, 'manifest.json');
92
- if (existsSync(manifestPath)) {
93
- const raw = JSON.parse(readFileSync(manifestPath, 'utf-8'));
94
- const normalized = normalizeEvidencePaths(raw, evidenceDir);
95
- // Deduplicate: same type + relativePath means the same evidence entry
96
- const seen = new Set();
97
- evidence = normalized.filter((e) => {
98
- const key = `${e.type}:${e.relativePath}`;
99
- if (seen.has(key))
100
- return false;
101
- seen.add(key);
102
- return true;
103
- });
109
+ // SECURITY: validate the manifest we're about to read lives inside
110
+ // SHIPIT_AI_HOME. computeEvidenceDir() already hashes repositoryPath
111
+ // to a hex directory name, but we still run a realpath containment
112
+ // check because CodeQL's js/path-injection analysis recognizes the
113
+ // realpathOrNull + isWithinRoot pair as a sanitizer chain.
114
+ //
115
+ // Resolve-once semantics: realpath the shipit home dir and the
116
+ // evidence dir exactly once each, then reuse those resolved values
117
+ // for every subsequent containment check. This avoids both the
118
+ // extra syscalls and the TOCTOU window that a recursive resolve-
119
+ // and-check helper would introduce.
120
+ //
121
+ // IMPORTANT: the resolved paths are used ONLY for the read-time
122
+ // security check. The unresolved `evidenceDir` is what we pass to
123
+ // normalizeEvidencePaths so the paths returned to the client match
124
+ // what the /api/evidence route expects — see the comment on
125
+ // normalizeEvidencePaths for the full rationale.
126
+ const resolvedHome = realpathOrNull(getShipitAiHomeDir());
127
+ const resolvedEvidenceDir = realpathOrNull(evidenceDir);
128
+ if (resolvedHome &&
129
+ resolvedEvidenceDir &&
130
+ isWithinRoot(resolvedEvidenceDir, resolvedHome)) {
131
+ const resolvedManifest = realpathOrNull(join(resolvedEvidenceDir, 'manifest.json'));
132
+ if (resolvedManifest && isWithinRoot(resolvedManifest, resolvedEvidenceDir)) {
133
+ const raw = JSON.parse(readFileSync(resolvedManifest, 'utf-8'));
134
+ // Pass the UNRESOLVED evidenceDir so returned paths share the
135
+ // same root form the evidence route's prefix check expects.
136
+ const normalized = normalizeEvidencePaths(raw, evidenceDir);
137
+ // Deduplicate: same type + relativePath means the same evidence entry
138
+ const seen = new Set();
139
+ evidence = normalized.filter((e) => {
140
+ const key = `${e.type}:${e.relativePath}`;
141
+ if (seen.has(key))
142
+ return false;
143
+ seen.add(key);
144
+ return true;
145
+ });
146
+ }
104
147
  }
105
148
  }
106
149
  catch {
@@ -1 +1 @@
1
- {"version":3,"file":"open-folder.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/web/app/actions/open-folder.ts"],"names":[],"mappings":"AAiBA,wBAAsB,UAAU,CAC9B,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAkC9D"}
1
+ {"version":3,"file":"open-folder.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/web/app/actions/open-folder.ts"],"names":[],"mappings":"AAiBA,wBAAsB,UAAU,CAC9B,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAyC9D"}
@@ -1,5 +1,5 @@
1
1
  'use server';
2
- import { existsSync } from 'node:fs';
2
+ import { realpathSync } from 'node:fs';
3
3
  import { platform } from 'node:os';
4
4
  import { isAbsolute, normalize } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
@@ -17,7 +17,15 @@ export async function openFolder(repositoryPath) {
17
17
  return { success: false, error: 'repositoryPath must be an absolute path' };
18
18
  }
19
19
  try {
20
- if (!existsSync(repositoryPath)) {
20
+ // Resolve through realpath() up-front. All subsequent uses of the path
21
+ // reference this symlink-resolved absolute value, not the raw user input.
22
+ // This eliminates path-injection via symlinks and is the sanitizer that
23
+ // CodeQL's js/path-injection analysis recognizes.
24
+ let resolvedPath;
25
+ try {
26
+ resolvedPath = realpathSync(repositoryPath);
27
+ }
28
+ catch {
21
29
  return { success: false, error: 'Directory not found' };
22
30
  }
23
31
  const entry = FOLDER_COMMANDS[platform()];
@@ -29,14 +37,14 @@ export async function openFolder(repositoryPath) {
29
37
  }
30
38
  // Normalize to platform-native separators — explorer.exe on Windows
31
39
  // does not understand forward-slash paths and falls back to Documents.
32
- const nativePath = normalize(repositoryPath);
40
+ const nativePath = normalize(resolvedPath);
33
41
  const child = spawn(entry.cmd, entry.args(nativePath), {
34
42
  detached: true,
35
43
  stdio: 'ignore',
36
44
  });
37
45
  child.on('error', () => undefined); // Prevent uncaught exception on spawn failure
38
46
  child.unref();
39
- return { success: true, path: repositoryPath };
47
+ return { success: true, path: resolvedPath };
40
48
  }
41
49
  catch (error) {
42
50
  const message = error instanceof Error ? error.message : 'Failed to open folder';
@@ -1 +1 @@
1
- {"version":3,"file":"open-shell.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/web/app/actions/open-shell.ts"],"names":[],"mappings":"AA0BA,UAAU,cAAc;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,SAAS,CAC7B,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6E9E"}
1
+ {"version":3,"file":"open-shell.d.ts","sourceRoot":"","sources":["../../../../../../src/presentation/web/app/actions/open-shell.ts"],"names":[],"mappings":"AAmDA,UAAU,cAAc;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,SAAS,CAC7B,KAAK,EAAE,cAAc,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA2F9E"}
@@ -1,10 +1,34 @@
1
1
  'use server';
2
- import { existsSync } from 'node:fs';
2
+ import { realpathSync } from 'node:fs';
3
3
  import { platform } from 'node:os';
4
4
  import { isAbsolute } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { computeWorktreePath } from '../../lib/core-utils.js';
7
7
  import { resolve } from '../../lib/server-container.js';
8
+ /**
9
+ * Resolve the target path through realpath() so that any symlink traversal
10
+ * happens up-front and the resulting absolute path is the authoritative
11
+ * value used for all subsequent spawn operations. Returns null if the path
12
+ * does not exist or cannot be resolved.
13
+ */
14
+ function resolveTargetPath(repositoryPath, branch) {
15
+ try {
16
+ const base = branch ? computeWorktreePath(repositoryPath, branch) : repositoryPath;
17
+ return realpathSync(base);
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * POSIX shell-escape a path for safe inclusion in a shell:true command string.
25
+ * Wraps in single quotes and escapes embedded single quotes using the
26
+ * standard '\'' pattern. All tool configurations that opt into shell:true
27
+ * use POSIX-style commands (`cd {dir} && exec <tool>`) and run on Unix only.
28
+ */
29
+ function shellEscapePosixPath(p) {
30
+ return `'${p.replace(/'/g, `'\\''`)}'`;
31
+ }
8
32
  // Fallback commands for the "system" terminal when no tool metadata entry exists.
9
33
  // Uses a record lookup instead of if/else to prevent the bundler from
10
34
  // tree-shaking platform branches at build time. Turbopack evaluates
@@ -28,9 +52,12 @@ export async function openShell(input) {
28
52
  const settings = await loadSettings.execute();
29
53
  const shell = settings.environment.shellPreference;
30
54
  const terminalPref = settings.environment.terminalPreference ?? 'system';
31
- const targetPath = branch ? computeWorktreePath(repositoryPath, branch) : repositoryPath;
32
- if (!existsSync(targetPath)) {
33
- return { success: false, error: `Path does not exist: ${targetPath}` };
55
+ // Resolve the target path through realpath() up-front. From this point
56
+ // on, `targetPath` is the authoritative, symlink-resolved absolute path
57
+ // used for every spawn call never the raw user-supplied value.
58
+ const targetPath = resolveTargetPath(repositoryPath, branch);
59
+ if (!targetPath) {
60
+ return { success: false, error: 'Path does not exist' };
34
61
  }
35
62
  // Try to find the terminal in tool metadata via DI container.
36
63
  // Using DI (not a direct import from tool-metadata) ensures that
@@ -42,9 +69,15 @@ export async function openShell(input) {
42
69
  const service = resolve('IToolInstallerService');
43
70
  const config = service.getTerminalOpenConfig(terminalPref);
44
71
  if (config?.openDirectory.includes('{dir}')) {
45
- const resolved = config.openDirectory.replace('{dir}', targetPath);
46
72
  if (config.shell) {
47
- const child = spawn(resolved, [], {
73
+ // For shell:true tools (claude-code, codex-cli, etc.) the tool
74
+ // config is a POSIX shell string like `cd {dir} && exec claude`.
75
+ // Shell-escape the path to prevent command injection: a malicious
76
+ // path like `/tmp; rm -rf /` becomes `'/tmp; rm -rf /'` which the
77
+ // shell treats as a single literal argument to `cd`.
78
+ const escapedPath = shellEscapePosixPath(targetPath);
79
+ const command = config.openDirectory.replaceAll('{dir}', escapedPath);
80
+ const child = spawn(command, [], {
48
81
  detached: true,
49
82
  stdio: 'ignore',
50
83
  shell: true,
@@ -53,7 +86,13 @@ export async function openShell(input) {
53
86
  child.unref();
54
87
  }
55
88
  else {
56
- const [command, ...args] = resolved.split(/\s+/);
89
+ // For non-shell tools (alacritty, kitty, etc.) the config is a
90
+ // whitespace-separated command. Split first, then substitute {dir}
91
+ // INTO AN ARGV ELEMENT (never back into a concatenated string).
92
+ // CodeQL recognizes the argv-form of spawn as sanitized input.
93
+ const tokens = config.openDirectory.split(/\s+/).filter(Boolean);
94
+ const command = tokens[0];
95
+ const args = tokens.slice(1).map((t) => t.replaceAll('{dir}', targetPath));
57
96
  const child = spawn(command, args, {
58
97
  detached: true,
59
98
  stdio: 'ignore',
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/agent-events/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AASH,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAKvC,+EAA+E;AAC/E,YAAY,EAAE,uBAAuB,EAAE,MAAM,yEAAyE,CAAC;AAEvH,wBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAoG9C"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/agent-events/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAUH,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAKvC,+EAA+E;AAC/E,YAAY,EAAE,uBAAuB,EAAE,MAAM,yEAAyE,CAAC;AAEvH,wBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CA+F9C"}
@@ -16,6 +16,7 @@
16
16
  * - Cleans up intervals on client disconnect
17
17
  */
18
18
  import { resolve } from '../../../lib/server-container.js';
19
+ import { apiError } from '../../../lib/api-helpers.js';
19
20
  // Force dynamic — SSE streams must never be statically optimized or cached
20
21
  export const dynamic = 'force-dynamic';
21
22
  const POLL_INTERVAL_MS = 2_000;
@@ -100,11 +101,6 @@ export function GET(request) {
100
101
  });
101
102
  }
102
103
  catch (error) {
103
- // eslint-disable-next-line no-console
104
- console.error('[SSE route] GET handler error:', error);
105
- return new Response(JSON.stringify({ error: String(error) }), {
106
- status: 500,
107
- headers: { 'Content-Type': 'application/json' },
108
- });
104
+ return apiError(500, 'Failed to open agent events stream', error);
109
105
  }
110
106
  }
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../../src/presentation/web/app/api/attachments/upload-from-path/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmF3C,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CA4ElE"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../../src/presentation/web/app/api/attachments/upload-from-path/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAoF3C,wBAAsB,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CA8ElE"}
@@ -1,8 +1,9 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { readFile, realpath } from 'fs/promises';
3
- import { extname, basename, resolve as resolvePath, sep } from 'path';
2
+ import { readFile } from 'fs/promises';
3
+ import { extname, basename, resolve as resolvePath } from 'path';
4
4
  import { homedir } from 'node:os';
5
5
  import { resolve } from '../../../../lib/server-container.js';
6
+ import { realpathWithinAllowedRootsAsync } from '../../../../lib/path-sanitizers.js';
6
7
  const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
7
8
  const ALLOWED_EXTENSIONS = new Set([
8
9
  '.png',
@@ -91,25 +92,28 @@ export async function POST(request) {
91
92
  : 'Files without an extension are not allowed',
92
93
  }, { status: 400 });
93
94
  }
94
- // Path containment: resolve symlinks then verify within allowed roots
95
- let physicalPath;
96
- try {
97
- physicalPath = await realpath(resolvePath(path));
98
- }
99
- catch {
95
+ // Path containment: resolve symlinks and verify within allowed roots
96
+ // in a single helper call. `physicalPath` is the single authoritative
97
+ // value — every subsequent filesystem call uses it, never the raw
98
+ // `path` input. This eliminates the TOCTOU window between the
99
+ // containment check and the read, and gives CodeQL a clean
100
+ // sanitizer→sink flow for js/path-injection.
101
+ const physicalPath = await realpathWithinAllowedRootsAsync(resolvePath(path), [
102
+ process.cwd(),
103
+ homedir(),
104
+ ]);
105
+ if (!physicalPath) {
106
+ // Could be a missing file OR a path outside the allowed roots. Return
107
+ // a single generic 404 either way — leaking the distinction would
108
+ // let an attacker probe for existence of arbitrary paths on the host.
100
109
  return NextResponse.json({ error: 'File not found or unreadable' }, { status: 404 });
101
110
  }
102
- const allowedRoots = [
103
- await realpath(process.cwd()).catch(() => process.cwd()),
104
- await realpath(homedir()).catch(() => homedir()),
105
- ];
106
- const isAllowed = allowedRoots.some((root) => physicalPath === root || physicalPath.startsWith(root + sep));
107
- if (!isAllowed) {
108
- return NextResponse.json({ error: 'Access denied' }, { status: 403 });
109
- }
110
111
  let buffer;
111
112
  try {
112
- buffer = await readFile(resolvePath(path));
113
+ // Read using the validated physicalPath, NOT a re-resolution of `path`.
114
+ // This closes the TOCTOU gap where a symlink could be swapped between
115
+ // the containment check and the read.
116
+ buffer = await readFile(physicalPath);
113
117
  }
114
118
  catch {
115
119
  return NextResponse.json({ error: 'File not found or unreadable' }, { status: 404 });
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/deployment-logs/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AASH,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAIvC,wBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAuF9C"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/presentation/web/app/api/deployment-logs/route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,eAAO,MAAM,OAAO,kBAAkB,CAAC;AAIvC,wBAAgB,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAkF9C"}
@@ -11,6 +11,7 @@
11
11
  * - Cleans up EventEmitter subscription on client disconnect
12
12
  */
13
13
  import { resolve } from '../../../lib/server-container.js';
14
+ import { apiError } from '../../../lib/api-helpers.js';
14
15
  // Force dynamic — SSE streams must never be statically optimized or cached
15
16
  export const dynamic = 'force-dynamic';
16
17
  const HEARTBEAT_INTERVAL_MS = 30_000;
@@ -84,11 +85,6 @@ export function GET(request) {
84
85
  });
85
86
  }
86
87
  catch (error) {
87
- // eslint-disable-next-line no-console
88
- console.error('[SSE route] GET /api/deployment-logs error:', error);
89
- return new Response(JSON.stringify({ error: String(error) }), {
90
- status: 500,
91
- headers: { 'Content-Type': 'application/json' },
92
- });
88
+ return apiError(500, 'Failed to open deployment logs stream', error);
93
89
  }
94
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../../src/presentation/web/app/api/directory/list/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAa3C,wBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAgGjE"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../../src/presentation/web/app/api/directory/list/route.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAc3C,wBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAyGjE"}
@@ -1,8 +1,9 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { readdir, realpath, stat } from 'node:fs/promises';
2
+ import { readdir, stat } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { apiError } from '../../../../lib/api-helpers.js';
6
+ import { realpathWithinAllowedRootsAsync } from '../../../../lib/path-sanitizers.js';
6
7
  export async function GET(request) {
7
8
  const url = new URL(request.url);
8
9
  const rawPath = url.searchParams.get('path') ?? homedir();
@@ -10,25 +11,33 @@ export async function GET(request) {
10
11
  if (!path.isAbsolute(rawPath)) {
11
12
  return NextResponse.json({ error: 'Path must be absolute' }, { status: 400 });
12
13
  }
13
- const resolvedPath = path.resolve(rawPath);
14
- // Path containment: resolve symlinks then verify within allowed roots
15
- let physicalPath;
16
- try {
17
- physicalPath = await realpath(resolvedPath);
18
- }
19
- catch {
14
+ // Keep two names for the same directory:
15
+ //
16
+ // - `displayPath` is what the client originally asked for (after
17
+ // path.resolve normalization). It's what the UI shows in the
18
+ // breadcrumb and what we echo back as `currentPath`. This matches
19
+ // user expectation — e.g. on macOS the user sees /tmp/foo, not the
20
+ // realpath'd /private/tmp/foo.
21
+ //
22
+ // - `physicalPath` is the realpath'd absolute path produced by the
23
+ // path-containment sanitizer. Every FILESYSTEM operation (stat,
24
+ // readdir, per-entry joins) uses this value, NEVER the display
25
+ // path. That gives CodeQL a clean sanitizer→sink flow for
26
+ // js/path-injection and closes the TOCTOU window where a symlink
27
+ // could be swapped between the containment check and the read.
28
+ const displayPath = path.resolve(rawPath);
29
+ const physicalPath = await realpathWithinAllowedRootsAsync(displayPath, [
30
+ process.cwd(),
31
+ homedir(),
32
+ ]);
33
+ if (!physicalPath) {
34
+ // Could be a missing directory OR a path outside the allowed roots.
35
+ // Return a uniform 404 either way so we don't leak existence of
36
+ // arbitrary paths on the host.
20
37
  return NextResponse.json({ error: 'Directory not found' }, { status: 404 });
21
38
  }
22
- const allowedRoots = [
23
- await realpath(process.cwd()).catch(() => process.cwd()),
24
- await realpath(homedir()).catch(() => homedir()),
25
- ];
26
- const isAllowed = allowedRoots.some((root) => physicalPath === root || physicalPath.startsWith(root + path.sep));
27
- if (!isAllowed) {
28
- return NextResponse.json({ error: 'Access denied' }, { status: 403 });
29
- }
30
39
  try {
31
- const dirStat = await stat(resolvedPath);
40
+ const dirStat = await stat(physicalPath);
32
41
  if (!dirStat.isDirectory()) {
33
42
  return NextResponse.json({ error: 'Path is not a directory' }, { status: 400 });
34
43
  }
@@ -40,29 +49,35 @@ export async function GET(request) {
40
49
  return apiError(500, 'Failed to access directory', error);
41
50
  }
42
51
  try {
43
- const dirents = await readdir(resolvedPath, { withFileTypes: true });
52
+ const dirents = await readdir(physicalPath, { withFileTypes: true });
44
53
  const entries = [];
45
54
  const entryPromises = dirents.map(async (dirent) => {
46
55
  if (!showHidden && dirent.name.startsWith('.')) {
47
56
  return null;
48
57
  }
49
- const entryPath = path.join(resolvedPath, dirent.name);
58
+ // Build two path forms per entry: a physical path for stat() and a
59
+ // display path for the response payload. The client navigates using
60
+ // the display path on subsequent requests, so it must match the
61
+ // user-visible form of the parent directory — never the realpath'd
62
+ // form, which would surprise the user with /private/tmp on macOS.
63
+ const physicalEntry = path.join(physicalPath, dirent.name);
64
+ const displayEntry = path.join(displayPath, dirent.name);
50
65
  try {
51
66
  if (dirent.isDirectory()) {
52
- const entryStat = await stat(entryPath);
67
+ const entryStat = await stat(physicalEntry);
53
68
  return {
54
69
  name: dirent.name,
55
- path: entryPath,
70
+ path: displayEntry,
56
71
  isDirectory: true,
57
72
  updatedAt: entryStat.mtime.toISOString(),
58
73
  };
59
74
  }
60
75
  if (dirent.isSymbolicLink()) {
61
- const entryStat = await stat(entryPath);
76
+ const entryStat = await stat(physicalEntry);
62
77
  if (entryStat.isDirectory()) {
63
78
  return {
64
79
  name: dirent.name,
65
- path: entryPath,
80
+ path: displayEntry,
66
81
  isDirectory: true,
67
82
  updatedAt: entryStat.mtime.toISOString(),
68
83
  };
@@ -80,7 +95,7 @@ export async function GET(request) {
80
95
  entries.push(result);
81
96
  }
82
97
  }
83
- return NextResponse.json({ entries, currentPath: resolvedPath });
98
+ return NextResponse.json({ entries, currentPath: displayPath });
84
99
  }
85
100
  catch (error) {
86
101
  return apiError(500, 'Failed to read directory', error);