@storyshelf/core 0.3.1 → 0.5.3

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 (393) hide show
  1. package/README.md +38 -31
  2. package/dist/adapter/capture-queue.d.mts +29 -1
  3. package/dist/adapter/capture-runner.d.mts +18 -2
  4. package/dist/adapter/capture-runner.mjs +12 -1
  5. package/dist/adapter/capture-runner.mjs.map +1 -0
  6. package/dist/adapter/database.d.mts +67 -17
  7. package/dist/adapter/database.mjs +22 -55
  8. package/dist/adapter/database.mjs.map +1 -1
  9. package/dist/adapter/email-sender.d.mts +27 -0
  10. package/dist/adapter/email-sender.mjs +4 -0
  11. package/dist/adapter/metadata.d.mts +21 -15
  12. package/dist/adapter/notifier.d.mts +7 -0
  13. package/dist/adapter/notifier.mjs +6 -0
  14. package/dist/adapter/setup.d.mts +61 -0
  15. package/dist/adapter/setup.mjs +29 -0
  16. package/dist/adapter/setup.mjs.map +1 -0
  17. package/dist/adapter/webhook-events.d.mts +29 -0
  18. package/dist/adapter/webhook-events.mjs +83 -0
  19. package/dist/adapter/webhook-events.mjs.map +1 -0
  20. package/dist/adapters/notifier/events.d.mts +26 -0
  21. package/dist/adapters/notifier/events.mjs +66 -0
  22. package/dist/adapters/notifier/events.mjs.map +1 -0
  23. package/dist/adapters/notifier/format.d.mts +14 -0
  24. package/dist/adapters/notifier/format.mjs +96 -0
  25. package/dist/adapters/notifier/format.mjs.map +1 -0
  26. package/dist/adapters/notifier/provider.d.mts +32 -0
  27. package/dist/adapters/notifier/types.d.mts +56 -0
  28. package/dist/adapters/setup-runner.d.mts +23 -0
  29. package/dist/adapters/setup-runner.mjs +149 -0
  30. package/dist/adapters/setup-runner.mjs.map +1 -0
  31. package/dist/adapters/validate.d.mts +30 -0
  32. package/dist/adapters/validate.mjs +313 -0
  33. package/dist/adapters/validate.mjs.map +1 -0
  34. package/dist/auth.d.mts +94 -0
  35. package/dist/auth.mjs +4 -0
  36. package/dist/brand-packs.d.mts +16 -0
  37. package/dist/brand-packs.mjs +130 -0
  38. package/dist/brand-packs.mjs.map +1 -0
  39. package/dist/capture/adapter.d.mts +51 -1
  40. package/dist/capture/adapter.mjs.map +1 -1
  41. package/dist/capture/affected.d.mts +16 -0
  42. package/dist/capture/affected.mjs +64 -0
  43. package/dist/capture/affected.mjs.map +1 -0
  44. package/dist/capture/diff-record.mjs +30 -0
  45. package/dist/capture/diff-record.mjs.map +1 -0
  46. package/dist/capture/dispatch.d.mts +36 -0
  47. package/dist/capture/dispatch.mjs +88 -31
  48. package/dist/capture/dispatch.mjs.map +1 -1
  49. package/dist/capture/guards.mjs +31 -0
  50. package/dist/capture/guards.mjs.map +1 -0
  51. package/dist/capture/orchestrator.d.mts +41 -6
  52. package/dist/capture/orchestrator.mjs +151 -28
  53. package/dist/capture/orchestrator.mjs.map +1 -1
  54. package/dist/capture/pipeline.d.mts +30 -16
  55. package/dist/capture/pipeline.mjs +143 -39
  56. package/dist/capture/pipeline.mjs.map +1 -1
  57. package/dist/capture/recovery.d.mts +21 -0
  58. package/dist/capture/recovery.mjs +83 -0
  59. package/dist/capture/recovery.mjs.map +1 -0
  60. package/dist/capture/rediff.d.mts +35 -0
  61. package/dist/capture/rediff.mjs +77 -0
  62. package/dist/capture/rediff.mjs.map +1 -0
  63. package/dist/capture/sizing.d.mts +58 -0
  64. package/dist/capture/sizing.mjs +119 -0
  65. package/dist/capture/sizing.mjs.map +1 -0
  66. package/dist/capture/skip-checks.mjs +3 -3
  67. package/dist/capture/skip-checks.mjs.map +1 -1
  68. package/dist/capture/statics.d.mts +38 -0
  69. package/dist/capture/statics.mjs +38 -4
  70. package/dist/capture/statics.mjs.map +1 -1
  71. package/dist/capture/status-fanout.mjs +7 -2
  72. package/dist/capture/status-fanout.mjs.map +1 -1
  73. package/dist/capture/storybook.d.mts +31 -2
  74. package/dist/capture/storybook.mjs +77 -2
  75. package/dist/capture/storybook.mjs.map +1 -1
  76. package/dist/capture.d.mts +10 -4
  77. package/dist/capture.mjs +7 -2
  78. package/dist/config.d.mts +121 -4
  79. package/dist/config.mjs +55 -8
  80. package/dist/config.mjs.map +1 -1
  81. package/dist/index.d.mts +5 -56
  82. package/dist/index.mjs +4 -284
  83. package/dist/logger.d.mts +26 -17
  84. package/dist/logger.mjs +10 -5
  85. package/dist/logger.mjs.map +1 -1
  86. package/dist/models/baseline-status.d.mts +29 -0
  87. package/dist/models/baseline-status.mjs +26 -0
  88. package/dist/models/baseline-status.mjs.map +1 -0
  89. package/dist/models/baseline.d.mts +33 -0
  90. package/dist/models/baseline.mjs +35 -13
  91. package/dist/models/baseline.mjs.map +1 -1
  92. package/dist/models/build.d.mts +87 -0
  93. package/dist/models/build.mjs +83 -35
  94. package/dist/models/build.mjs.map +1 -1
  95. package/dist/models/capture-attempt.d.mts +40 -0
  96. package/dist/models/capture-attempt.mjs +85 -0
  97. package/dist/models/capture-attempt.mjs.map +1 -0
  98. package/dist/models/capture-log.d.mts +21 -0
  99. package/dist/models/capture-log.mjs +44 -0
  100. package/dist/models/capture-log.mjs.map +1 -0
  101. package/dist/models/comment.d.mts +29 -0
  102. package/dist/models/comment.mjs +11 -8
  103. package/dist/models/comment.mjs.map +1 -1
  104. package/dist/models/content-ref.d.mts +21 -0
  105. package/dist/models/content-ref.mjs +49 -0
  106. package/dist/models/content-ref.mjs.map +1 -0
  107. package/dist/models/credentials.d.mts +36 -0
  108. package/dist/models/credentials.mjs +103 -0
  109. package/dist/models/credentials.mjs.map +1 -0
  110. package/dist/models/label.d.mts +58 -0
  111. package/dist/models/label.mjs +32 -27
  112. package/dist/models/label.mjs.map +1 -1
  113. package/dist/models/member.d.mts +34 -0
  114. package/dist/models/member.mjs +20 -11
  115. package/dist/models/member.mjs.map +1 -1
  116. package/dist/models/notification-channel.d.mts +52 -0
  117. package/dist/models/notification-channel.mjs +93 -0
  118. package/dist/models/notification-channel.mjs.map +1 -0
  119. package/dist/models/notification-subscription.d.mts +44 -0
  120. package/dist/models/notification-subscription.mjs +89 -0
  121. package/dist/models/notification-subscription.mjs.map +1 -0
  122. package/dist/models/project-group-mapping.d.mts +30 -0
  123. package/dist/models/project-group-mapping.mjs +46 -0
  124. package/dist/models/project-group-mapping.mjs.map +1 -0
  125. package/dist/models/project.d.mts +54 -0
  126. package/dist/models/project.mjs +15 -11
  127. package/dist/models/project.mjs.map +1 -1
  128. package/dist/models/snapshot.d.mts +58 -0
  129. package/dist/models/snapshot.mjs +13 -7
  130. package/dist/models/snapshot.mjs.map +1 -1
  131. package/dist/models/status-config.d.mts +38 -0
  132. package/dist/models/status-config.mjs +12 -11
  133. package/dist/models/status-config.mjs.map +1 -1
  134. package/dist/models/token.d.mts +44 -0
  135. package/dist/models/token.mjs +18 -15
  136. package/dist/models/token.mjs.map +1 -1
  137. package/dist/models/user.d.mts +42 -0
  138. package/dist/models/user.mjs +65 -0
  139. package/dist/models/user.mjs.map +1 -0
  140. package/dist/models/webhook.d.mts +50 -0
  141. package/dist/models/webhook.mjs +12 -10
  142. package/dist/models/webhook.mjs.map +1 -1
  143. package/dist/models.d.mts +22 -0
  144. package/dist/models.mjs +23 -0
  145. package/dist/paths.mjs +29 -0
  146. package/dist/paths.mjs.map +1 -1
  147. package/dist/retention.d.mts +45 -0
  148. package/dist/retention.mjs +145 -0
  149. package/dist/retention.mjs.map +1 -0
  150. package/dist/schema/baseline.d.mts +1 -0
  151. package/dist/schema/build.d.mts +8 -0
  152. package/dist/schema/capture-attempt.d.mts +38 -0
  153. package/dist/schema/comment.d.mts +1 -1
  154. package/dist/schema/content-ref.d.mts +13 -0
  155. package/dist/schema/member.d.mts +2 -0
  156. package/dist/schema/notification.d.mts +35 -0
  157. package/dist/schema/project-group-mapping.d.mts +15 -0
  158. package/dist/schema/project.d.mts +4 -0
  159. package/dist/schema/snapshot.d.mts +7 -0
  160. package/dist/schema/token.d.mts +2 -0
  161. package/dist/schema/user.d.mts +11 -1
  162. package/dist/schema.d.mts +9 -46
  163. package/dist/schema.mjs +1 -55
  164. package/dist/test-helpers/create-project.d.mts +15 -0
  165. package/dist/test-helpers/create-project.mjs +28 -0
  166. package/dist/test-helpers/create-project.mjs.map +1 -0
  167. package/dist/test-helpers/database-contract.d.mts +13 -0
  168. package/dist/test-helpers/database-contract.mjs +58 -0
  169. package/dist/test-helpers/database-contract.mjs.map +1 -0
  170. package/dist/test-helpers/fake-adapters.d.mts +14 -0
  171. package/dist/test-helpers/fake-adapters.mjs +20 -0
  172. package/dist/test-helpers/fake-adapters.mjs.map +1 -0
  173. package/dist/test-helpers/fake-database.d.mts +11 -0
  174. package/dist/test-helpers/fake-database.mjs +104 -0
  175. package/dist/test-helpers/fake-database.mjs.map +1 -0
  176. package/dist/test-helpers/fake-storage.d.mts +14 -0
  177. package/dist/test-helpers/fake-storage.mjs +49 -0
  178. package/dist/test-helpers/fake-storage.mjs.map +1 -0
  179. package/dist/test-helpers/fake-tables.mjs +276 -0
  180. package/dist/test-helpers/fake-tables.mjs.map +1 -0
  181. package/dist/test-helpers/git-contract.d.mts +11 -0
  182. package/dist/test-helpers/git-contract.mjs +34 -0
  183. package/dist/test-helpers/git-contract.mjs.map +1 -0
  184. package/dist/test-helpers/notifier-contract.d.mts +11 -0
  185. package/dist/test-helpers/notifier-contract.mjs +34 -0
  186. package/dist/test-helpers/notifier-contract.mjs.map +1 -0
  187. package/dist/test-helpers/queue-contract.d.mts +11 -0
  188. package/dist/test-helpers/queue-contract.mjs +64 -0
  189. package/dist/test-helpers/queue-contract.mjs.map +1 -0
  190. package/dist/test-helpers/runner-contract.d.mts +11 -0
  191. package/dist/test-helpers/runner-contract.mjs +32 -0
  192. package/dist/test-helpers/runner-contract.mjs.map +1 -0
  193. package/dist/test-helpers/sql-chunks.mjs +138 -0
  194. package/dist/test-helpers/sql-chunks.mjs.map +1 -0
  195. package/dist/test-helpers/storage-contract.d.mts +11 -0
  196. package/dist/test-helpers/storage-contract.mjs +87 -0
  197. package/dist/test-helpers/storage-contract.mjs.map +1 -0
  198. package/dist/test-helpers.d.mts +13 -0
  199. package/dist/test-helpers.mjs +14 -0
  200. package/dist/tracing.mjs +63 -0
  201. package/dist/tracing.mjs.map +1 -0
  202. package/dist/types.d.mts +38 -5
  203. package/dist/types.mjs +18 -5
  204. package/dist/types.mjs.map +1 -1
  205. package/dist/urls.d.mts +12 -1
  206. package/dist/urls.mjs +22 -3
  207. package/dist/urls.mjs.map +1 -1
  208. package/dist/utils/encrypt.d.mts +48 -0
  209. package/dist/utils/encrypt.mjs +67 -13
  210. package/dist/utils/encrypt.mjs.map +1 -1
  211. package/dist/utils/hash.d.mts +17 -0
  212. package/dist/utils/hash.mjs +8 -2
  213. package/dist/utils/hash.mjs.map +1 -1
  214. package/dist/utils/http.d.mts +58 -0
  215. package/dist/utils/http.mjs +147 -0
  216. package/dist/utils/http.mjs.map +1 -0
  217. package/dist/utils/mime.d.mts +9 -0
  218. package/dist/utils/mime.mjs +33 -0
  219. package/dist/utils/mime.mjs.map +1 -0
  220. package/dist/utils/path-security.d.mts +12 -0
  221. package/dist/utils/path-security.mjs +24 -0
  222. package/dist/utils/path-security.mjs.map +1 -0
  223. package/dist/utils/redact.d.mts +17 -0
  224. package/dist/utils/redact.mjs +42 -0
  225. package/dist/utils/redact.mjs.map +1 -0
  226. package/dist/utils/timing.d.mts +19 -0
  227. package/dist/utils/timing.mjs +46 -0
  228. package/dist/utils/timing.mjs.map +1 -0
  229. package/dist/utils/ulid.d.mts +10 -0
  230. package/dist/utils/ulid.mjs +8 -8
  231. package/dist/utils/ulid.mjs.map +1 -1
  232. package/dist/utils.d.mts +12 -0
  233. package/dist/utils.mjs +13 -0
  234. package/package.json +69 -28
  235. package/dist/adapter/auth.d.mts +0 -50
  236. package/dist/adapter/auth.mjs +0 -10
  237. package/dist/adapter/auth.mjs.map +0 -1
  238. package/dist/adapter/init.d.mts +0 -58
  239. package/dist/adapter/init.mjs +0 -93
  240. package/dist/adapter/init.mjs.map +0 -1
  241. package/dist/adapters/webhook-events.mjs +0 -55
  242. package/dist/adapters/webhook-events.mjs.map +0 -1
  243. package/dist/ddl.d.mts +0 -8
  244. package/dist/ddl.mjs +0 -158
  245. package/dist/ddl.mjs.map +0 -1
  246. package/dist/index.mjs.map +0 -1
  247. package/dist/middleware/auth-gate.mjs +0 -36
  248. package/dist/middleware/auth-gate.mjs.map +0 -1
  249. package/dist/middleware/csrf.mjs +0 -40
  250. package/dist/middleware/csrf.mjs.map +0 -1
  251. package/dist/middleware/rate-limit.mjs +0 -39
  252. package/dist/middleware/rate-limit.mjs.map +0 -1
  253. package/dist/middleware/request-log.mjs +0 -28
  254. package/dist/middleware/request-log.mjs.map +0 -1
  255. package/dist/middleware/store-scope.mjs +0 -29
  256. package/dist/middleware/store-scope.mjs.map +0 -1
  257. package/dist/pages/build-detail.mjs +0 -339
  258. package/dist/pages/build-detail.mjs.map +0 -1
  259. package/dist/pages/build-diff-comments.mjs +0 -113
  260. package/dist/pages/build-diff-comments.mjs.map +0 -1
  261. package/dist/pages/build-diff-header.mjs +0 -163
  262. package/dist/pages/build-diff-header.mjs.map +0 -1
  263. package/dist/pages/build-diff-nav.mjs +0 -66
  264. package/dist/pages/build-diff-nav.mjs.map +0 -1
  265. package/dist/pages/build-diff-viewer.mjs +0 -197
  266. package/dist/pages/build-diff-viewer.mjs.map +0 -1
  267. package/dist/pages/build-diff.mjs +0 -90
  268. package/dist/pages/build-diff.mjs.map +0 -1
  269. package/dist/pages/compute-jobs.mjs +0 -198
  270. package/dist/pages/compute-jobs.mjs.map +0 -1
  271. package/dist/pages/label-detail.mjs +0 -228
  272. package/dist/pages/label-detail.mjs.map +0 -1
  273. package/dist/pages/login.mjs +0 -78
  274. package/dist/pages/login.mjs.map +0 -1
  275. package/dist/pages/project-builds.mjs +0 -231
  276. package/dist/pages/project-builds.mjs.map +0 -1
  277. package/dist/pages/project-create.mjs +0 -169
  278. package/dist/pages/project-create.mjs.map +0 -1
  279. package/dist/pages/project-settings.mjs +0 -102
  280. package/dist/pages/project-settings.mjs.map +0 -1
  281. package/dist/pages/projects.mjs +0 -184
  282. package/dist/pages/projects.mjs.map +0 -1
  283. package/dist/pages/root.mjs +0 -94
  284. package/dist/pages/root.mjs.map +0 -1
  285. package/dist/pages/settings-general.mjs +0 -192
  286. package/dist/pages/settings-general.mjs.map +0 -1
  287. package/dist/pages/settings-labels.mjs +0 -150
  288. package/dist/pages/settings-labels.mjs.map +0 -1
  289. package/dist/pages/settings-members.mjs +0 -125
  290. package/dist/pages/settings-members.mjs.map +0 -1
  291. package/dist/pages/settings-status.mjs +0 -152
  292. package/dist/pages/settings-status.mjs.map +0 -1
  293. package/dist/pages/settings-tests.mjs +0 -149
  294. package/dist/pages/settings-tests.mjs.map +0 -1
  295. package/dist/pages/settings-tokens.mjs +0 -96
  296. package/dist/pages/settings-tokens.mjs.map +0 -1
  297. package/dist/pages/settings-webhooks.mjs +0 -150
  298. package/dist/pages/settings-webhooks.mjs.map +0 -1
  299. package/dist/pages/storybook.mjs +0 -96
  300. package/dist/pages/storybook.mjs.map +0 -1
  301. package/dist/retention/purge.mjs +0 -67
  302. package/dist/retention/purge.mjs.map +0 -1
  303. package/dist/routers/admin.mjs +0 -42
  304. package/dist/routers/admin.mjs.map +0 -1
  305. package/dist/routers/assets.mjs +0 -25
  306. package/dist/routers/assets.mjs.map +0 -1
  307. package/dist/routers/auth.mjs +0 -78
  308. package/dist/routers/auth.mjs.map +0 -1
  309. package/dist/routers/builds.handlers.mjs +0 -184
  310. package/dist/routers/builds.handlers.mjs.map +0 -1
  311. package/dist/routers/builds.mjs +0 -185
  312. package/dist/routers/builds.mjs.map +0 -1
  313. package/dist/routers/comments.mjs +0 -90
  314. package/dist/routers/comments.mjs.map +0 -1
  315. package/dist/routers/health.mjs +0 -137
  316. package/dist/routers/health.mjs.map +0 -1
  317. package/dist/routers/helpers.mjs +0 -82
  318. package/dist/routers/helpers.mjs.map +0 -1
  319. package/dist/routers/htmx.mjs +0 -20
  320. package/dist/routers/htmx.mjs.map +0 -1
  321. package/dist/routers/labels.mjs +0 -110
  322. package/dist/routers/labels.mjs.map +0 -1
  323. package/dist/routers/media.mjs +0 -55
  324. package/dist/routers/media.mjs.map +0 -1
  325. package/dist/routers/members.mjs +0 -102
  326. package/dist/routers/members.mjs.map +0 -1
  327. package/dist/routers/projects.mjs +0 -122
  328. package/dist/routers/projects.mjs.map +0 -1
  329. package/dist/routers/schemas.mjs +0 -224
  330. package/dist/routers/schemas.mjs.map +0 -1
  331. package/dist/routers/settings-general.mjs +0 -95
  332. package/dist/routers/settings-general.mjs.map +0 -1
  333. package/dist/routers/settings-labels.mjs +0 -49
  334. package/dist/routers/settings-labels.mjs.map +0 -1
  335. package/dist/routers/settings-members.mjs +0 -30
  336. package/dist/routers/settings-members.mjs.map +0 -1
  337. package/dist/routers/settings-status.mjs +0 -73
  338. package/dist/routers/settings-status.mjs.map +0 -1
  339. package/dist/routers/settings-tokens.mjs +0 -40
  340. package/dist/routers/settings-tokens.mjs.map +0 -1
  341. package/dist/routers/settings-webhooks.mjs +0 -63
  342. package/dist/routers/settings-webhooks.mjs.map +0 -1
  343. package/dist/routers/settings.handlers.mjs +0 -93
  344. package/dist/routers/settings.handlers.mjs.map +0 -1
  345. package/dist/routers/settings.mjs +0 -23
  346. package/dist/routers/settings.mjs.map +0 -1
  347. package/dist/routers/snapshots.mjs +0 -144
  348. package/dist/routers/snapshots.mjs.map +0 -1
  349. package/dist/routers/status-configs.mjs +0 -101
  350. package/dist/routers/status-configs.mjs.map +0 -1
  351. package/dist/routers/storybook.mjs +0 -120
  352. package/dist/routers/storybook.mjs.map +0 -1
  353. package/dist/routers/tokens.mjs +0 -81
  354. package/dist/routers/tokens.mjs.map +0 -1
  355. package/dist/routers/ui.mjs +0 -165
  356. package/dist/routers/ui.mjs.map +0 -1
  357. package/dist/routers/webhooks.mjs +0 -90
  358. package/dist/routers/webhooks.mjs.map +0 -1
  359. package/dist/schema/baseline.mjs +0 -22
  360. package/dist/schema/baseline.mjs.map +0 -1
  361. package/dist/schema/build.mjs +0 -29
  362. package/dist/schema/build.mjs.map +0 -1
  363. package/dist/schema/comment.mjs +0 -26
  364. package/dist/schema/comment.mjs.map +0 -1
  365. package/dist/schema/label.mjs +0 -30
  366. package/dist/schema/label.mjs.map +0 -1
  367. package/dist/schema/member.mjs +0 -19
  368. package/dist/schema/member.mjs.map +0 -1
  369. package/dist/schema/project.mjs +0 -25
  370. package/dist/schema/project.mjs.map +0 -1
  371. package/dist/schema/snapshot.mjs +0 -34
  372. package/dist/schema/snapshot.mjs.map +0 -1
  373. package/dist/schema/status-config.mjs +0 -20
  374. package/dist/schema/status-config.mjs.map +0 -1
  375. package/dist/schema/token.mjs +0 -19
  376. package/dist/schema/token.mjs.map +0 -1
  377. package/dist/schema/user.mjs +0 -19
  378. package/dist/schema/user.mjs.map +0 -1
  379. package/dist/schema/webhook.mjs +0 -20
  380. package/dist/schema/webhook.mjs.map +0 -1
  381. package/dist/schema.mjs.map +0 -1
  382. package/dist/store.mjs +0 -20
  383. package/dist/store.mjs.map +0 -1
  384. package/dist/ui/document.mjs +0 -348
  385. package/dist/ui/document.mjs.map +0 -1
  386. package/dist/ui/feedback.mjs +0 -24
  387. package/dist/ui/feedback.mjs.map +0 -1
  388. package/dist/ui/forms.mjs +0 -117
  389. package/dist/ui/forms.mjs.map +0 -1
  390. package/dist/ui/styles.mjs +0 -174
  391. package/dist/ui/styles.mjs.map +0 -1
  392. package/dist/ui/theme.mjs +0 -44
  393. package/dist/ui/theme.mjs.map +0 -1
@@ -1,96 +0,0 @@
1
- import "node:path";
2
- import "node:url";
3
- import.meta.url;
4
- import { getStore } from "../store.mjs";
5
- //#region src/pages/storybook.tsx
6
- /**
7
- * Placeholder shown while a build's statics are not yet available.
8
- * Refreshes itself; once extraction lands, the route serves the live page.
9
- */
10
- function renderStorybookPreparingPage(project, build, slug) {
11
- const { ui } = getStore();
12
- const name = ui.name ?? "StoryShelf";
13
- return `<!doctype html>
14
- <html lang="en" data-theme="system">
15
- <head>
16
- <meta charset="utf-8" />
17
- <meta name="viewport" content="width=device-width, initial-scale=1" />
18
- <meta http-equiv="refresh" content="5" />
19
- <title>${escapeHtml(project.name)} Storybook · ${name}</title>
20
- <style>
21
- html, body { margin: 0; height: 100%; display: flex; flex-direction: column; font-family: system-ui, sans-serif; }
22
- header { display: flex; align-items: center; gap: .75rem; padding: .5rem 1rem; border-bottom: 1px solid var(--border, #ddd); }
23
- header a { color: inherit; text-decoration: none; }
24
- header .crumb { opacity: .7; }
25
- main { flex: 1; display: flex; align-items: center; justify-content: center; opacity: .7; }
26
- </style>
27
- </head>
28
- <body>
29
- <header>
30
- <a href="/">${name}</a>
31
- <span class="crumb">/</span>
32
- <a href="/projects/${escapeAttr(slug)}/builds">${escapeHtml(project.name)}</a>
33
- <span class="crumb">/</span>
34
- <span>${escapeHtml(build.gitBranch)}</span>
35
- <span class="crumb">· ${build.gitSha.slice(0, 7)}</span>
36
- </header>
37
- <main>
38
- <p>Preparing preview — this page refreshes automatically. <a href="/projects/${escapeAttr(slug)}/builds/${escapeAttr(build.id)}">View build status</a></p>
39
- </main>
40
- </body>
41
- </html>`;
42
- }
43
- /**
44
- * Landing page for a published Storybook build. Presents the live Storybook
45
- * (via an iframe against the same build's statics) with the project chrome, so
46
- * the URL is shareable with designers/managers (ADR 0011).
47
- */
48
- function renderStorybookPage(project, build, slug) {
49
- const { ui } = getStore();
50
- const name = ui.name ?? "StoryShelf";
51
- return `<!doctype html>
52
- <html lang="en" data-theme="system">
53
- <head>
54
- <meta charset="utf-8" />
55
- <meta name="viewport" content="width=device-width, initial-scale=1" />
56
- <title>${escapeHtml(project.name)} Storybook · ${name}</title>
57
- <style>
58
- html, body { margin: 0; height: 100%; display: flex; flex-direction: column; font-family: system-ui, sans-serif; }
59
- header { display: flex; align-items: center; gap: .75rem; padding: .5rem 1rem; border-bottom: 1px solid var(--border, #ddd); }
60
- header a { color: inherit; text-decoration: none; }
61
- header .crumb { opacity: .7; }
62
- header .branch { font-weight: 600; }
63
- iframe { flex: 1; border: 0; width: 100%; }
64
- </style>
65
- </head>
66
- <body>
67
- <header>
68
- <a href="/">${name}</a>
69
- <span class="crumb">/</span>
70
- <a href="/projects/${escapeAttr(slug)}/builds">${escapeHtml(project.name)}</a>
71
- <span class="crumb">/</span>
72
- <span class="branch">${escapeHtml(build.gitBranch)}</span>
73
- <span class="crumb">· ${build.gitSha.slice(0, 7)}</span>
74
- </header>
75
- <iframe src="${escapeAttr(`./iframe.html`)}" title="${escapeAttr(project.name)} Storybook"></iframe>
76
- </body>
77
- </html>`;
78
- }
79
- function escapeHtml(value) {
80
- return value.replaceAll(/[&<>"']/gu, (ch) => {
81
- switch (ch) {
82
- case "&": return "&amp;";
83
- case "<": return "&lt;";
84
- case ">": return "&gt;";
85
- case "\"": return "&quot;";
86
- default: return "&#39;";
87
- }
88
- });
89
- }
90
- function escapeAttr(value) {
91
- return escapeHtml(value);
92
- }
93
- //#endregion
94
- export { renderStorybookPage, renderStorybookPreparingPage };
95
-
96
- //# sourceMappingURL=storybook.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/pages/storybook.tsx"],"sourcesContent":["import type { Build } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { getStore } from \"../store.ts\";\n\n/**\n * Placeholder shown while a build's statics are not yet available.\n * Refreshes itself; once extraction lands, the route serves the live page.\n */\nexport function renderStorybookPreparingPage(project: Project, build: Build, slug: string): string {\n const { ui } = getStore();\n const name = ui.name ?? \"StoryShelf\";\n return `<!doctype html>\n<html lang=\"en\" data-theme=\"system\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta http-equiv=\"refresh\" content=\"5\" />\n <title>${escapeHtml(project.name)} Storybook · ${name}</title>\n <style>\n html, body { margin: 0; height: 100%; display: flex; flex-direction: column; font-family: system-ui, sans-serif; }\n header { display: flex; align-items: center; gap: .75rem; padding: .5rem 1rem; border-bottom: 1px solid var(--border, #ddd); }\n header a { color: inherit; text-decoration: none; }\n header .crumb { opacity: .7; }\n main { flex: 1; display: flex; align-items: center; justify-content: center; opacity: .7; }\n </style>\n </head>\n <body>\n <header>\n <a href=\"/\">${name}</a>\n <span class=\"crumb\">/</span>\n <a href=\"/projects/${escapeAttr(slug)}/builds\">${escapeHtml(project.name)}</a>\n <span class=\"crumb\">/</span>\n <span>${escapeHtml(build.gitBranch)}</span>\n <span class=\"crumb\">· ${build.gitSha.slice(0, 7)}</span>\n </header>\n <main>\n <p>Preparing preview — this page refreshes automatically. <a href=\"/projects/${escapeAttr(slug)}/builds/${escapeAttr(build.id)}\">View build status</a></p>\n </main>\n </body>\n</html>`;\n}\n/**\n * Landing page for a published Storybook build. Presents the live Storybook\n * (via an iframe against the same build's statics) with the project chrome, so\n * the URL is shareable with designers/managers (ADR 0011).\n */\nexport function renderStorybookPage(project: Project, build: Build, slug: string): string {\n const { ui } = getStore();\n const name = ui.name ?? \"StoryShelf\";\n const iframeSrc = `./iframe.html`;\n return `<!doctype html>\n<html lang=\"en\" data-theme=\"system\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>${escapeHtml(project.name)} Storybook · ${name}</title>\n <style>\n html, body { margin: 0; height: 100%; display: flex; flex-direction: column; font-family: system-ui, sans-serif; }\n header { display: flex; align-items: center; gap: .75rem; padding: .5rem 1rem; border-bottom: 1px solid var(--border, #ddd); }\n header a { color: inherit; text-decoration: none; }\n header .crumb { opacity: .7; }\n header .branch { font-weight: 600; }\n iframe { flex: 1; border: 0; width: 100%; }\n </style>\n </head>\n <body>\n <header>\n <a href=\"/\">${name}</a>\n <span class=\"crumb\">/</span>\n <a href=\"/projects/${escapeAttr(slug)}/builds\">${escapeHtml(project.name)}</a>\n <span class=\"crumb\">/</span>\n <span class=\"branch\">${escapeHtml(build.gitBranch)}</span>\n <span class=\"crumb\">· ${build.gitSha.slice(0, 7)}</span>\n </header>\n <iframe src=\"${escapeAttr(iframeSrc)}\" title=\"${escapeAttr(project.name)} Storybook\"></iframe>\n </body>\n</html>`;\n}\n\nfunction escapeHtml(value: string): string {\n return value.replaceAll(/[&<>\"']/gu, (ch) => {\n switch (ch) {\n case \"&\": {\n return \"&amp;\";\n }\n case \"<\": {\n return \"&lt;\";\n }\n case \">\": {\n return \"&gt;\";\n }\n case '\"': {\n return \"&quot;\";\n }\n default: {\n return \"&#39;\";\n }\n }\n });\n}\n\nfunction escapeAttr(value: string): string {\n return escapeHtml(value);\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,6BAA6B,SAAkB,OAAc,MAAsB;CACjG,MAAM,EAAE,OAAO,SAAS;CACxB,MAAM,OAAO,GAAG,QAAQ;CACxB,OAAO;;;;;;aAMI,WAAW,QAAQ,IAAI,EAAE,eAAe,KAAK;;;;;;;;;;;oBAWtC,KAAK;;2BAEE,WAAW,IAAI,EAAE,WAAW,WAAW,QAAQ,IAAI,EAAE;;cAElE,WAAW,MAAM,SAAS,EAAE;8BACZ,MAAM,OAAO,MAAM,GAAG,CAAC,EAAE;;;qFAG8B,WAAW,IAAI,EAAE,UAAU,WAAW,MAAM,EAAE,EAAE;;;;AAIrI;;;;;;AAMA,SAAgB,oBAAoB,SAAkB,OAAc,MAAsB;CACxF,MAAM,EAAE,OAAO,SAAS;CACxB,MAAM,OAAO,GAAG,QAAQ;CAExB,OAAO;;;;;aAKI,WAAW,QAAQ,IAAI,EAAE,eAAe,KAAK;;;;;;;;;;;;oBAYtC,KAAK;;2BAEE,WAAW,IAAI,EAAE,WAAW,WAAW,QAAQ,IAAI,EAAE;;6BAEnD,WAAW,MAAM,SAAS,EAAE;8BAC3B,MAAM,OAAO,MAAM,GAAG,CAAC,EAAE;;mBAEpC,WAAW,eAAS,EAAE,WAAW,WAAW,QAAQ,IAAI,EAAE;;;AAG7E;AAEA,SAAS,WAAW,OAAuB;CACzC,OAAO,MAAM,WAAW,cAAc,OAAO;EAC3C,QAAQ,IAAR;GACE,KAAK,KACH,OAAO;GAET,KAAK,KACH,OAAO;GAET,KAAK,KACH,OAAO;GAET,KAAK,MACH,OAAO;GAET,SACE,OAAO;EAEX;CACF,CAAC;AACH;AAEA,SAAS,WAAW,OAAuB;CACzC,OAAO,WAAW,KAAK;AACzB"}
@@ -1,67 +0,0 @@
1
- import "node:path";
2
- import "node:url";
3
- import.meta.url;
4
- import { builds } from "../schema/build.mjs";
5
- import { BuildModel } from "../models/build.mjs";
6
- import { TERMINAL_BUILD_STATUSES } from "../types.mjs";
7
- import { LabelModel } from "../models/label.mjs";
8
- import { and, eq, inArray, lt, sql } from "drizzle-orm";
9
- //#region src/retention/purge.ts
10
- /** Removes expired transient builds while keeping baselines and persistent builds. */
11
- var Retention = class {
12
- db;
13
- storage;
14
- logger;
15
- constructor(db, storage, logger) {
16
- this.db = db;
17
- this.storage = storage;
18
- this.logger = logger;
19
- }
20
- async purge(project, options) {
21
- const cutoff = (/* @__PURE__ */ new Date(Date.now() - options.ttlDays * 864e5)).toISOString();
22
- const candidates = await this.db.list(builds, { where: and(eq(builds.projectId, project.id), inArray(builds.status, [...TERMINAL_BUILD_STATUSES]), lt(builds.updatedAt, cutoff)) });
23
- const keep = options.keepLatestPerBranch ? await this.latestPerBranch(project.id) : /* @__PURE__ */ new Set();
24
- const labelModel = new LabelModel(this.db);
25
- const buildIds = (await Promise.all(candidates.map(async (build) => {
26
- if (keep.has(build.id) || await labelModel.hasPersistent(project.id, build.id)) return null;
27
- return build.id;
28
- }))).filter((id) => id !== null);
29
- const results = await Promise.all(buildIds.map(async (buildId) => ({
30
- files: await this.deleteBuildFiles(project.id, buildId),
31
- removed: await this.removeBuild(buildId)
32
- })));
33
- const removedBuilds = results.filter((r) => r.removed).length;
34
- const removedFiles = results.reduce((sum, r) => sum + r.files, 0);
35
- this.logger?.info({
36
- projectId: project.id,
37
- removedBuilds,
38
- removedFiles
39
- }, "build retention purge complete");
40
- return {
41
- removedBuilds,
42
- removedFiles
43
- };
44
- }
45
- async removeBuild(buildId) {
46
- await new BuildModel(this.db).remove(buildId);
47
- return true;
48
- }
49
- async latestPerBranch(projectId) {
50
- const rows = await this.db.all(sql`SELECT id, gitBranch, createdAt FROM builds WHERE projectId = ${projectId} ORDER BY createdAt DESC`);
51
- const latest = /* @__PURE__ */ new Map();
52
- for (const row of rows) if (!latest.get(row.gitBranch)) latest.set(row.gitBranch, row.id);
53
- return new Set(latest.values());
54
- }
55
- async deleteBuildFiles(projectId, buildId) {
56
- const prefix = `${projectId}/builds/${buildId}/`;
57
- const files = await this.storage.list(prefix);
58
- await Promise.all(files.map(async (file) => {
59
- await this.storage.delete(file);
60
- }));
61
- return files.length;
62
- }
63
- };
64
- //#endregion
65
- export { Retention };
66
-
67
- //# sourceMappingURL=purge.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"purge.mjs","names":[],"sources":["../../src/retention/purge.ts"],"sourcesContent":["import { and, eq, inArray, lt, sql } from \"drizzle-orm\";\nimport type { Logger } from \"pino\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { builds } from \"../schema/build.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { TERMINAL_BUILD_STATUSES } from \"../types.ts\";\n\n/** Options controlling which builds a retention purge removes. */\nexport interface PurgeOptions {\n ttlDays: number;\n keepLatestPerBranch: boolean;\n}\n\n/** Counts of builds and files removed by a retention purge. */\nexport interface PurgeResult {\n removedBuilds: number;\n removedFiles: number;\n}\n/** Removes expired transient builds while keeping baselines and persistent builds. */\nexport class Retention {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly storage: StorageAdapter,\n private readonly logger?: Logger,\n ) {}\n\n async purge(project: Project, options: PurgeOptions): Promise<PurgeResult> {\n const cutoff = new Date(Date.now() - options.ttlDays * 86_400_000).toISOString();\n const candidates = await this.db.list(builds, {\n where: and(\n eq(builds.projectId, project.id),\n inArray(builds.status, [...TERMINAL_BUILD_STATUSES]),\n lt(builds.updatedAt, cutoff),\n ),\n });\n\n const keep = options.keepLatestPerBranch\n ? await this.latestPerBranch(project.id)\n : new Set<string>();\n const labelModel = new LabelModel(this.db);\n const target = await Promise.all(\n candidates.map(async (build) => {\n if (keep.has(build.id) || (await labelModel.hasPersistent(project.id, build.id))) {\n return null;\n }\n return build.id;\n }),\n );\n\n const buildIds = target.filter((id): id is string => id !== null);\n const results = await Promise.all(\n buildIds.map(async (buildId) => ({\n files: await this.deleteBuildFiles(project.id, buildId),\n removed: await this.removeBuild(buildId),\n })),\n );\n const removedBuilds = results.filter((r) => r.removed).length;\n const removedFiles = results.reduce((sum, r) => sum + r.files, 0);\n this.logger?.info(\n { projectId: project.id, removedBuilds, removedFiles },\n \"build retention purge complete\",\n );\n return { removedBuilds, removedFiles };\n }\n\n private async removeBuild(buildId: string): Promise<boolean> {\n await new BuildModel(this.db).remove(buildId);\n return true;\n }\n\n private async latestPerBranch(projectId: string): Promise<Set<string>> {\n const rows = await this.db.all<{ id: string; gitBranch: string }>(\n sql`SELECT id, gitBranch, createdAt FROM builds WHERE projectId = ${projectId} ORDER BY createdAt DESC`,\n );\n const latest = new Map<string, string>();\n for (const row of rows) {\n const current = latest.get(row.gitBranch);\n if (!current) {\n latest.set(row.gitBranch, row.id);\n }\n }\n return new Set(latest.values());\n }\n\n private async deleteBuildFiles(projectId: string, buildId: string): Promise<number> {\n const prefix = `${projectId}/builds/${buildId}/`;\n const files = await this.storage.list(prefix);\n await Promise.all(\n files.map(async (file) => {\n await this.storage.delete(file);\n }),\n );\n return files.length;\n }\n}\n"],"mappings":";;;;;;;;;;AAsBA,IAAa,YAAb,MAAuB;CAEF;CACA;CACA;CAHnB,YACE,IACA,SACA,QACA;EAHiB,KAAA,KAAA;EACA,KAAA,UAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,MAAM,SAAkB,SAA6C;EACzE,MAAM,0BAAS,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,UAAU,KAAU,EAAA,CAAE,YAAY;EAC/E,MAAM,aAAa,MAAM,KAAK,GAAG,KAAK,QAAQ,EAC5C,OAAO,IACL,GAAG,OAAO,WAAW,QAAQ,EAAE,GAC/B,QAAQ,OAAO,QAAQ,CAAC,GAAG,uBAAuB,CAAC,GACnD,GAAG,OAAO,WAAW,MAAM,CAC7B,EACF,CAAC;EAED,MAAM,OAAO,QAAQ,sBACjB,MAAM,KAAK,gBAAgB,QAAQ,EAAE,oBACrC,IAAI,IAAY;EACpB,MAAM,aAAa,IAAI,WAAW,KAAK,EAAE;EAUzC,MAAM,YAAW,MATI,QAAQ,IAC3B,WAAW,IAAI,OAAO,UAAU;GAC9B,IAAI,KAAK,IAAI,MAAM,EAAE,KAAM,MAAM,WAAW,cAAc,QAAQ,IAAI,MAAM,EAAE,GAC5E,OAAO;GAET,OAAO,MAAM;EACf,CAAC,CACH,EAAA,CAEwB,QAAQ,OAAqB,OAAO,IAAI;EAChE,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,IAAI,OAAO,aAAa;GAC/B,OAAO,MAAM,KAAK,iBAAiB,QAAQ,IAAI,OAAO;GACtD,SAAS,MAAM,KAAK,YAAY,OAAO;EACzC,EAAE,CACJ;EACA,MAAM,gBAAgB,QAAQ,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC;EACvD,MAAM,eAAe,QAAQ,QAAQ,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;EAChE,KAAK,QAAQ,KACX;GAAE,WAAW,QAAQ;GAAI;GAAe;EAAa,GACrD,gCACF;EACA,OAAO;GAAE;GAAe;EAAa;CACvC;CAEA,MAAc,YAAY,SAAmC;EAC3D,MAAM,IAAI,WAAW,KAAK,EAAE,CAAC,CAAC,OAAO,OAAO;EAC5C,OAAO;CACT;CAEA,MAAc,gBAAgB,WAAyC;EACrE,MAAM,OAAO,MAAM,KAAK,GAAG,IACzB,GAAG,iEAAiE,UAAU,yBAChF;EACA,MAAM,yBAAS,IAAI,IAAoB;EACvC,KAAK,MAAM,OAAO,MAEhB,IAAI,CADY,OAAO,IAAI,IAAI,SACpB,GACT,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;EAGpC,OAAO,IAAI,IAAI,OAAO,OAAO,CAAC;CAChC;CAEA,MAAc,iBAAiB,WAAmB,SAAkC;EAClF,MAAM,SAAS,GAAG,UAAU,UAAU,QAAQ;EAC9C,MAAM,QAAQ,MAAM,KAAK,QAAQ,KAAK,MAAM;EAC5C,MAAM,QAAQ,IACZ,MAAM,IAAI,OAAO,SAAS;GACxB,MAAM,KAAK,QAAQ,OAAO,IAAI;EAChC,CAAC,CACH;EACA,OAAO,MAAM;CACf;AACF"}
@@ -1,42 +0,0 @@
1
- import "node:path";
2
- import "node:url";
3
- import.meta.url;
4
- import { ProjectModel } from "../models/project.mjs";
5
- import { getStore } from "../store.mjs";
6
- import { Retention } from "../retention/purge.mjs";
7
- import { requireSiteAdmin } from "./helpers.mjs";
8
- import { forbidden, purgeInputSchema, purgeSchema } from "./schemas.mjs";
9
- import { createRoute } from "@hono/zod-openapi";
10
- //#region src/routers/admin.ts
11
- const purgeRoute = createRoute({
12
- method: "post",
13
- path: "/api/v1/admin/purge",
14
- request: { body: { content: { "application/json": { schema: purgeInputSchema } } } },
15
- responses: {
16
- 200: {
17
- content: { "application/json": { schema: purgeSchema } },
18
- description: "Retention purge run"
19
- },
20
- ...forbidden
21
- }
22
- });
23
- /** Register the site-admin retention purge endpoint. */
24
- function registerAdmin(app) {
25
- app.openapi(purgeRoute, async (c) => {
26
- requireSiteAdmin();
27
- const ttlDays = c.req.valid("json").ttlDays ?? getStore().config.purgeTtlDays ?? 30;
28
- const projects = await new ProjectModel(getStore().db).list();
29
- const retention = new Retention(getStore().db, getStore().storage);
30
- const removedBuilds = (await Promise.all(projects.map(async (project) => {
31
- return await retention.purge(project, {
32
- ttlDays,
33
- keepLatestPerBranch: true
34
- });
35
- }))).reduce((sum, result) => sum + result.removedBuilds, 0);
36
- return c.json({ removedBuilds });
37
- });
38
- }
39
- //#endregion
40
- export { registerAdmin };
41
-
42
- //# sourceMappingURL=admin.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"admin.mjs","names":["forbiddenResponse"],"sources":["../../src/routers/admin.ts"],"sourcesContent":["import { createRoute } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { Retention } from \"../retention/purge.ts\";\nimport { getStore } from \"../store.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { forbidden as forbiddenResponse, purgeInputSchema, purgeSchema } from \"./schemas.ts\";\n\nconst purgeRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/admin/purge\",\n request: { body: { content: { \"application/json\": { schema: purgeInputSchema } } } },\n responses: {\n 200: {\n content: { \"application/json\": { schema: purgeSchema } },\n description: \"Retention purge run\",\n },\n ...forbiddenResponse,\n },\n});\n\n/** Register the site-admin retention purge endpoint. */\nexport function registerAdmin(app: ShelfApp): void {\n app.openapi(purgeRoute, async (c) => {\n requireSiteAdmin();\n const body = c.req.valid(\"json\");\n const ttlDays = body.ttlDays ?? getStore().config.purgeTtlDays ?? 30;\n const projects = await new ProjectModel(getStore().db).list();\n const retention = new Retention(getStore().db, getStore().storage);\n const results = await Promise.all(\n projects.map(async (project) => {\n return await retention.purge(project, { ttlDays, keepLatestPerBranch: true });\n }),\n );\n const removedBuilds = results.reduce((sum, result) => sum + result.removedBuilds, 0);\n return c.json({ removedBuilds });\n });\n}\n"],"mappings":";;;;;;;;;;AAQA,MAAM,aAAa,YAAY;CAC7B,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,iBAAiB,EAAE,EAAE,EAAE;CACnF,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,cAAc,KAAqB;CACjD,IAAI,QAAQ,YAAY,OAAO,MAAM;EACnC,iBAAiB;EAEjB,MAAM,UADO,EAAE,IAAI,MAAM,MACN,CAAC,CAAC,WAAW,SAAS,CAAC,CAAC,OAAO,gBAAgB;EAClE,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK;EAC5D,MAAM,YAAY,IAAI,UAAU,SAAS,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,OAAO;EAMjE,MAAM,iBAAgB,MALA,QAAQ,IAC5B,SAAS,IAAI,OAAO,YAAY;GAC9B,OAAO,MAAM,UAAU,MAAM,SAAS;IAAE;IAAS,qBAAqB;GAAK,CAAC;EAC9E,CAAC,CACH,EAAA,CAC8B,QAAQ,KAAK,WAAW,MAAM,OAAO,eAAe,CAAC;EACnF,OAAO,EAAE,KAAK,EAAE,cAAc,CAAC;CACjC,CAAC;AACH"}
@@ -1,25 +0,0 @@
1
- import "node:path";
2
- import "node:url";
3
- import.meta.url;
4
- import { readFile } from "node:fs/promises";
5
- import { fileURLToPath } from "node:url";
6
- //#region src/routers/assets.ts
7
- let htmxSource = null;
8
- async function htmxScript() {
9
- htmxSource ??= await readFile(fileURLToPath(new URL("../assets/htmx.min.js", import.meta.url)), "utf8");
10
- return htmxSource;
11
- }
12
- /** Register the vendored static-asset routes (HTMX bundle). */
13
- function registerAssets(app) {
14
- app.get("/assets/htmx.js", async (c) => {
15
- const body = await htmxScript();
16
- return c.body(body, 200, {
17
- "content-type": "application/javascript",
18
- "cache-control": "public, max-age=31536000, immutable"
19
- });
20
- });
21
- }
22
- //#endregion
23
- export { registerAssets };
24
-
25
- //# sourceMappingURL=assets.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"assets.mjs","names":[],"sources":["../../src/routers/assets.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nlet htmxSource: string | null = null;\n\nasync function htmxScript(): Promise<string> {\n htmxSource ??= await readFile(\n fileURLToPath(new URL(\"../assets/htmx.min.js\", import.meta.url)),\n \"utf8\",\n );\n return htmxSource;\n}\n\n/** Register the vendored static-asset routes (HTMX bundle). */\nexport function registerAssets(app: ShelfApp): void {\n app.get(\"/assets/htmx.js\", async (c) => {\n const body = await htmxScript();\n return c.body(body, 200, {\n \"content-type\": \"application/javascript\",\n \"cache-control\": \"public, max-age=31536000, immutable\",\n });\n });\n}\n"],"mappings":";;;;;;AAIA,IAAI,aAA4B;AAEhC,eAAe,aAA8B;CAC3C,eAAe,MAAM,SACnB,cAAc,IAAI,IAAI,yBAAyB,YAAY,GAAG,CAAC,GAC/D,MACF;CACA,OAAO;AACT;;AAGA,SAAgB,eAAe,KAAqB;CAClD,IAAI,IAAI,mBAAmB,OAAO,MAAM;EACtC,MAAM,OAAO,MAAM,WAAW;EAC9B,OAAO,EAAE,KAAK,MAAM,KAAK;GACvB,gBAAgB;GAChB,iBAAiB;EACnB,CAAC;CACH,CAAC;AACH"}
@@ -1,78 +0,0 @@
1
- import "node:path";
2
- import "node:url";
3
- import.meta.url;
4
- import { randomToken } from "../utils/hash.mjs";
5
- import { SESSION_COOKIE } from "../adapter/auth.mjs";
6
- import { renderLoginPage } from "../pages/login.mjs";
7
- import { hxRedirect } from "./htmx.mjs";
8
- import { HTTPException } from "hono/http-exception";
9
- //#region src/routers/auth.ts
10
- const OAUTH_STATE_COOKIE = "storyshelf_oauth_state";
11
- const SESSION_TTL_SECONDS = 604800;
12
- const SHARED_USER = {
13
- id: "shared",
14
- email: "admin@local",
15
- name: "Admin",
16
- role: "admin"
17
- };
18
- function sessionCookieHeader(token) {
19
- return `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}`;
20
- }
21
- function clearSessionCookieHeader() {
22
- return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
23
- }
24
- function hasPasswordLogin(auth) {
25
- return "login" in auth;
26
- }
27
- function hasSso(auth) {
28
- return "loginUrl" in auth;
29
- }
30
- function buildSsoUrl(c, auth) {
31
- const state = randomToken("shelf_").value;
32
- c.header("set-cookie", `${OAUTH_STATE_COOKIE}=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`);
33
- return auth.loginUrl(state);
34
- }
35
- /** Register the password and SSO login, callback, and logout routes. */
36
- function registerAuth(app, auth) {
37
- app.get("/auth/login", async (c) => {
38
- if (hasSso(auth) && !hasPasswordLogin(auth)) return c.redirect(buildSsoUrl(c, auth));
39
- const ssoUrl = hasSso(auth) ? buildSsoUrl(c, auth) : void 0;
40
- return c.html(await renderLoginPage({ ssoUrl }));
41
- });
42
- app.post("/auth/login", async (c) => {
43
- if (!hasPasswordLogin(auth)) throw new HTTPException(404, { message: "Password login is not configured" });
44
- const raw = (await c.req.formData()).get("password");
45
- const password = typeof raw === "string" ? raw : "";
46
- try {
47
- const token = await auth.login(password, SHARED_USER);
48
- c.header("set-cookie", sessionCookieHeader(token));
49
- return hxRedirect(c, "/");
50
- } catch {
51
- return c.html(await renderLoginPage({ error: "Invalid password" }), 401);
52
- }
53
- });
54
- app.get("/auth/callback", async (c) => {
55
- if (!auth.handleCallback) return c.notFound();
56
- const code = c.req.query("code") ?? "";
57
- const state = c.req.query("state") ?? "";
58
- if (!code || !state) throw new HTTPException(400, { message: "Missing code or state" });
59
- const user = await auth.handleCallback({
60
- provider: "oidc",
61
- code,
62
- state
63
- });
64
- if (!user) return c.html(await renderLoginPage({ error: "SSO sign in failed" }), 401);
65
- const token = await auth.createSession(user);
66
- c.header("set-cookie", sessionCookieHeader(token));
67
- return c.redirect("/");
68
- });
69
- app.post("/auth/logout", async (c) => {
70
- await auth.destroySession("");
71
- c.header("set-cookie", clearSessionCookieHeader());
72
- return hxRedirect(c, "/auth/login");
73
- });
74
- }
75
- //#endregion
76
- export { registerAuth };
77
-
78
- //# sourceMappingURL=auth.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth.mjs","names":[],"sources":["../../src/routers/auth.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { SESSION_COOKIE, type AuthAdapter, type AuthUser } from \"../adapters/auth.ts\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { renderLoginPage } from \"../pages/login.tsx\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\nconst OAUTH_STATE_COOKIE = \"storyshelf_oauth_state\";\nconst SESSION_TTL_SECONDS = 7 * 24 * 60 * 60;\n\nconst SHARED_USER: AuthUser = { id: \"shared\", email: \"admin@local\", name: \"Admin\", role: \"admin\" };\n\nfunction sessionCookieHeader(token: string): string {\n return `${SESSION_COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}`;\n}\n\nfunction clearSessionCookieHeader(): string {\n return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;\n}\n\ntype PasswordAuth = AuthAdapter & { login(password: string, user: AuthUser): Promise<string> };\ntype SsoAuth = AuthAdapter & { loginUrl(state: string): string };\n\nfunction hasPasswordLogin(auth: AuthAdapter): auth is PasswordAuth {\n return \"login\" in auth;\n}\n\nfunction hasSso(auth: AuthAdapter): auth is SsoAuth {\n return \"loginUrl\" in auth;\n}\n\nfunction buildSsoUrl(c: Context, auth: SsoAuth): string {\n const state = randomToken(\"shelf_\").value;\n c.header(\n \"set-cookie\",\n `${OAUTH_STATE_COOKIE}=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,\n );\n return auth.loginUrl(state);\n}\n\n/** Register the password and SSO login, callback, and logout routes. */\nexport function registerAuth(app: ShelfApp, auth: AuthAdapter): void {\n app.get(\"/auth/login\", async (c) => {\n if (hasSso(auth) && !hasPasswordLogin(auth)) {\n return c.redirect(buildSsoUrl(c, auth));\n }\n const ssoUrl = hasSso(auth) ? buildSsoUrl(c, auth) : undefined;\n return c.html(await renderLoginPage({ ssoUrl }));\n });\n\n app.post(\"/auth/login\", async (c) => {\n if (!hasPasswordLogin(auth)) {\n throw new HTTPException(404, { message: \"Password login is not configured\" });\n }\n const form = await c.req.formData();\n const raw = form.get(\"password\");\n const password = typeof raw === \"string\" ? raw : \"\";\n try {\n const token = await auth.login(password, SHARED_USER);\n c.header(\"set-cookie\", sessionCookieHeader(token));\n return hxRedirect(c, \"/\");\n } catch {\n return c.html(await renderLoginPage({ error: \"Invalid password\" }), 401);\n }\n });\n\n app.get(\"/auth/callback\", async (c) => {\n if (!auth.handleCallback) {\n return c.notFound();\n }\n const code = c.req.query(\"code\") ?? \"\";\n const state = c.req.query(\"state\") ?? \"\";\n if (!code || !state) {\n throw new HTTPException(400, { message: \"Missing code or state\" });\n }\n const user = await auth.handleCallback({ provider: \"oidc\", code, state });\n if (!user) {\n return c.html(await renderLoginPage({ error: \"SSO sign in failed\" }), 401);\n }\n const token = await auth.createSession(user);\n c.header(\"set-cookie\", sessionCookieHeader(token));\n return c.redirect(\"/\");\n });\n\n app.post(\"/auth/logout\", async (c) => {\n await auth.destroySession(\"\");\n c.header(\"set-cookie\", clearSessionCookieHeader());\n return hxRedirect(c, \"/auth/login\");\n });\n}\n"],"mappings":";;;;;;;;;AAQA,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAE5B,MAAM,cAAwB;CAAE,IAAI;CAAU,OAAO;CAAe,MAAM;CAAS,MAAM;AAAQ;AAEjG,SAAS,oBAAoB,OAAuB;CAClD,OAAO,GAAG,eAAe,GAAG,MAAM,4CAA4C;AAChF;AAEA,SAAS,2BAAmC;CAC1C,OAAO,GAAG,eAAe;AAC3B;AAKA,SAAS,iBAAiB,MAAyC;CACjE,OAAO,WAAW;AACpB;AAEA,SAAS,OAAO,MAAoC;CAClD,OAAO,cAAc;AACvB;AAEA,SAAS,YAAY,GAAY,MAAuB;CACtD,MAAM,QAAQ,YAAY,QAAQ,CAAC,CAAC;CACpC,EAAE,OACA,cACA,GAAG,mBAAmB,GAAG,MAAM,8CACjC;CACA,OAAO,KAAK,SAAS,KAAK;AAC5B;;AAGA,SAAgB,aAAa,KAAe,MAAyB;CACnE,IAAI,IAAI,eAAe,OAAO,MAAM;EAClC,IAAI,OAAO,IAAI,KAAK,CAAC,iBAAiB,IAAI,GACxC,OAAO,EAAE,SAAS,YAAY,GAAG,IAAI,CAAC;EAExC,MAAM,SAAS,OAAO,IAAI,IAAI,YAAY,GAAG,IAAI,IAAI,KAAA;EACrD,OAAO,EAAE,KAAK,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC;CACjD,CAAC;CAED,IAAI,KAAK,eAAe,OAAO,MAAM;EACnC,IAAI,CAAC,iBAAiB,IAAI,GACxB,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,mCAAmC,CAAC;EAG9E,MAAM,OAAM,MADO,EAAE,IAAI,SAAS,EAAA,CACjB,IAAI,UAAU;EAC/B,MAAM,WAAW,OAAO,QAAQ,WAAW,MAAM;EACjD,IAAI;GACF,MAAM,QAAQ,MAAM,KAAK,MAAM,UAAU,WAAW;GACpD,EAAE,OAAO,cAAc,oBAAoB,KAAK,CAAC;GACjD,OAAO,WAAW,GAAG,GAAG;EAC1B,QAAQ;GACN,OAAO,EAAE,KAAK,MAAM,gBAAgB,EAAE,OAAO,mBAAmB,CAAC,GAAG,GAAG;EACzE;CACF,CAAC;CAED,IAAI,IAAI,kBAAkB,OAAO,MAAM;EACrC,IAAI,CAAC,KAAK,gBACR,OAAO,EAAE,SAAS;EAEpB,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM,KAAK;EACpC,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO,KAAK;EACtC,IAAI,CAAC,QAAQ,CAAC,OACZ,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,wBAAwB,CAAC;EAEnE,MAAM,OAAO,MAAM,KAAK,eAAe;GAAE,UAAU;GAAQ;GAAM;EAAM,CAAC;EACxE,IAAI,CAAC,MACH,OAAO,EAAE,KAAK,MAAM,gBAAgB,EAAE,OAAO,qBAAqB,CAAC,GAAG,GAAG;EAE3E,MAAM,QAAQ,MAAM,KAAK,cAAc,IAAI;EAC3C,EAAE,OAAO,cAAc,oBAAoB,KAAK,CAAC;EACjD,OAAO,EAAE,SAAS,GAAG;CACvB,CAAC;CAED,IAAI,KAAK,gBAAgB,OAAO,MAAM;EACpC,MAAM,KAAK,eAAe,EAAE;EAC5B,EAAE,OAAO,cAAc,yBAAyB,CAAC;EACjD,OAAO,WAAW,GAAG,aAAa;CACpC,CAAC;AACH"}
@@ -1,184 +0,0 @@
1
- import "node:path";
2
- import "node:url";
3
- import.meta.url;
4
- import { BuildModel } from "../models/build.mjs";
5
- import { ProjectModel } from "../models/project.mjs";
6
- import { emitWebhookEvent } from "../adapters/webhook-events.mjs";
7
- import { BaselineModel } from "../models/baseline.mjs";
8
- import { SnapshotModel } from "../models/snapshot.mjs";
9
- import { extractStorybookToScratch, persistStorybookStatics } from "../capture/statics.mjs";
10
- import { getStore } from "../store.mjs";
11
- import { BUILD_STATUSES } from "../types.mjs";
12
- import { LabelModel } from "../models/label.mjs";
13
- import { notFound } from "./helpers.mjs";
14
- import { z } from "@hono/zod-openapi";
15
- import { HTTPException } from "hono/http-exception";
16
- import { Readable, Transform } from "node:stream";
17
- //#region src/routers/builds.handlers.ts
18
- /** Roles permitted to view builds, snapshots, and comments. */
19
- const VIEW_ROLES = [
20
- "viewer",
21
- "developer",
22
- "approver",
23
- "admin"
24
- ];
25
- /** Roles permitted to upload builds and write comments. */
26
- const DEVELOPER_ROLES = [
27
- "developer",
28
- "approver",
29
- "admin"
30
- ];
31
- /** Roles permitted to approve or reject snapshots and delete builds. */
32
- const APPROVER_ROLES = ["approver", "admin"];
33
- /** JSON input schema for creating a build before streaming its bundle. */
34
- const buildCreateJsonSchema = z.object({
35
- gitSha: z.string().min(1),
36
- gitBranch: z.string().min(1),
37
- authorEmail: z.string().optional(),
38
- authorName: z.string().optional(),
39
- message: z.string().optional(),
40
- labels: z.array(z.object({
41
- key: z.string().min(1),
42
- value: z.string()
43
- })).optional()
44
- }).openapi("BuildCreate");
45
- async function attachLabels(db, projectId, buildId, labels) {
46
- const models = new LabelModel(db);
47
- for (const { key, value } of labels) {
48
- if (!await models.getType(projectId, key)) await models.createType(projectId, {
49
- key,
50
- name: key
51
- });
52
- await models.attach(projectId, buildId, key, value);
53
- }
54
- }
55
- /**
56
- * Create a build record with labels and the `build:created` webhook.
57
- * Shared by the build-creation route.
58
- */
59
- async function createBuildRecord(project, meta) {
60
- const { db, config } = getStore();
61
- if (!meta.gitSha || !meta.gitBranch) throw new HTTPException(400, { message: "gitSha and gitBranch are required" });
62
- const build = await new BuildModel(db).create(project.id, {
63
- gitSha: meta.gitSha,
64
- gitBranch: meta.gitBranch,
65
- isDefault: meta.gitBranch === project.gitDefaultBranch,
66
- authorEmail: meta.authorEmail,
67
- authorName: meta.authorName,
68
- message: meta.message
69
- });
70
- if (meta.labels && meta.labels.length > 0) await attachLabels(db, project.id, build.id, meta.labels);
71
- await emitWebhookEvent(db, project.id, "build:created", {
72
- buildId: build.id,
73
- gitSha: meta.gitSha,
74
- gitBranch: meta.gitBranch,
75
- authorEmail: meta.authorEmail,
76
- authorName: meta.authorName,
77
- message: meta.message
78
- }, config.secret);
79
- return build;
80
- }
81
- /**
82
- * Stream a raw request body into storage with a byte cap.
83
- * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.
84
- */
85
- async function storeUploadStream(storage, path, body, maxBytes) {
86
- if (!body) throw new HTTPException(400, { message: "Missing request body" });
87
- let seen = 0;
88
- const limiter = new Transform({ transform(chunk, _encoding, callback) {
89
- seen += chunk.length;
90
- if (seen > maxBytes) callback(/* @__PURE__ */ new Error(`Upload exceeds ${maxBytes} byte limit`));
91
- else callback(null, chunk);
92
- } });
93
- try {
94
- const nodeBody = Readable.fromWeb(body);
95
- await storage.writeStream(path, nodeBody.pipe(limiter));
96
- } catch (error) {
97
- if (seen > maxBytes) throw new HTTPException(413, { message: "Upload exceeds size limit" });
98
- throw error;
99
- }
100
- }
101
- /** Parse a Content-Length header value into bytes, or undefined when unknown. */
102
- function parseContentLength(value) {
103
- if (!value) return;
104
- const parsed = Math.trunc(Number(value));
105
- return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
106
- }
107
- /**
108
- * Best-effort inline statics extraction for small uploads (see
109
- * `maxInlineUnzipSize`). Never throws: failures are logged and the capture
110
- * job backfills the same keys idempotently.
111
- */
112
- async function persistInlineStatics(storage, scratchDir, maxInlineBytes, contentLength, projectId, buildId, logger) {
113
- if (!scratchDir || !maxInlineBytes) return;
114
- const size = parseContentLength(contentLength);
115
- if (size === void 0 || size > maxInlineBytes) return;
116
- try {
117
- const extractedDir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId);
118
- await persistStorybookStatics(storage, extractedDir, projectId, buildId);
119
- logger.info({
120
- buildId,
121
- size
122
- }, "upload statics persisted inline");
123
- } catch (error) {
124
- logger.error({
125
- err: error,
126
- buildId
127
- }, "inline statics persist failed; capture will backfill");
128
- }
129
- }
130
- /** Query filters accepted by the build list endpoint. */
131
- const buildListQuery = z.object({
132
- status: z.enum(BUILD_STATUSES).optional(),
133
- branch: z.string().optional(),
134
- labelKey: z.string().optional(),
135
- labelValue: z.string().optional()
136
- });
137
- /** Fetch a build scoped to its project, throwing 404 when it does not belong. */
138
- async function buildForProject(projectId, buildId) {
139
- const build = await new BuildModel(getStore().db).get(buildId);
140
- if (!build || build.projectId !== projectId) notFound("Build not found");
141
- return build;
142
- }
143
- /** Fetch a snapshot scoped to its build, throwing 404 when it does not belong. */
144
- async function snapshotForBuild(build, snapshotId) {
145
- const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);
146
- if (!snapshot || snapshot.buildId !== build.id) notFound("Snapshot not found");
147
- return snapshot;
148
- }
149
- /** Recompute a build's counts and roll its status up from its snapshots. */
150
- async function refreshBuild(buildId) {
151
- const { db, config } = getStore();
152
- await new BuildModel(db).updateCounts(buildId);
153
- const snapshots = await new SnapshotModel(db).listByBuild(buildId);
154
- const unresolved = snapshots.some((s) => s.status === "new" || s.status === "changed");
155
- let status;
156
- if (unresolved) status = "reviewing";
157
- else status = snapshots.some((s) => s.status === "rejected") ? "rejected" : "approved";
158
- const build = await new BuildModel(db).get(buildId);
159
- if (build) {
160
- await new BuildModel(db).setStatus(buildId, status);
161
- await emitWebhookEvent(db, build.projectId, `build:${status}`, {
162
- buildId,
163
- status,
164
- snapshotCount: snapshots.length
165
- }, config.secret);
166
- }
167
- }
168
- /** Approve a snapshot, promote its screenshot to baseline, and refresh the build. */
169
- async function approveSnapshot(snapshotId, userId) {
170
- const { db, config } = getStore();
171
- const snapshots = new SnapshotModel(db);
172
- const snapshot = await snapshots.get(snapshotId);
173
- if (!snapshot) notFound("Snapshot not found");
174
- const project = await new ProjectModel(db).get(snapshot.projectId);
175
- const build = await new BuildModel(db).get(snapshot.buildId);
176
- if (!project || !build) notFound("Project or build not found");
177
- await snapshots.review(snapshotId, "approved", userId);
178
- await new BaselineModel(db, getStore().storage, config.secret).upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);
179
- await refreshBuild(build.id);
180
- }
181
- //#endregion
182
- export { APPROVER_ROLES, DEVELOPER_ROLES, VIEW_ROLES, approveSnapshot, buildCreateJsonSchema, buildForProject, buildListQuery, createBuildRecord, persistInlineStatics, refreshBuild, snapshotForBuild, storeUploadStream };
183
-
184
- //# sourceMappingURL=builds.handlers.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { Readable, Transform } from \"node:stream\";\nimport type { ReadableStream as NodeWebStream } from \"node:stream/web\";\nimport type { Logger } from \"pino\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { extractStorybookToScratch, persistStorybookStatics } from \"../capture/statics.ts\";\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { LabelModel } from \"../models/label.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { getStore } from \"../store.ts\";\nimport { type ProjectRole, BUILD_STATUSES } from \"../types.ts\";\nimport { notFound } from \"./helpers.ts\";\n\n/** Roles permitted to view builds, snapshots, and comments. */\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n/** Roles permitted to upload builds and write comments. */\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\n/** Roles permitted to approve or reject snapshots and delete builds. */\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\n/** JSON input schema for creating a build before streaming its bundle. */\nexport const buildCreateJsonSchema = z\n .object({\n gitSha: z.string().min(1),\n gitBranch: z.string().min(1),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n labels: z.array(z.object({ key: z.string().min(1), value: z.string() })).optional(),\n })\n .openapi(\"BuildCreate\");\n\n/** Metadata accepted by either build-creation route. */\nexport interface BuildCreateMetadata {\n gitSha: string;\n gitBranch: string;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n labels?: { key: string; value: string }[];\n}\n\n/* oxlint-disable eslint/no-await-in-loop -- label attach is intentionally sequential */\nasync function attachLabels(\n db: import(\"../adapters/database.ts\").DatabaseAdapter,\n projectId: string,\n buildId: string,\n labels: { key: string; value: string }[],\n): Promise<void> {\n const models = new LabelModel(db);\n for (const { key, value } of labels) {\n const existing = await models.getType(projectId, key);\n if (!existing) {\n await models.createType(projectId, { key, name: key });\n }\n await models.attach(projectId, buildId, key, value);\n }\n}\n/* oxlint-enable eslint/no-await-in-loop */\n\n/**\n * Create a build record with labels and the `build:created` webhook.\n * Shared by the build-creation route.\n */\nexport async function createBuildRecord(\n project: Project,\n meta: BuildCreateMetadata,\n): Promise<import(\"../schema/build.ts\").Build> {\n const { db, config } = getStore();\n if (!meta.gitSha || !meta.gitBranch) {\n throw new HTTPException(400, { message: \"gitSha and gitBranch are required\" });\n }\n const build = await new BuildModel(db).create(project.id, {\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n isDefault: meta.gitBranch === project.gitDefaultBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n });\n if (meta.labels && meta.labels.length > 0) {\n await attachLabels(db, project.id, build.id, meta.labels);\n }\n await emitWebhookEvent(\n db,\n project.id,\n \"build:created\",\n {\n buildId: build.id,\n gitSha: meta.gitSha,\n gitBranch: meta.gitBranch,\n authorEmail: meta.authorEmail,\n authorName: meta.authorName,\n message: meta.message,\n },\n config.secret,\n );\n return build;\n}\n\n/**\n * Stream a raw request body into storage with a byte cap.\n * Throws 400 for a missing body, 413 once `maxBytes` is exceeded.\n */\nexport async function storeUploadStream(\n storage: StorageAdapter,\n path: string,\n body: ReadableStream<Uint8Array> | null,\n maxBytes: number,\n): Promise<void> {\n if (!body) {\n throw new HTTPException(400, { message: \"Missing request body\" });\n }\n let seen = 0;\n const limiter = new Transform({\n transform(\n chunk: Buffer,\n _encoding: string,\n callback: (error?: Error | null, data?: Buffer) => void,\n ): void {\n seen += chunk.length;\n if (seen > maxBytes) {\n callback(new Error(`Upload exceeds ${maxBytes} byte limit`));\n } else {\n callback(null, chunk);\n }\n },\n });\n try {\n const nodeBody = Readable.fromWeb(body as unknown as NodeWebStream);\n await storage.writeStream(path, nodeBody.pipe(limiter));\n } catch (error) {\n if (seen > maxBytes) {\n throw new HTTPException(413, { message: \"Upload exceeds size limit\" });\n }\n throw error;\n }\n}\n\n/** Parse a Content-Length header value into bytes, or undefined when unknown. */\nfunction parseContentLength(value: string | undefined): number | undefined {\n if (!value) {\n return undefined;\n }\n const parsed = Math.trunc(Number(value));\n return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;\n}\n\n/**\n * Best-effort inline statics extraction for small uploads (see\n * `maxInlineUnzipSize`). Never throws: failures are logged and the capture\n * job backfills the same keys idempotently.\n */\nexport async function persistInlineStatics(\n storage: StorageAdapter,\n scratchDir: string | undefined,\n maxInlineBytes: number | undefined,\n contentLength: string | undefined,\n projectId: string,\n buildId: string,\n logger: Logger,\n): Promise<void> {\n if (!scratchDir || !maxInlineBytes) {\n return;\n }\n const size = parseContentLength(contentLength);\n if (size === undefined || size > maxInlineBytes) {\n return;\n }\n try {\n const extractedDir = await extractStorybookToScratch(storage, scratchDir, projectId, buildId);\n await persistStorybookStatics(storage, extractedDir, projectId, buildId);\n logger.info({ buildId, size }, \"upload statics persisted inline\");\n } catch (error) {\n logger.error({ err: error, buildId }, \"inline statics persist failed; capture will backfill\");\n }\n}\n/** Query filters accepted by the build list endpoint. */\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n labelKey: z.string().optional(),\n labelValue: z.string().optional(),\n});\n\n/** Fetch a build scoped to its project, throwing 404 when it does not belong. */\nexport async function buildForProject(\n projectId: string,\n buildId: string,\n): Promise<import(\"../schema/build.ts\").Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\n/** Fetch a snapshot scoped to its build, throwing 404 when it does not belong. */\nexport async function snapshotForBuild(\n build: { id: string },\n snapshotId: string,\n): Promise<import(\"../schema/snapshot.ts\").Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\n/** Recompute a build's counts and roll its status up from its snapshots. */\nexport async function refreshBuild(buildId: string): Promise<void> {\n const { db, config } = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const unresolved = snapshots.some((s) => s.status === \"new\" || s.status === \"changed\");\n let status: \"reviewing\" | \"rejected\" | \"approved\";\n if (unresolved) {\n status = \"reviewing\";\n } else {\n const rejected = snapshots.some((s) => s.status === \"rejected\");\n status = rejected ? \"rejected\" : \"approved\";\n }\n const build = await new BuildModel(db).get(buildId);\n if (build) {\n await new BuildModel(db).setStatus(buildId, status);\n await emitWebhookEvent(\n db,\n build.projectId,\n `build:${status}`,\n {\n buildId,\n status,\n snapshotCount: snapshots.length,\n },\n config.secret,\n );\n }\n}\n\n/** Approve a snapshot, promote its screenshot to baseline, and refresh the build. */\nexport async function approveSnapshot(snapshotId: string, userId: string): Promise<void> {\n const { db, config } = getStore();\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, getStore().storage, config.secret);\n await baselines.upsert(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n snapshot.id,\n snapshot.screenshotPath,\n );\n await refreshBuild(build.id);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,MAAa,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;;AAE7F,MAAa,kBAA0C;CAAC;CAAa;CAAY;AAAO;;AAExF,MAAa,iBAAyC,CAAC,YAAY,OAAO;;AAG1E,MAAa,wBAAwB,EAClC,OAAO;CACN,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CACxB,WAAW,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;CAC3B,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;CAChC,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,QAAQ,EAAE,MAAM,EAAE,OAAO;EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC;EAAG,OAAO,EAAE,OAAO;CAAE,CAAC,CAAC,CAAC,CAAC,SAAS;AACpF,CAAC,CAAC,CACD,QAAQ,aAAa;AAaxB,eAAe,aACb,IACA,WACA,SACA,QACe;CACf,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,KAAK,MAAM,EAAE,KAAK,WAAW,QAAQ;EAEnC,IAAI,CAAC,MADkB,OAAO,QAAQ,WAAW,GAAG,GAElD,MAAM,OAAO,WAAW,WAAW;GAAE;GAAK,MAAM;EAAI,CAAC;EAEvD,MAAM,OAAO,OAAO,WAAW,SAAS,KAAK,KAAK;CACpD;AACF;;;;;AAOA,eAAsB,kBACpB,SACA,MAC6C;CAC7C,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,IAAI,CAAC,KAAK,UAAU,CAAC,KAAK,WACxB,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,oCAAoC,CAAC;CAE/E,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI;EACxD,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,WAAW,KAAK,cAAc,QAAQ;EACtC,aAAa,KAAK;EAClB,YAAY,KAAK;EACjB,SAAS,KAAK;CAChB,CAAC;CACD,IAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GACtC,MAAM,aAAa,IAAI,QAAQ,IAAI,MAAM,IAAI,KAAK,MAAM;CAE1D,MAAM,iBACJ,IACA,QAAQ,IACR,iBACA;EACE,SAAS,MAAM;EACf,QAAQ,KAAK;EACb,WAAW,KAAK;EAChB,aAAa,KAAK;EAClB,YAAY,KAAK;EACjB,SAAS,KAAK;CAChB,GACA,OAAO,MACT;CACA,OAAO;AACT;;;;;AAMA,eAAsB,kBACpB,SACA,MACA,MACA,UACe;CACf,IAAI,CAAC,MACH,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,uBAAuB,CAAC;CAElE,IAAI,OAAO;CACX,MAAM,UAAU,IAAI,UAAU,EAC5B,UACE,OACA,WACA,UACM;EACN,QAAQ,MAAM;EACd,IAAI,OAAO,UACT,yBAAS,IAAI,MAAM,kBAAkB,SAAS,YAAY,CAAC;OAE3D,SAAS,MAAM,KAAK;CAExB,EACF,CAAC;CACD,IAAI;EACF,MAAM,WAAW,SAAS,QAAQ,IAAgC;EAClE,MAAM,QAAQ,YAAY,MAAM,SAAS,KAAK,OAAO,CAAC;CACxD,SAAS,OAAO;EACd,IAAI,OAAO,UACT,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,4BAA4B,CAAC;EAEvE,MAAM;CACR;AACF;;AAGA,SAAS,mBAAmB,OAA+C;CACzE,IAAI,CAAC,OACH;CAEF,MAAM,SAAS,KAAK,MAAM,OAAO,KAAK,CAAC;CACvC,OAAO,OAAO,cAAc,MAAM,KAAK,SAAS,IAAI,SAAS,KAAA;AAC/D;;;;;;AAOA,eAAsB,qBACpB,SACA,YACA,gBACA,eACA,WACA,SACA,QACe;CACf,IAAI,CAAC,cAAc,CAAC,gBAClB;CAEF,MAAM,OAAO,mBAAmB,aAAa;CAC7C,IAAI,SAAS,KAAA,KAAa,OAAO,gBAC/B;CAEF,IAAI;EACF,MAAM,eAAe,MAAM,0BAA0B,SAAS,YAAY,WAAW,OAAO;EAC5F,MAAM,wBAAwB,SAAS,cAAc,WAAW,OAAO;EACvE,OAAO,KAAK;GAAE;GAAS;EAAK,GAAG,iCAAiC;CAClE,SAAS,OAAO;EACd,OAAO,MAAM;GAAE,KAAK;GAAO;EAAQ,GAAG,sDAAsD;CAC9F;AACF;;AAEA,MAAa,iBAAiB,EAAE,OAAO;CACrC,QAAQ,EAAE,KAAK,cAAc,CAAC,CAAC,SAAS;CACxC,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;CAC5B,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS;CAC9B,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;AAClC,CAAC;;AAGD,eAAsB,gBACpB,WACA,SAC6C;CAC7C,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO;CAC7D,IAAI,CAAC,SAAS,MAAM,cAAc,WAChC,SAAS,iBAAiB;CAE5B,OAAO;AACT;;AAGA,eAAsB,iBACpB,OACA,YACmD;CACnD,MAAM,WAAW,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,UAAU;CACtE,IAAI,CAAC,YAAY,SAAS,YAAY,MAAM,IAC1C,SAAS,oBAAoB;CAE/B,OAAO;AACT;;AAGA,eAAsB,aAAa,SAAgC;CACjE,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,aAAa,OAAO;CAC7C,MAAM,YAAY,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,YAAY,OAAO;CACjE,MAAM,aAAa,UAAU,MAAM,MAAM,EAAE,WAAW,SAAS,EAAE,WAAW,SAAS;CACrF,IAAI;CACJ,IAAI,YACF,SAAS;MAGT,SADiB,UAAU,MAAM,MAAM,EAAE,WAAW,UACpC,IAAI,aAAa;CAEnC,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,OAAO;CAClD,IAAI,OAAO;EACT,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,UAAU,SAAS,MAAM;EAClD,MAAM,iBACJ,IACA,MAAM,WACN,SAAS,UACT;GACE;GACA;GACA,eAAe,UAAU;EAC3B,GACA,OAAO,MACT;CACF;AACF;;AAGA,eAAsB,gBAAgB,YAAoB,QAA+B;CACvF,MAAM,EAAE,IAAI,WAAW,SAAS;CAChC,MAAM,YAAY,IAAI,cAAc,EAAE;CACtC,MAAM,WAAW,MAAM,UAAU,IAAI,UAAU;CAC/C,IAAI,CAAC,UACH,SAAS,oBAAoB;CAE/B,MAAM,UAAU,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,SAAS,SAAS;CACjE,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,SAAS,OAAO;CAC3D,IAAI,CAAC,WAAW,CAAC,OACf,SAAS,4BAA4B;CAEvC,MAAM,UAAU,OAAO,YAAY,YAAY,MAAM;CAErD,MAAM,IADgB,cAAc,IAAI,SAAS,CAAC,CAAC,SAAS,OAAO,MACrD,CAAC,CAAC,OACd,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,SAAS,IACT,SAAS,cACX;CACA,MAAM,aAAa,MAAM,EAAE;AAC7B"}