@storyshelf/app 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/dist/app-types.d.mts +1 -3
  2. package/dist/asset-manifest.mjs +1 -2
  3. package/dist/assets/htmx.mjs +1 -2
  4. package/dist/assets/mark.mjs +1 -2
  5. package/dist/boot-tasks.mjs +1 -2
  6. package/dist/build-state-flags.mjs +1 -2
  7. package/dist/build-state-setup.mjs +1 -2
  8. package/dist/capture-alerts.mjs +1 -2
  9. package/dist/capture-process.mjs +1 -2
  10. package/dist/capture-setup.mjs +1 -2
  11. package/dist/connections.mjs +1 -2
  12. package/dist/consumer-setup.mjs +1 -2
  13. package/dist/credential-keys.mjs +1 -2
  14. package/dist/credentials.mjs +1 -2
  15. package/dist/git-status.mjs +1 -2
  16. package/dist/hidden-data.mjs +1 -2
  17. package/dist/index.d.mts +0 -2
  18. package/dist/index.mjs +2 -3
  19. package/dist/insights/budget.mjs +1 -2
  20. package/dist/insights/deps.mjs +1 -2
  21. package/dist/insights/errors.mjs +1 -2
  22. package/dist/insights/health-panel.mjs +0 -1
  23. package/dist/insights/job.mjs +1 -2
  24. package/dist/insights/load-health.mjs +1 -2
  25. package/dist/insights/load-triage.mjs +1 -2
  26. package/dist/insights/panel.mjs +1 -2
  27. package/dist/insights/request.mjs +1 -2
  28. package/dist/insights/stale.mjs +1 -2
  29. package/dist/insights/usage-report.mjs +1 -2
  30. package/dist/insights/view.mjs +1 -2
  31. package/dist/job-canceller.mjs +1 -2
  32. package/dist/job-queue.mjs +1 -2
  33. package/dist/jobs-setup.mjs +1 -2
  34. package/dist/lifecycle.mjs +2 -3
  35. package/dist/lifecycle.mjs.map +1 -1
  36. package/dist/middleware/auth-gate.mjs +1 -2
  37. package/dist/middleware/csrf.mjs +1 -2
  38. package/dist/middleware/rate-limit.mjs +1 -2
  39. package/dist/middleware/request-log.mjs +1 -2
  40. package/dist/middleware/require-review.mjs +1 -2
  41. package/dist/middleware/require-testing.mjs +1 -2
  42. package/dist/middleware/server-timing.mjs +1 -2
  43. package/dist/middleware/setup-gate.mjs +1 -2
  44. package/dist/middleware/store-scope.mjs +1 -2
  45. package/dist/notifications-pause.mjs +1 -2
  46. package/dist/notify.d.mts +1 -3
  47. package/dist/notify.mjs +1 -2
  48. package/dist/pages/a11y-violations.mjs +1 -2
  49. package/dist/pages/admin-ai.mjs +1 -2
  50. package/dist/pages/admin-system-origin.mjs +19 -10
  51. package/dist/pages/admin-system-origin.mjs.map +1 -1
  52. package/dist/pages/admin-system-targets.mjs +1 -2
  53. package/dist/pages/admin-system.mjs +1 -2
  54. package/dist/pages/build-detail.mjs +1 -2
  55. package/dist/pages/build-diff-actions.mjs +1 -2
  56. package/dist/pages/build-diff-comments.mjs +1 -2
  57. package/dist/pages/build-diff-default.mjs +53 -0
  58. package/dist/pages/build-diff-default.mjs.map +1 -0
  59. package/dist/pages/build-diff-environment.mjs +1 -2
  60. package/dist/pages/build-diff-header.mjs +1 -2
  61. package/dist/pages/build-diff-insight.mjs +1 -2
  62. package/dist/pages/build-diff-model.mjs +20 -3
  63. package/dist/pages/build-diff-model.mjs.map +1 -1
  64. package/dist/pages/build-diff-nav.mjs +1 -2
  65. package/dist/pages/build-diff-reference.mjs +84 -0
  66. package/dist/pages/build-diff-reference.mjs.map +1 -0
  67. package/dist/pages/build-diff-shortcuts.mjs +1 -2
  68. package/dist/pages/build-diff-stale.mjs +1 -2
  69. package/dist/pages/build-diff-usage.mjs +1 -2
  70. package/dist/pages/build-diff-viewer.mjs +38 -12
  71. package/dist/pages/build-diff-viewer.mjs.map +1 -1
  72. package/dist/pages/build-diff.mjs +6 -12
  73. package/dist/pages/build-diff.mjs.map +1 -1
  74. package/dist/pages/build-tests.mjs +1 -2
  75. package/dist/pages/component-detail.mjs +1 -2
  76. package/dist/pages/component-stories.mjs +1 -2
  77. package/dist/pages/compute-jobs.mjs +1 -2
  78. package/dist/pages/design-links.mjs +1 -2
  79. package/dist/pages/environment-change.mjs +1 -2
  80. package/dist/pages/health-panel.mjs +1 -2
  81. package/dist/pages/invite.mjs +1 -2
  82. package/dist/pages/label-detail.mjs +1 -2
  83. package/dist/pages/library-grid.mjs +1 -2
  84. package/dist/pages/library-model.mjs +1 -2
  85. package/dist/pages/library-snapshots.mjs +1 -2
  86. package/dist/pages/library.mjs +1 -2
  87. package/dist/pages/login.mjs +1 -2
  88. package/dist/pages/notifications-paused.mjs +1 -2
  89. package/dist/pages/passkey-ceremony.mjs +1 -2
  90. package/dist/pages/profile.mjs +1 -2
  91. package/dist/pages/project-builds-table.mjs +1 -2
  92. package/dist/pages/project-builds.mjs +1 -2
  93. package/dist/pages/project-create.mjs +1 -2
  94. package/dist/pages/project-settings.mjs +1 -2
  95. package/dist/pages/projects-card.mjs +1 -2
  96. package/dist/pages/projects.mjs +1 -2
  97. package/dist/pages/review-changeset.mjs +1 -2
  98. package/dist/pages/review-comments.mjs +1 -2
  99. package/dist/pages/review-verdict.mjs +1 -2
  100. package/dist/pages/root.mjs +1 -2
  101. package/dist/pages/settings-ai.mjs +1 -2
  102. package/dist/pages/settings-capture.mjs +1 -2
  103. package/dist/pages/settings-design-credential.mjs +1 -2
  104. package/dist/pages/settings-design-links.mjs +1 -2
  105. package/dist/pages/settings-design.mjs +1 -2
  106. package/dist/pages/settings-general.mjs +1 -2
  107. package/dist/pages/settings-labels.mjs +1 -2
  108. package/dist/pages/settings-links.mjs +1 -2
  109. package/dist/pages/settings-members.mjs +1 -2
  110. package/dist/pages/settings-notifications-git.mjs +1 -2
  111. package/dist/pages/settings-notifications.mjs +1 -2
  112. package/dist/pages/settings-tests.mjs +1 -2
  113. package/dist/pages/settings-tokens.mjs +1 -2
  114. package/dist/pages/settings-webhooks.mjs +1 -2
  115. package/dist/pages/storybook.mjs +1 -2
  116. package/dist/pages/target-choices.mjs +1 -2
  117. package/dist/publish-setup.mjs +1 -2
  118. package/dist/retention-timer.mjs +10 -11
  119. package/dist/retention-timer.mjs.map +1 -1
  120. package/dist/review-events.mjs +1 -2
  121. package/dist/routers/admin-pages.mjs +1 -2
  122. package/dist/routers/admin-public-origin.mjs +10 -3
  123. package/dist/routers/admin-public-origin.mjs.map +1 -1
  124. package/dist/routers/admin.mjs +8 -4
  125. package/dist/routers/admin.mjs.map +1 -1
  126. package/dist/routers/assets.mjs +1 -2
  127. package/dist/routers/attempts.mjs +1 -2
  128. package/dist/routers/auth-sync.mjs +1 -2
  129. package/dist/routers/auth.mjs +1 -2
  130. package/dist/routers/baseline-guard.mjs +2 -3
  131. package/dist/routers/baseline-guard.mjs.map +1 -1
  132. package/dist/routers/baseline-reference.mjs +72 -0
  133. package/dist/routers/baseline-reference.mjs.map +1 -0
  134. package/dist/routers/baselines.handlers.mjs +1 -2
  135. package/dist/routers/baselines.mjs +1 -2
  136. package/dist/routers/build-cancel.mjs +1 -2
  137. package/dist/routers/build-changeset.mjs +1 -2
  138. package/dist/routers/build-reviews.handlers.mjs +1 -2
  139. package/dist/routers/build-reviews.mjs +1 -2
  140. package/dist/routers/builds.handlers.mjs +23 -9
  141. package/dist/routers/builds.handlers.mjs.map +1 -1
  142. package/dist/routers/builds.mjs +7 -3
  143. package/dist/routers/builds.mjs.map +1 -1
  144. package/dist/routers/capture-cache.mjs +29 -0
  145. package/dist/routers/capture-cache.mjs.map +1 -0
  146. package/dist/routers/capture-config.mjs +1 -2
  147. package/dist/routers/comments.mjs +1 -2
  148. package/dist/routers/comparison-image.mjs +29 -0
  149. package/dist/routers/comparison-image.mjs.map +1 -0
  150. package/dist/routers/component-catalog.handlers.mjs +1 -2
  151. package/dist/routers/component.handlers.mjs +1 -2
  152. package/dist/routers/connections.mjs +1 -2
  153. package/dist/routers/design-connect.handlers.mjs +1 -2
  154. package/dist/routers/design.handlers.mjs +1 -2
  155. package/dist/routers/flash.mjs +1 -2
  156. package/dist/routers/health-report.mjs +1 -2
  157. package/dist/routers/health.mjs +1 -2
  158. package/dist/routers/helpers.mjs +1 -2
  159. package/dist/routers/htmx.mjs +1 -2
  160. package/dist/routers/insights-ui.mjs +1 -2
  161. package/dist/routers/insights.handlers.mjs +1 -2
  162. package/dist/routers/insights.mjs +1 -2
  163. package/dist/routers/labels.mjs +1 -2
  164. package/dist/routers/links.handlers.mjs +1 -2
  165. package/dist/routers/links.mjs +1 -2
  166. package/dist/routers/media.mjs +14 -5
  167. package/dist/routers/media.mjs.map +1 -1
  168. package/dist/routers/members.mjs +1 -2
  169. package/dist/routers/notification-channels.mjs +1 -2
  170. package/dist/routers/notification-subscriptions.mjs +1 -2
  171. package/dist/routers/oauth-state.mjs +1 -2
  172. package/dist/routers/preview-base.mjs +1 -2
  173. package/dist/routers/preview-token.mjs +1 -2
  174. package/dist/routers/profile.mjs +1 -2
  175. package/dist/routers/projects.mjs +1 -2
  176. package/dist/routers/publish-upload.mjs +19 -12
  177. package/dist/routers/publish-upload.mjs.map +1 -1
  178. package/dist/routers/published-access.mjs +1 -2
  179. package/dist/routers/published-compression.mjs +109 -0
  180. package/dist/routers/published-compression.mjs.map +1 -0
  181. package/dist/routers/published-files.mjs +26 -9
  182. package/dist/routers/published-files.mjs.map +1 -1
  183. package/dist/routers/review-forms.mjs +1 -2
  184. package/dist/routers/review-next.mjs +1 -2
  185. package/dist/routers/review-section.handlers.mjs +1 -2
  186. package/dist/routers/review-switch-off.mjs +1 -2
  187. package/dist/routers/schemas.mjs +1 -2
  188. package/dist/routers/scoped-models.mjs +1 -2
  189. package/dist/routers/settings-ai.mjs +1 -2
  190. package/dist/routers/settings-capture.mjs +1 -2
  191. package/dist/routers/settings-design.mjs +1 -2
  192. package/dist/routers/settings-general.mjs +1 -2
  193. package/dist/routers/settings-git-channel.mjs +1 -2
  194. package/dist/routers/settings-labels.mjs +1 -2
  195. package/dist/routers/settings-links.mjs +1 -2
  196. package/dist/routers/settings-members.mjs +1 -2
  197. package/dist/routers/settings-notifications.mjs +1 -2
  198. package/dist/routers/settings-tokens.mjs +1 -2
  199. package/dist/routers/settings-webhooks.mjs +1 -2
  200. package/dist/routers/settings.handlers.mjs +1 -2
  201. package/dist/routers/settings.mjs +1 -2
  202. package/dist/routers/signed-storybook.mjs +5 -3
  203. package/dist/routers/signed-storybook.mjs.map +1 -1
  204. package/dist/routers/snapshot-groups.mjs +1 -2
  205. package/dist/routers/snapshots.mjs +1 -2
  206. package/dist/routers/storybook.mjs +21 -17
  207. package/dist/routers/storybook.mjs.map +1 -1
  208. package/dist/routers/tokens.mjs +1 -2
  209. package/dist/routers/ui.mjs +4 -2
  210. package/dist/routers/ui.mjs.map +1 -1
  211. package/dist/routers/webhooks.mjs +1 -2
  212. package/dist/routers/well-known.mjs +1 -2
  213. package/dist/routes/api-routes.mjs +1 -2
  214. package/dist/routes/classification.mjs +1 -2
  215. package/dist/routes/openapi-classes.mjs +1 -2
  216. package/dist/routes/page-routes.mjs +2 -3
  217. package/dist/routes/page-routes.mjs.map +1 -1
  218. package/dist/runtime.mjs +1 -2
  219. package/dist/serverless.mjs +1 -2
  220. package/dist/store.mjs +1 -2
  221. package/dist/targets.mjs +1 -2
  222. package/dist/ui/avatar.mjs +1 -2
  223. package/dist/ui/buttons.mjs +1 -2
  224. package/dist/ui/code-block.mjs +1 -2
  225. package/dist/ui/compare-stage.mjs +86 -54
  226. package/dist/ui/compare-stage.mjs.map +1 -1
  227. package/dist/ui/csrf-field.mjs +1 -2
  228. package/dist/ui/css.mjs +1 -2
  229. package/dist/ui/design-reference.mjs +1 -2
  230. package/dist/ui/document.mjs +1 -2
  231. package/dist/ui/dropdown.mjs +1 -2
  232. package/dist/ui/feedback.mjs +1 -2
  233. package/dist/ui/filter-input.mjs +1 -2
  234. package/dist/ui/forms.mjs +1 -2
  235. package/dist/ui/icons/icon.mjs +1 -2
  236. package/dist/ui/icons/paths.mjs +1 -2
  237. package/dist/ui/icons/sprite.mjs +1 -2
  238. package/dist/ui/kbd.mjs +1 -2
  239. package/dist/ui/layout.mjs +1 -2
  240. package/dist/ui/preview-embed.mjs +1 -2
  241. package/dist/ui/progress.mjs +1 -2
  242. package/dist/ui/segmented.mjs +1 -2
  243. package/dist/ui/shell/account.mjs +1 -2
  244. package/dist/ui/shell/bare.mjs +1 -2
  245. package/dist/ui/shell/boot-script.mjs +1 -2
  246. package/dist/ui/shell/client-script.mjs +1 -2
  247. package/dist/ui/shell/nav.mjs +1 -2
  248. package/dist/ui/shell/review-script.mjs +17 -3
  249. package/dist/ui/shell/review-script.mjs.map +1 -1
  250. package/dist/ui/shell/sidebar.mjs +1 -2
  251. package/dist/ui/shell/topbar.mjs +1 -2
  252. package/dist/ui/stacks.mjs +1 -2
  253. package/dist/ui/styles/base.mjs +1 -2
  254. package/dist/ui/styles/behavior.mjs +1 -2
  255. package/dist/ui/styles/review-layout.mjs +1 -2
  256. package/dist/ui/styles/review.mjs +1 -2
  257. package/dist/ui/styles/tokens.mjs +1 -2
  258. package/dist/ui/styles.mjs +1 -2
  259. package/dist/ui/subnav.mjs +1 -2
  260. package/dist/ui/table.mjs +1 -2
  261. package/dist/ui/theme.mjs +1 -2
  262. package/dist/ui/thumbnail.mjs +1 -2
  263. package/dist/ui/time.mjs +1 -2
  264. package/package.json +9 -9
@@ -3,9 +3,11 @@ import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
5
  import { publishedCorsHeaders } from "./published-access.mjs";
6
+ import { SIGNED_NO_STORE, signedCacheControl } from "./capture-cache.mjs";
7
+ import { acceptsGzip, publishedGzip } from "./published-compression.mjs";
6
8
  import { isSafeSegment, mimeFor } from "@storyshelf/core/utils";
7
- import { readPublishedFile } from "@storyshelf/core/capture";
8
- //#region src/routers/published-files.ts
9
+ import { readPublishedFileInfo } from "@storyshelf/core/capture";
10
+ // src/routers/published-files.ts
9
11
  /**
10
12
  * Serving a build's published files (ADR 0010, ADR 0007 decision 5): the single place that decodes
11
13
  * and checks a request path, reads the file through the build's manifest and sets the headers.
@@ -13,9 +15,13 @@ import { readPublishedFile } from "@storyshelf/core/capture";
13
15
  * signed URL route (`/_s/:token/*`) all end here, each bringing the access {@link publishedAccess}
14
16
  * granted.
15
17
  */
16
- /** Headers of every signed URL answer: a token URL must not be cached, indexed or leaked by `Referer`. */
18
+ /**
19
+ * Headers of every signed URL answer: a token URL must not be indexed or leaked by `Referer`, and
20
+ * is `no-store` here. Only a successful answer for a capture grant may replace `cache-control`
21
+ * (`signedCacheControl`); an error always sends these as they are.
22
+ */
17
23
  const SIGNED_URL_HEADERS = {
18
- "cache-control": "private, no-store",
24
+ "cache-control": SIGNED_NO_STORE,
19
25
  "referrer-policy": "no-referrer",
20
26
  "x-robots-tag": "noindex, nofollow",
21
27
  "x-content-type-options": "nosniff"
@@ -37,16 +43,27 @@ function publishedRelativePath(path, base) {
37
43
  /** The file as a response, or `null` when the build has no such file. */
38
44
  async function servePublishedFile(c, request) {
39
45
  const { projectId, buildId, rel, access } = request;
40
- const file = await readPublishedFile(getStore().storage, projectId, buildId, rel);
46
+ const { storage, config } = getStore();
47
+ const file = await readPublishedFileInfo(storage, projectId, buildId, rel);
41
48
  if (!file) return null;
42
- const headers = access === "token" ? SIGNED_URL_HEADERS : { "cache-control": request.cacheControl };
43
- return c.body(new Uint8Array(file), 200, {
49
+ const headers = access === "token" ? {
50
+ ...SIGNED_URL_HEADERS,
51
+ "cache-control": signedCacheControl({
52
+ cacheMaxAgeSeconds: config.captureAssets?.cacheMaxAgeSeconds,
53
+ grant: request.grant,
54
+ immutable: file.contentHash !== void 0
55
+ })
56
+ } : { "cache-control": request.cacheControl };
57
+ const mime = mimeFor(rel);
58
+ const compressed = config.publishedCompression && acceptsGzip(c.req.header("accept-encoding")) ? await publishedGzip(storage, file.contentHash, file.bytes, mime) : void 0;
59
+ return c.body(new Uint8Array(compressed ?? file.bytes), 200, {
44
60
  ...headers,
45
61
  ...publishedCorsHeaders(access),
46
- "content-type": mimeFor(rel)
62
+ "content-type": mime,
63
+ ...config.publishedCompression ? { vary: "Accept-Encoding" } : {},
64
+ ...compressed ? { "content-encoding": "gzip" } : {}
47
65
  });
48
66
  }
49
- //#endregion
50
67
  export { SIGNED_URL_HEADERS, publishedRelativePath, servePublishedFile };
51
68
 
52
69
  //# sourceMappingURL=published-files.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"published-files.mjs","names":[],"sources":["../../src/routers/published-files.ts"],"sourcesContent":["/**\n * Serving a build's published files (ADR 0010, ADR 0007 decision 5): the single place that decodes\n * and checks a request path, reads the file through the build's manifest and sets the headers.\n * The published Storybook routes (`/projects/:slug/storybook/build/:id/*`, `/_/:id/*`) and the\n * signed URL route (`/_s/:token/*`) all end here, each bringing the access {@link publishedAccess}\n * granted.\n */\nimport { readPublishedFile } from \"@storyshelf/core/capture\";\nimport { isSafeSegment, mimeFor } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { getStore } from \"../store.ts\";\nimport { publishedCorsHeaders, type PublishedAccess } from \"./published-access.ts\";\n\n/** Headers of every signed URL answer: a token URL must not be cached, indexed or leaked by `Referer`. */\nexport const SIGNED_URL_HEADERS = {\n \"cache-control\": \"private, no-store\",\n \"referrer-policy\": \"no-referrer\",\n \"x-robots-tag\": \"noindex, nofollow\",\n \"x-content-type-options\": \"nosniff\",\n} as const;\n\n/**\n * The file path after `base`, decoded once. `\"\"` is the root; `null` when the path is not under\n * `base` or could leave the build: empty, `.` or `..` segments, backslashes and NUL are refused.\n */\nexport function publishedRelativePath(path: string, base: string): string | null {\n if (!path.startsWith(base)) {\n return null;\n }\n let rest: string;\n try {\n rest = decodeURIComponent(path.slice(base.length));\n } catch {\n return null;\n }\n const safe =\n rest === \"\" ||\n rest\n .split(\"/\")\n .every(\n (segment) =>\n segment !== \"\" && segment !== \".\" && !segment.includes(\"\\0\") && isSafeSegment(segment),\n );\n return safe ? rest : null;\n}\n\n/** Where a file of a build lives and how the request got to it. */\nexport interface PublishedFileRequest {\n projectId: string;\n buildId: string;\n /** A path accepted by {@link publishedRelativePath}, not the root. */\n rel: string;\n access: PublishedAccess;\n /** `Cache-Control` of a normal answer; a signed URL always answers `private, no-store`. */\n cacheControl: string;\n}\n\n/** The file as a response, or `null` when the build has no such file. */\nexport async function servePublishedFile(\n c: Context,\n request: PublishedFileRequest,\n): Promise<Response | null> {\n const { projectId, buildId, rel, access } = request;\n const file = await readPublishedFile(getStore().storage, projectId, buildId, rel);\n if (!file) {\n return null;\n }\n const headers =\n access === \"token\" ? SIGNED_URL_HEADERS : { \"cache-control\": request.cacheControl };\n return c.body(new Uint8Array(file), 200, {\n ...headers,\n ...publishedCorsHeaders(access),\n \"content-type\": mimeFor(rel),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAcA,MAAa,qBAAqB;CAChC,iBAAiB;CACjB,mBAAmB;CACnB,gBAAgB;CAChB,0BAA0B;AAC5B;;;;;AAMA,SAAgB,sBAAsB,MAAc,MAA6B;CAC/E,IAAI,CAAC,KAAK,WAAW,IAAI,GACvB,OAAO;CAET,IAAI;CACJ,IAAI;EACF,OAAO,mBAAmB,KAAK,MAAM,KAAK,MAAM,CAAC;CACnD,QAAQ;EACN,OAAO;CACT;CASA,OAPE,SAAS,MACT,KACG,MAAM,GAAG,CAAC,CACV,OACE,YACC,YAAY,MAAM,YAAY,OAAO,CAAC,QAAQ,SAAS,IAAI,KAAK,cAAc,OAAO,CACzF,IACU,OAAO;AACvB;;AAcA,eAAsB,mBACpB,GACA,SAC0B;CAC1B,MAAM,EAAE,WAAW,SAAS,KAAK,WAAW;CAC5C,MAAM,OAAO,MAAM,kBAAkB,SAAS,CAAC,CAAC,SAAS,WAAW,SAAS,GAAG;CAChF,IAAI,CAAC,MACH,OAAO;CAET,MAAM,UACJ,WAAW,UAAU,qBAAqB,EAAE,iBAAiB,QAAQ,aAAa;CACpF,OAAO,EAAE,KAAK,IAAI,WAAW,IAAI,GAAG,KAAK;EACvC,GAAG;EACH,GAAG,qBAAqB,MAAM;EAC9B,gBAAgB,QAAQ,GAAG;CAC7B,CAAC;AACH"}
1
+ {"version":3,"file":"published-files.mjs","names":[],"sources":["../../src/routers/published-files.ts"],"sourcesContent":["/**\n * Serving a build's published files (ADR 0010, ADR 0007 decision 5): the single place that decodes\n * and checks a request path, reads the file through the build's manifest and sets the headers.\n * The published Storybook routes (`/projects/:slug/storybook/build/:id/*`, `/_/:id/*`) and the\n * signed URL route (`/_s/:token/*`) all end here, each bringing the access {@link publishedAccess}\n * granted.\n */\nimport { readPublishedFileInfo } from \"@storyshelf/core/capture\";\nimport { isSafeSegment, mimeFor, type StorybookTokenClaims } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { getStore } from \"../store.ts\";\nimport { signedCacheControl, SIGNED_NO_STORE } from \"./capture-cache.ts\";\nimport { publishedCorsHeaders, type PublishedAccess } from \"./published-access.ts\";\nimport { acceptsGzip, publishedGzip } from \"./published-compression.ts\";\n\n/**\n * Headers of every signed URL answer: a token URL must not be indexed or leaked by `Referer`, and\n * is `no-store` here. Only a successful answer for a capture grant may replace `cache-control`\n * (`signedCacheControl`); an error always sends these as they are.\n */\nexport const SIGNED_URL_HEADERS = {\n \"cache-control\": SIGNED_NO_STORE,\n \"referrer-policy\": \"no-referrer\",\n \"x-robots-tag\": \"noindex, nofollow\",\n \"x-content-type-options\": \"nosniff\",\n} as const;\n\n/**\n * The file path after `base`, decoded once. `\"\"` is the root; `null` when the path is not under\n * `base` or could leave the build: empty, `.` or `..` segments, backslashes and NUL are refused.\n */\nexport function publishedRelativePath(path: string, base: string): string | null {\n if (!path.startsWith(base)) {\n return null;\n }\n let rest: string;\n try {\n rest = decodeURIComponent(path.slice(base.length));\n } catch {\n return null;\n }\n const safe =\n rest === \"\" ||\n rest\n .split(\"/\")\n .every(\n (segment) =>\n segment !== \"\" && segment !== \".\" && !segment.includes(\"\\0\") && isSafeSegment(segment),\n );\n return safe ? rest : null;\n}\n\n/** Where a file of a build lives and how the request got to it. */\nexport interface PublishedFileRequest {\n projectId: string;\n buildId: string;\n /** A path accepted by {@link publishedRelativePath}, not the root. */\n rel: string;\n access: PublishedAccess;\n /** `Cache-Control` of a normal answer; a signed URL answers `signedCacheControl` instead. */\n cacheControl: string;\n /** The verified claims of a signed URL's token; decides whether its answer may be fresh. */\n grant?: StorybookTokenClaims | undefined;\n}\n\n/** The file as a response, or `null` when the build has no such file. */\nexport async function servePublishedFile(\n c: Context,\n request: PublishedFileRequest,\n): Promise<Response | null> {\n const { projectId, buildId, rel, access } = request;\n const { storage, config } = getStore();\n const file = await readPublishedFileInfo(storage, projectId, buildId, rel);\n if (!file) {\n return null;\n }\n const headers =\n access === \"token\"\n ? {\n ...SIGNED_URL_HEADERS,\n \"cache-control\": signedCacheControl({\n cacheMaxAgeSeconds: config.captureAssets?.cacheMaxAgeSeconds,\n grant: request.grant,\n immutable: file.contentHash !== undefined,\n }),\n }\n : { \"cache-control\": request.cacheControl };\n const mime = mimeFor(rel);\n const compressed =\n config.publishedCompression && acceptsGzip(c.req.header(\"accept-encoding\"))\n ? await publishedGzip(storage, file.contentHash, file.bytes, mime)\n : undefined;\n return c.body(new Uint8Array(compressed ?? file.bytes), 200, {\n ...headers,\n ...publishedCorsHeaders(access),\n \"content-type\": mime,\n ...(config.publishedCompression ? { vary: \"Accept-Encoding\" } : {}),\n ...(compressed ? { \"content-encoding\": \"gzip\" } : {}),\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAoBA,MAAa,qBAAqB;CAChC,iBAAiB;CACjB,mBAAmB;CACnB,gBAAgB;CAChB,0BAA0B;AAC5B;;;;;AAMA,SAAgB,sBAAsB,MAAc,MAA6B;CAC/E,IAAI,CAAC,KAAK,WAAW,IAAI,GACvB,OAAO;CAET,IAAI;CACJ,IAAI;EACF,OAAO,mBAAmB,KAAK,MAAM,KAAK,MAAM,CAAC;CACnD,QAAQ;EACN,OAAO;CACT;CASA,OAPE,SAAS,MACT,KACG,MAAM,GAAG,CAAC,CACV,OACE,YACC,YAAY,MAAM,YAAY,OAAO,CAAC,QAAQ,SAAS,IAAI,KAAK,cAAc,OAAO,CACzF,IACU,OAAO;AACvB;;AAgBA,eAAsB,mBACpB,GACA,SAC0B;CAC1B,MAAM,EAAE,WAAW,SAAS,KAAK,WAAW;CAC5C,MAAM,EAAE,SAAS,WAAW,SAAS;CACrC,MAAM,OAAO,MAAM,sBAAsB,SAAS,WAAW,SAAS,GAAG;CACzE,IAAI,CAAC,MACH,OAAO;CAET,MAAM,UACJ,WAAW,UACP;EACE,GAAG;EACH,iBAAiB,mBAAmB;GAClC,oBAAoB,OAAO,eAAe;GAC1C,OAAO,QAAQ;GACf,WAAW,KAAK,gBAAgB,KAAA;EAClC,CAAC;CACH,IACA,EAAE,iBAAiB,QAAQ,aAAa;CAC9C,MAAM,OAAO,QAAQ,GAAG;CACxB,MAAM,aACJ,OAAO,wBAAwB,YAAY,EAAE,IAAI,OAAO,iBAAiB,CAAC,IACtE,MAAM,cAAc,SAAS,KAAK,aAAa,KAAK,OAAO,IAAI,IAC/D,KAAA;CACN,OAAO,EAAE,KAAK,IAAI,WAAW,cAAc,KAAK,KAAK,GAAG,KAAK;EAC3D,GAAG;EACH,GAAG,qBAAqB,MAAM;EAC9B,gBAAgB;EAChB,GAAI,OAAO,uBAAuB,EAAE,MAAM,kBAAkB,IAAI,CAAC;EACjE,GAAI,aAAa,EAAE,oBAAoB,OAAO,IAAI,CAAC;CACrD,CAAC;AACH"}
@@ -9,7 +9,7 @@ import { APPROVER_ROLES, DEVELOPER_ROLES, buildForProject } from "./builds.handl
9
9
  import { giveVerdict } from "./build-reviews.handlers.mjs";
10
10
  import { createComment, resolveBuildComment } from "./comments.mjs";
11
11
  import { HTTPException } from "hono/http-exception";
12
- //#region src/routers/review-forms.ts
12
+ // src/routers/review-forms.ts
13
13
  /** Register the form routes of review (`review` class). */
14
14
  function registerReviewForms(app) {
15
15
  const base = "/projects/:slug/builds/:buildId";
@@ -66,7 +66,6 @@ function returnPath(c, buildId) {
66
66
  return fallback;
67
67
  }
68
68
  }
69
- //#endregion
70
69
  export { registerReviewForms };
71
70
 
72
71
  //# sourceMappingURL=review-forms.mjs.map
@@ -5,7 +5,7 @@ import { getStore } from "../store.mjs";
5
5
  import { hxRefresh, isHxRequest } from "./htmx.mjs";
6
6
  import { isOpenSnapshot } from "./baseline-guard.mjs";
7
7
  import { SnapshotModel } from "@storyshelf/core/models";
8
- //#region src/routers/review-next.ts
8
+ // src/routers/review-next.ts
9
9
  /**
10
10
  * The next snapshot still waiting for a decision after `currentId`, in list
11
11
  * order, wrapping to the start. Null when nothing else is open.
@@ -41,7 +41,6 @@ async function hxAdvance(c, target) {
41
41
  }
42
42
  c.header("HX-Redirect", url);
43
43
  }
44
- //#endregion
45
44
  export { hxAdvance, nextOpenSnapshotId };
46
45
 
47
46
  //# sourceMappingURL=review-next.mjs.map
@@ -6,7 +6,7 @@ import { currentProjectRole } from "./helpers.mjs";
6
6
  import { reviewStateOf } from "./build-reviews.handlers.mjs";
7
7
  import { publishingOf } from "@storyshelf/core/capture";
8
8
  import { CommentModel, UserModel, changesetOf } from "@storyshelf/core/models";
9
- //#region src/routers/review-section.handlers.ts
9
+ // src/routers/review-section.handlers.ts
10
10
  /** Data behind the review section of the build page (ADR 0013): verdicts, changeset and comments. */
11
11
  /** Whether review is on for the project (a row without the column counts as on). */
12
12
  function reviewIsOn(project) {
@@ -79,7 +79,6 @@ async function loadComponentReview(project, build) {
79
79
  canComment: capabilities.canComment
80
80
  };
81
81
  }
82
- //#endregion
83
82
  export { loadComponentReview, loadReviewSection, namesOf, reviewCapabilities, reviewIsOn };
84
83
 
85
84
  //# sourceMappingURL=review-section.handlers.mjs.map
@@ -4,7 +4,7 @@ import.meta.url;
4
4
  import { getStore } from "../store.mjs";
5
5
  import { requestBuildState } from "../git-status.mjs";
6
6
  import { acceptPendingWithoutWork } from "@storyshelf/core/models";
7
- //#region src/routers/review-switch-off.ts
7
+ // src/routers/review-switch-off.ts
8
8
  /** The switch-off pass at the app level (ADR 0013, decision 1): accept the queue review leaves behind. */
9
9
  /**
10
10
  * When a save turned review off (`before` had it on), accept the project's pending builds that no
@@ -23,7 +23,6 @@ async function runSwitchOffPass(before, after) {
23
23
  }, "review turned off: accepted pending builds");
24
24
  return accepted.length;
25
25
  }
26
- //#endregion
27
26
  export { runSwitchOffPass };
28
27
 
29
28
  //# sourceMappingURL=review-switch-off.mjs.map
@@ -6,7 +6,7 @@ import { storybookUrlTtlOverrideError } from "@storyshelf/core/utils";
6
6
  import { WebhookModel } from "@storyshelf/core/models";
7
7
  import { BUILD_STATUSES, PROJECT_ROLES, REVIEW_VERDICTS, SNAPSHOT_STATUSES } from "@storyshelf/core/types";
8
8
  import { BROWSER_NAMES, TARGET_NAME_PATTERN } from "@storyshelf/core/adapter/capture-runner";
9
- //#region src/routers/schemas.ts
9
+ // src/routers/schemas.ts
10
10
  /** A capture target: an engine (`chromium`) or a remote one, `<engine>@<name>` (ADR 0007). */
11
11
  const captureTargetSchema = z.string().regex(new RegExp(`^(?:${BROWSER_NAMES.join("|")})(?:@${TARGET_NAME_PATTERN.source.slice(1, -1)})?$`, "u"), "Expected an engine (chromium, firefox, webkit, chrome) or <engine>@<name>");
12
12
  const reviewApprovalsSchema = z.number().int().min(1).max(20);
@@ -449,7 +449,6 @@ const healthRequestSchema = z.object({
449
449
  window: z.string().optional(),
450
450
  force: z.boolean().optional()
451
451
  }).openapi("HealthInsightRequest");
452
- //#endregion
453
452
  export { approveAllResultSchema, attemptLogSchema, attemptSchema, badRequest, baselineChangedSchema, buildReviewCreateSchema, buildReviewSchema, buildReviewStateSchema, buildSchema, commentCreateSchema, commentSchema, errorSchema, forbidden, groupDecisionResultSchema, groupMappingCreateSchema, groupMappingSchema, healthRequestSchema, insightRequestSchema, insightSchema, labelTypeCreateSchema, labelTypeSchema, labelTypeUpdateSchema, memberRoleSchema, memberSchema, memberSetSchema, notFound, notificationChannelCreateSchema, notificationChannelSchema, notificationSubscriptionPutSchema, notificationSubscriptionSchema, okSchema, projectCreateSchema, projectSchema, projectUpdateSchema, purgeInputSchema, purgeSchema, rediffResultSchema, snapshotSchema, tokenCreateSchema, tokenCreatedSchema, tokenPublicSchema, unauthorized, webhookCreateSchema, webhookCreatedSchema, webhookPublicSchema };
454
453
 
455
454
  //# sourceMappingURL=schemas.mjs.map
@@ -4,7 +4,7 @@ import.meta.url;
4
4
  import { credentialKeys } from "../credential-keys.mjs";
5
5
  import { getStore } from "../store.mjs";
6
6
  import { NotificationChannelModel, WebhookModel } from "@storyshelf/core/models";
7
- //#region src/routers/scoped-models.ts
7
+ // src/routers/scoped-models.ts
8
8
  /** Models that encrypt secrets at rest, bound to the current request's db and server secret. */
9
9
  /** Webhook model for the current request. */
10
10
  function webhookModel() {
@@ -16,7 +16,6 @@ function notificationChannelModel() {
16
16
  const { db, config } = getStore();
17
17
  return new NotificationChannelModel(db, void 0, credentialKeys(config));
18
18
  }
19
- //#endregion
20
19
  export { notificationChannelModel, webhookModel };
21
20
 
22
21
  //# sourceMappingURL=scoped-models.mjs.map
@@ -7,7 +7,7 @@ import { hxRedirect } from "./htmx.mjs";
7
7
  import { flash } from "./flash.mjs";
8
8
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
9
9
  import { ProjectModel } from "@storyshelf/core/models";
10
- //#region src/routers/settings-ai.ts
10
+ // src/routers/settings-ai.ts
11
11
  /** Register the project AI settings tab (site admins set the profile gate). */
12
12
  function registerAiSettings(app) {
13
13
  app.get("/projects/:slug/settings/ai", async (c) => c.html(await renderSettingsPage(c, "ai")));
@@ -29,7 +29,6 @@ async function handleAiUpdate(c) {
29
29
  flash(c, requested === "" ? "AI turned off for this project" : "AI profile saved");
30
30
  return hxRedirect(c, `/projects/${project.slug}/settings/ai`);
31
31
  }
32
- //#endregion
33
32
  export { registerAiSettings };
34
33
 
35
34
  //# sourceMappingURL=settings-ai.mjs.map
@@ -12,7 +12,7 @@ import { resolveBrowsers } from "@storyshelf/core/capture";
12
12
  import { ProjectModel } from "@storyshelf/core/models";
13
13
  import { HTTPException } from "hono/http-exception";
14
14
  import { isCaptureTarget } from "@storyshelf/core/adapter/capture-runner";
15
- //#region src/routers/settings-capture.ts
15
+ // src/routers/settings-capture.ts
16
16
  /** Capture settings: browsers (primary + extras), the mode allowlist and the signed URL lifetime. */
17
17
  function registerCaptureSettings(app) {
18
18
  app.get("/projects/:slug/settings/capture", async (c) => c.html(await renderSettingsPage(c, "capture")));
@@ -71,7 +71,6 @@ async function handleCaptureUpdate(c) {
71
71
  flash(c, "Capture settings saved");
72
72
  return hxRedirect(c, `/projects/${project.slug}/settings/capture`);
73
73
  }
74
- //#endregion
75
74
  export { registerCaptureSettings };
76
75
 
77
76
  //# sourceMappingURL=settings-capture.mjs.map
@@ -9,7 +9,7 @@ import { flash } from "./flash.mjs";
9
9
  import { asString, renderSettingsPage } from "./settings.handlers.mjs";
10
10
  import { DesignCredentialModel, DesignModel } from "@storyshelf/core/models";
11
11
  import { InvalidDesignUrlError } from "@storyshelf/core/adapter/design";
12
- //#region src/routers/settings-design.ts
12
+ // src/routers/settings-design.ts
13
13
  /** Design settings (project design files, component mapping, provider credentials). */
14
14
  function registerDesignSettings(app) {
15
15
  const base = "/projects/:slug/settings/design";
@@ -97,7 +97,6 @@ async function handleRemoveToken(c) {
97
97
  await credentialModel().remove(project.id, provider.metadata.kind);
98
98
  return done(c, project.slug, `${provider.metadata.name} token removed`);
99
99
  }
100
- //#endregion
101
100
  export { registerDesignSettings };
102
101
 
103
102
  //# sourceMappingURL=settings-design.mjs.map
@@ -10,7 +10,7 @@ import { runSwitchOffPass } from "./review-switch-off.mjs";
10
10
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
11
11
  import { resolveBrowsers } from "@storyshelf/core/capture";
12
12
  import { ProjectModel } from "@storyshelf/core/models";
13
- //#region src/routers/settings-general.ts
13
+ // src/routers/settings-general.ts
14
14
  /** General project settings (name, repo, thresholds) and danger-zone delete. */
15
15
  function registerGeneralSettings(app) {
16
16
  app.get("/projects/:slug/settings", async (c) => c.html(await renderSettingsPage(c, "general")));
@@ -143,7 +143,6 @@ function approvalsOf(form, project) {
143
143
  function reviewSavedMessage(accepted) {
144
144
  return accepted > 0 ? `Review settings saved; accepted ${accepted} pending build${accepted === 1 ? "" : "s"}` : "Review settings saved";
145
145
  }
146
- //#endregion
147
146
  export { registerGeneralSettings };
148
147
 
149
148
  //# sourceMappingURL=settings-general.mjs.map
@@ -6,7 +6,7 @@ import { flash } from "./flash.mjs";
6
6
  import { notificationChannelModel } from "./scoped-models.mjs";
7
7
  import { asString, renderSettingsPage } from "./settings.handlers.mjs";
8
8
  import { NotificationChannelModel } from "@storyshelf/core/models";
9
- //#region src/routers/settings-git-channel.ts
9
+ // src/routers/settings-git-channel.ts
10
10
  /** Create a git (`state`) channel from the settings form: repository, status prefix and token. */
11
11
  const CONFIG_FIELDS = [
12
12
  "owner",
@@ -71,7 +71,6 @@ function gitChannelProblem(project, provider, config, secret) {
71
71
  });
72
72
  return checked.ok ? null : fieldErrors(secret.secretField, checked);
73
73
  }
74
- //#endregion
75
74
  export { createGitChannel };
76
75
 
77
76
  //# sourceMappingURL=settings-git-channel.mjs.map
@@ -6,7 +6,7 @@ import { hxRedirect } from "./htmx.mjs";
6
6
  import { flash } from "./flash.mjs";
7
7
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
8
  import { LabelModel } from "@storyshelf/core/models";
9
- //#region src/routers/settings-labels.ts
9
+ // src/routers/settings-labels.ts
10
10
  /** Label-type settings (create custom types, delete removable ones). */
11
11
  function registerLabelSettings(app) {
12
12
  app.get("/projects/:slug/settings/labels", async (c) => c.html(await renderSettingsPage(c, "labels", { editKey: c.req.query("edit") })));
@@ -72,7 +72,6 @@ async function handleDeleteLabelType(c) {
72
72
  flash(c, "Label type deleted");
73
73
  return hxRedirect(c, `/projects/${project.slug}/settings/labels`);
74
74
  }
75
- //#endregion
76
75
  export { registerLabelSettings };
77
76
 
78
77
  //# sourceMappingURL=settings-labels.mjs.map
@@ -6,7 +6,7 @@ import { hxRedirect } from "./htmx.mjs";
6
6
  import { flash } from "./flash.mjs";
7
7
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
8
  import { PackageUsageModel, ProjectModel } from "@storyshelf/core/models";
9
- //#region src/routers/settings-links.ts
9
+ // src/routers/settings-links.ts
10
10
  /** Package-link settings (link a consumed package to its upstream project, or unlink it). */
11
11
  function registerLinkSettings(app) {
12
12
  app.get("/projects/:slug/settings/links", async (c) => c.html(await renderSettingsPage(c, "links")));
@@ -57,7 +57,6 @@ async function handleRemoveLink(c) {
57
57
  flash(c, "Link removed");
58
58
  return hxRedirect(c, `/projects/${project.slug}/settings/links`);
59
59
  }
60
- //#endregion
61
60
  export { registerLinkSettings };
62
61
 
63
62
  //# sourceMappingURL=settings-links.mjs.map
@@ -7,7 +7,7 @@ import { flash } from "./flash.mjs";
7
7
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
8
  import { MemberModel, ProjectGroupMappingModel } from "@storyshelf/core/models";
9
9
  import { isProjectRole } from "@storyshelf/core/types";
10
- //#region src/routers/settings-members.ts
10
+ // src/routers/settings-members.ts
11
11
  /** Project member settings (assign roles, remove members). */
12
12
  function registerMemberSettings(app) {
13
13
  app.get("/projects/:slug/settings/members", async (c) => c.html(await renderSettingsPage(c, "members")));
@@ -49,7 +49,6 @@ function registerGroupMappingSettings(app) {
49
49
  return hxRedirect(c, `/projects/${project.slug}/settings/members`);
50
50
  });
51
51
  }
52
- //#endregion
53
52
  export { registerMemberSettings };
54
53
 
55
54
  //# sourceMappingURL=settings-members.mjs.map
@@ -7,7 +7,7 @@ import { notificationChannelModel } from "./scoped-models.mjs";
7
7
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
8
  import { createGitChannel } from "./settings-git-channel.mjs";
9
9
  import { NotificationChannelModel, NotificationSubscriptionModel } from "@storyshelf/core/models";
10
- //#region src/routers/settings-notifications.ts
10
+ // src/routers/settings-notifications.ts
11
11
  /** Notifications settings (message and git channels plus my preferences). */
12
12
  function registerNotificationSettings(app) {
13
13
  app.get("/projects/:slug/settings/notifications", async (c) => c.html(await renderSettingsPage(c, "notifications")));
@@ -107,7 +107,6 @@ async function saveSubscription(project, userId, form) {
107
107
  }
108
108
  await model.remove(project.id, userId);
109
109
  }
110
- //#endregion
111
110
  export { registerNotificationSettings };
112
111
 
113
112
  //# sourceMappingURL=settings-notifications.mjs.map
@@ -6,7 +6,7 @@ import { hxRedirect } from "./htmx.mjs";
6
6
  import { flash } from "./flash.mjs";
7
7
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
8
8
  import { TokenModel } from "@storyshelf/core/models";
9
- //#region src/routers/settings-tokens.ts
9
+ // src/routers/settings-tokens.ts
10
10
  /** CI token settings (create with one-time display, delete). */
11
11
  function registerTokenSettings(app) {
12
12
  app.get("/projects/:slug/settings/tokens", async (c) => c.html(await renderSettingsPage(c, "tokens")));
@@ -31,7 +31,6 @@ async function handleDeleteToken(c) {
31
31
  flash(c, "Token deleted");
32
32
  return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);
33
33
  }
34
- //#endregion
35
34
  export { registerTokenSettings };
36
35
 
37
36
  //# sourceMappingURL=settings-tokens.mjs.map
@@ -7,7 +7,7 @@ import { flash } from "./flash.mjs";
7
7
  import { webhookModel } from "./scoped-models.mjs";
8
8
  import { asString, findProject, renderSettingsPage } from "./settings.handlers.mjs";
9
9
  import { WebhookModel } from "@storyshelf/core/models";
10
- //#region src/routers/settings-webhooks.ts
10
+ // src/routers/settings-webhooks.ts
11
11
  /** Webhook settings (create with one-time secret display, delete). */
12
12
  function registerWebhookSettings(app) {
13
13
  app.get("/projects/:slug/settings/webhooks", async (c) => c.html(await renderSettingsPage(c, "webhooks")));
@@ -44,7 +44,6 @@ async function handleDeleteWebhook(c) {
44
44
  flash(c, "Webhook deleted");
45
45
  return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);
46
46
  }
47
- //#endregion
48
47
  export { registerWebhookSettings };
49
48
 
50
49
  //# sourceMappingURL=settings-webhooks.mjs.map
@@ -9,7 +9,7 @@ import { runnerTargets } from "../targets.mjs";
9
9
  import { isSiteAdmin } from "./insights.handlers.mjs";
10
10
  import { renderProjectSettingsPage } from "../pages/project-settings.mjs";
11
11
  import { DesignCredentialModel, DesignModel, LabelModel, MemberModel, NotificationChannelModel, NotificationSubscriptionModel, PackageUsageModel, ProjectGroupMappingModel, ProjectModel, TokenModel, WebhookModel, credentialDaysLeft } from "@storyshelf/core/models";
12
- //#region src/routers/settings.handlers.ts
12
+ // src/routers/settings.handlers.ts
13
13
  /** Tokens without hashes for the settings UI. */
14
14
  async function loadTokenSummaries(db, projectId) {
15
15
  return (await new TokenModel(db).list(projectId)).map(({ hash: _hash, ...rest }) => rest);
@@ -224,7 +224,6 @@ async function findProject(slug) {
224
224
  function asString(value) {
225
225
  return typeof value === "string" ? value.trim() : void 0;
226
226
  }
227
- //#endregion
228
227
  export { asString, findProject, renderSettingsPage };
229
228
 
230
229
  //# sourceMappingURL=settings.handlers.mjs.map
@@ -11,7 +11,7 @@ import { registerMemberSettings } from "./settings-members.mjs";
11
11
  import { registerNotificationSettings } from "./settings-notifications.mjs";
12
12
  import { registerTokenSettings } from "./settings-tokens.mjs";
13
13
  import { registerWebhookSettings } from "./settings-webhooks.mjs";
14
- //#region src/routers/settings.ts
14
+ // src/routers/settings.ts
15
15
  /** Register the server-rendered project settings pages and their form handlers. */
16
16
  function registerSettingsPages(app) {
17
17
  registerProjectSettings(app);
@@ -31,7 +31,6 @@ function registerCatalogSettings(app) {
31
31
  registerLinkSettings(app);
32
32
  registerDesignSettings(app);
33
33
  }
34
- //#endregion
35
34
  export { registerSettingsPages };
36
35
 
37
36
  //# sourceMappingURL=settings.mjs.map
@@ -7,13 +7,15 @@ import { publishedAccess } from "./published-access.mjs";
7
7
  import { SIGNED_URL_HEADERS, publishedRelativePath, servePublishedFile } from "./published-files.mjs";
8
8
  import { STORYBOOK_TOKEN_PREFIX, storybookTokenPath, verifyStorybookToken } from "@storyshelf/core/utils";
9
9
  import { BuildModel, ProjectModel } from "@storyshelf/core/models";
10
- //#region src/routers/signed-storybook.ts
10
+ // src/routers/signed-storybook.ts
11
11
  /**
12
12
  * Signed Storybook URLs (ADR 0007, decision 5): `GET /_s/<token>/<file>` serves one build's static
13
13
  * Storybook files to a remote browser, with no session. The HMAC token is the only credential; it
14
14
  * names one project and one build and expires. The file itself is served by the same handler and
15
15
  * access policy as the other published routes (`published-files.ts`, `published-access.ts`).
16
16
  * Nothing else lives under `/_s/`: no wrapper page, no API, no other build, no listing.
17
+ * Answers are `private, no-store`, except a success for a capture-purpose grant when the server
18
+ * opted in (`captureAssets.cacheMaxAgeSeconds`, see `capture-cache.ts`).
17
19
  */
18
20
  /** Register the signed-URL file route. */
19
21
  function registerSignedStorybook(app) {
@@ -39,7 +41,8 @@ async function serveGranted(c, token, rel) {
39
41
  buildId,
40
42
  rel,
41
43
  access,
42
- cacheControl: ""
44
+ cacheControl: "",
45
+ grant: verified.claims
43
46
  });
44
47
  }
45
48
  /** The one answer for every refusal, so a caller cannot tell a bad signature from an expired token. */
@@ -47,7 +50,6 @@ function refuse(c, reason) {
47
50
  getStore().logger.debug({ reason }, "signed storybook request refused");
48
51
  return c.text("Not found", 404, SIGNED_URL_HEADERS);
49
52
  }
50
- //#endregion
51
53
  export { registerSignedStorybook };
52
54
 
53
55
  //# sourceMappingURL=signed-storybook.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"signed-storybook.mjs","names":[],"sources":["../../src/routers/signed-storybook.ts"],"sourcesContent":["/**\n * Signed Storybook URLs (ADR 0007, decision 5): `GET /_s/<token>/<file>` serves one build's static\n * Storybook files to a remote browser, with no session. The HMAC token is the only credential; it\n * names one project and one build and expires. The file itself is served by the same handler and\n * access policy as the other published routes (`published-files.ts`, `published-access.ts`).\n * Nothing else lives under `/_s/`: no wrapper page, no API, no other build, no listing.\n */\nimport { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport {\n STORYBOOK_TOKEN_PREFIX,\n storybookTokenPath,\n verifyStorybookToken,\n} from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { publishedAccess } from \"./published-access.ts\";\nimport {\n publishedRelativePath,\n servePublishedFile,\n SIGNED_URL_HEADERS,\n} from \"./published-files.ts\";\n\n/** Register the signed-URL file route. */\nexport function registerSignedStorybook(app: ShelfRouter): void {\n app.get(`${STORYBOOK_TOKEN_PREFIX}:token/*`, async (c) => await serveSigned(c));\n}\n\nasync function serveSigned(c: Context): Promise<Response> {\n const token = c.req.param(\"token\") ?? \"\";\n const rel = publishedRelativePath(c.req.path, storybookTokenPath(token));\n if (!rel) {\n return refuse(c, \"bad path\");\n }\n return (await serveGranted(c, token, rel)) ?? refuse(c, \"no such file\");\n}\n\n/** The file the token grants as a response, or null (for the debug log only: why not). */\nasync function serveGranted(c: Context, token: string, rel: string): Promise<Response | null> {\n const { config, db } = getStore();\n const verified = verifyStorybookToken(token, credentialKeys(config));\n if (!verified.ok) {\n return refuse(c, verified.reason);\n }\n const { projectId, buildId } = verified.claims;\n const [project, build] = await Promise.all([\n new ProjectModel(db).get(projectId),\n new BuildModel(db).get(buildId),\n ]);\n const access = project && build ? await publishedAccess(build, project, verified.claims) : null;\n if (access !== \"token\") {\n return refuse(c, \"unknown build\");\n }\n return await servePublishedFile(c, { projectId, buildId, rel, access, cacheControl: \"\" });\n}\n\n/** The one answer for every refusal, so a caller cannot tell a bad signature from an expired token. */\nfunction refuse(c: Context, reason: string): Response {\n getStore().logger.debug({ reason }, \"signed storybook request refused\");\n return c.text(\"Not found\", 404, SIGNED_URL_HEADERS);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAyBA,SAAgB,wBAAwB,KAAwB;CAC9D,IAAI,IAAI,GAAG,uBAAuB,WAAW,OAAO,MAAM,MAAM,YAAY,CAAC,CAAC;AAChF;AAEA,eAAe,YAAY,GAA+B;CACxD,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO,KAAK;CACtC,MAAM,MAAM,sBAAsB,EAAE,IAAI,MAAM,mBAAmB,KAAK,CAAC;CACvE,IAAI,CAAC,KACH,OAAO,OAAO,GAAG,UAAU;CAE7B,OAAQ,MAAM,aAAa,GAAG,OAAO,GAAG,KAAM,OAAO,GAAG,cAAc;AACxE;;AAGA,eAAe,aAAa,GAAY,OAAe,KAAuC;CAC5F,MAAM,EAAE,QAAQ,OAAO,SAAS;CAChC,MAAM,WAAW,qBAAqB,OAAO,eAAe,MAAM,CAAC;CACnE,IAAI,CAAC,SAAS,IACZ,OAAO,OAAO,GAAG,SAAS,MAAM;CAElC,MAAM,EAAE,WAAW,YAAY,SAAS;CACxC,MAAM,CAAC,SAAS,SAAS,MAAM,QAAQ,IAAI,CACzC,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,SAAS,GAClC,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,OAAO,CAChC,CAAC;CACD,MAAM,SAAS,WAAW,QAAQ,MAAM,gBAAgB,OAAO,SAAS,SAAS,MAAM,IAAI;CAC3F,IAAI,WAAW,SACb,OAAO,OAAO,GAAG,eAAe;CAElC,OAAO,MAAM,mBAAmB,GAAG;EAAE;EAAW;EAAS;EAAK;EAAQ,cAAc;CAAG,CAAC;AAC1F;;AAGA,SAAS,OAAO,GAAY,QAA0B;CACpD,SAAS,CAAC,CAAC,OAAO,MAAM,EAAE,OAAO,GAAG,kCAAkC;CACtE,OAAO,EAAE,KAAK,aAAa,KAAK,kBAAkB;AACpD"}
1
+ {"version":3,"file":"signed-storybook.mjs","names":[],"sources":["../../src/routers/signed-storybook.ts"],"sourcesContent":["/**\n * Signed Storybook URLs (ADR 0007, decision 5): `GET /_s/<token>/<file>` serves one build's static\n * Storybook files to a remote browser, with no session. The HMAC token is the only credential; it\n * names one project and one build and expires. The file itself is served by the same handler and\n * access policy as the other published routes (`published-files.ts`, `published-access.ts`).\n * Nothing else lives under `/_s/`: no wrapper page, no API, no other build, no listing.\n * Answers are `private, no-store`, except a success for a capture-purpose grant when the server\n * opted in (`captureAssets.cacheMaxAgeSeconds`, see `capture-cache.ts`).\n */\nimport { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport {\n STORYBOOK_TOKEN_PREFIX,\n storybookTokenPath,\n verifyStorybookToken,\n} from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { publishedAccess } from \"./published-access.ts\";\nimport {\n publishedRelativePath,\n servePublishedFile,\n SIGNED_URL_HEADERS,\n} from \"./published-files.ts\";\n\n/** Register the signed-URL file route. */\nexport function registerSignedStorybook(app: ShelfRouter): void {\n app.get(`${STORYBOOK_TOKEN_PREFIX}:token/*`, async (c) => await serveSigned(c));\n}\n\nasync function serveSigned(c: Context): Promise<Response> {\n const token = c.req.param(\"token\") ?? \"\";\n const rel = publishedRelativePath(c.req.path, storybookTokenPath(token));\n if (!rel) {\n return refuse(c, \"bad path\");\n }\n return (await serveGranted(c, token, rel)) ?? refuse(c, \"no such file\");\n}\n\n/** The file the token grants as a response, or null (for the debug log only: why not). */\nasync function serveGranted(c: Context, token: string, rel: string): Promise<Response | null> {\n const { config, db } = getStore();\n const verified = verifyStorybookToken(token, credentialKeys(config));\n if (!verified.ok) {\n return refuse(c, verified.reason);\n }\n const { projectId, buildId } = verified.claims;\n const [project, build] = await Promise.all([\n new ProjectModel(db).get(projectId),\n new BuildModel(db).get(buildId),\n ]);\n const access = project && build ? await publishedAccess(build, project, verified.claims) : null;\n if (access !== \"token\") {\n return refuse(c, \"unknown build\");\n }\n return await servePublishedFile(c, {\n projectId,\n buildId,\n rel,\n access,\n cacheControl: \"\",\n grant: verified.claims,\n });\n}\n\n/** The one answer for every refusal, so a caller cannot tell a bad signature from an expired token. */\nfunction refuse(c: Context, reason: string): Response {\n getStore().logger.debug({ reason }, \"signed storybook request refused\");\n return c.text(\"Not found\", 404, SIGNED_URL_HEADERS);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,wBAAwB,KAAwB;CAC9D,IAAI,IAAI,GAAG,uBAAuB,WAAW,OAAO,MAAM,MAAM,YAAY,CAAC,CAAC;AAChF;AAEA,eAAe,YAAY,GAA+B;CACxD,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO,KAAK;CACtC,MAAM,MAAM,sBAAsB,EAAE,IAAI,MAAM,mBAAmB,KAAK,CAAC;CACvE,IAAI,CAAC,KACH,OAAO,OAAO,GAAG,UAAU;CAE7B,OAAQ,MAAM,aAAa,GAAG,OAAO,GAAG,KAAM,OAAO,GAAG,cAAc;AACxE;;AAGA,eAAe,aAAa,GAAY,OAAe,KAAuC;CAC5F,MAAM,EAAE,QAAQ,OAAO,SAAS;CAChC,MAAM,WAAW,qBAAqB,OAAO,eAAe,MAAM,CAAC;CACnE,IAAI,CAAC,SAAS,IACZ,OAAO,OAAO,GAAG,SAAS,MAAM;CAElC,MAAM,EAAE,WAAW,YAAY,SAAS;CACxC,MAAM,CAAC,SAAS,SAAS,MAAM,QAAQ,IAAI,CACzC,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,SAAS,GAClC,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,OAAO,CAChC,CAAC;CACD,MAAM,SAAS,WAAW,QAAQ,MAAM,gBAAgB,OAAO,SAAS,SAAS,MAAM,IAAI;CAC3F,IAAI,WAAW,SACb,OAAO,OAAO,GAAG,eAAe;CAElC,OAAO,MAAM,mBAAmB,GAAG;EACjC;EACA;EACA;EACA;EACA,cAAc;EACd,OAAO,SAAS;CAClB,CAAC;AACH;;AAGA,SAAS,OAAO,GAAY,QAA0B;CACpD,SAAS,CAAC,CAAC,OAAO,MAAM,EAAE,OAAO,GAAG,kCAAkC;CACtE,OAAO,EAAE,KAAK,aAAa,KAAK,kBAAkB;AACpD"}
@@ -6,7 +6,7 @@ import { notFound } from "./helpers.mjs";
6
6
  import { baselineChanged, findDriftedSnapshots, isOpenSnapshot } from "./baseline-guard.mjs";
7
7
  import { approveSnapshot, refreshBuild } from "./builds.handlers.mjs";
8
8
  import { SnapshotModel } from "@storyshelf/core/models";
9
- //#region src/routers/snapshot-groups.ts
9
+ // src/routers/snapshot-groups.ts
10
10
  /** Shared (story, mode) review: one decision covers every browser's snapshot. */
11
11
  /** Identity of a review group: the same story and mode across engines. */
12
12
  function groupKey(snapshot) {
@@ -57,7 +57,6 @@ function splitByDrift(open, drifted) {
57
57
  skipped: open.filter((s) => blocked.has(groupKey(s)))
58
58
  };
59
59
  }
60
- //#endregion
61
60
  export { approveGroup, groupKey, rejectGroup, splitByDrift };
62
61
 
63
62
  //# sourceMappingURL=snapshot-groups.mjs.map
@@ -14,7 +14,7 @@ import { createRoute, z } from "@hono/zod-openapi";
14
14
  import { REDIFFABLE_BUILD_STATUSES, rediffBuild } from "@storyshelf/core/capture";
15
15
  import { SnapshotModel } from "@storyshelf/core/models";
16
16
  import { HTTPException } from "hono/http-exception";
17
- //#region src/routers/snapshots.ts
17
+ // src/routers/snapshots.ts
18
18
  const listSnapshotsRoute = createRoute({
19
19
  method: "get",
20
20
  tags: ["Review"],
@@ -332,7 +332,6 @@ function registerSnapshots(app) {
332
332
  return c.json({ ok: true });
333
333
  });
334
334
  }
335
- //#endregion
336
335
  export { registerSnapshots };
337
336
 
338
337
  //# sourceMappingURL=snapshots.mjs.map
@@ -1,4 +1,4 @@
1
- import "node:path";
1
+ import { posix } from "node:path";
2
2
  import "node:url";
3
3
  import.meta.url;
4
4
  import { getStore } from "../store.mjs";
@@ -8,8 +8,7 @@ import { publishedRelativePath, servePublishedFile } from "./published-files.mjs
8
8
  import { renderStorybookPage, renderStorybookPreparingPage } from "../pages/storybook.mjs";
9
9
  import { storybookDir } from "@storyshelf/core/utils";
10
10
  import { BuildModel, LabelModel, ProjectModel } from "@storyshelf/core/models";
11
- import { posix } from "node:path";
12
- //#region src/routers/storybook.ts
11
+ // src/routers/storybook.ts
13
12
  /** Check whether a build's extracted statics are available for serving. */
14
13
  async function staticsReady(projectId, buildId) {
15
14
  if (await getStore().storage.exists(posix.join(storybookDir(projectId, buildId), "manifest.json"))) return true;
@@ -28,19 +27,6 @@ function registerStorybook(app) {
28
27
  if (!await publishedAccess(build, project)) return c.redirect("/auth/login", 302);
29
28
  return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);
30
29
  });
31
- app.get("/projects/:slug/storybook/:key/:value", async (c) => {
32
- const slug = c.req.param("slug");
33
- const key = c.req.param("key");
34
- if (key === "build") return c.notFound();
35
- const value = c.req.param("value");
36
- const project = await new ProjectModel(getStore().db).getBySlug(slug);
37
- if (!project) notFound("Project not found");
38
- const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);
39
- if (!buildId) notFound("No build carries that label");
40
- const build = await new BuildModel(getStore().db).get(buildId);
41
- if (!build || !await publishedAccess(build, project)) return c.redirect("/auth/login", 302);
42
- return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);
43
- });
44
30
  app.get("/_/:id", async (c) => {
45
31
  const id = c.req.param("id");
46
32
  if (id.length === 26 && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(id)) {
@@ -108,13 +94,31 @@ function registerStorybook(app) {
108
94
  cacheControl: "public, max-age=31536000, immutable"
109
95
  }) ?? c.notFound();
110
96
  });
97
+ app.get("/projects/:slug/storybook/:key/*", async (c) => {
98
+ const slug = c.req.param("slug");
99
+ const key = c.req.param("key");
100
+ if (key === "build") return c.notFound();
101
+ let value;
102
+ try {
103
+ value = decodeURIComponent(c.req.path.split("/").slice(5).join("/"));
104
+ } catch {
105
+ return c.notFound();
106
+ }
107
+ if (!value) return c.notFound();
108
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
109
+ if (!project) notFound("Project not found");
110
+ const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);
111
+ if (!buildId) notFound("No build carries that label");
112
+ const build = await new BuildModel(getStore().db).get(buildId);
113
+ if (!build || !await publishedAccess(build, project)) return c.redirect("/auth/login", 302);
114
+ return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);
115
+ });
111
116
  }
112
117
  /** Cache policy of the short link: a ULID names one build forever; a slug follows the latest. */
113
118
  function shortLinkCache(isUlid26, isRoot) {
114
119
  if (isUlid26) return "public, max-age=31536000, immutable";
115
120
  return isRoot ? "public, max-age=60" : "public, max-age=3600";
116
121
  }
117
- //#endregion
118
122
  export { registerStorybook };
119
123
 
120
124
  //# sourceMappingURL=storybook.mjs.map