@storyshelf/app 0.6.1 → 0.7.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.
- package/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/credentials.mjs +3 -2
- package/dist/credentials.mjs.map +1 -1
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +202 -124
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +109 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +2 -2
- package/dist/routers/auth.mjs.map +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +4 -2
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -6
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
|
+
import { getStore } from "../store.mjs";
|
|
6
|
+
import { publishedAccess } from "./published-access.mjs";
|
|
7
|
+
import { SIGNED_URL_HEADERS, publishedRelativePath, servePublishedFile } from "./published-files.mjs";
|
|
8
|
+
import { STORYBOOK_TOKEN_PREFIX, storybookTokenPath, verifyStorybookToken } from "@storyshelf/core/utils";
|
|
9
|
+
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
10
|
+
//#region src/routers/signed-storybook.ts
|
|
11
|
+
/**
|
|
12
|
+
* Signed Storybook URLs (ADR 0007, decision 5): `GET /_s/<token>/<file>` serves one build's static
|
|
13
|
+
* Storybook files to a remote browser, with no session. The HMAC token is the only credential; it
|
|
14
|
+
* names one project and one build and expires. The file itself is served by the same handler and
|
|
15
|
+
* access policy as the other published routes (`published-files.ts`, `published-access.ts`).
|
|
16
|
+
* Nothing else lives under `/_s/`: no wrapper page, no API, no other build, no listing.
|
|
17
|
+
*/
|
|
18
|
+
/** Register the signed-URL file route. */
|
|
19
|
+
function registerSignedStorybook(app) {
|
|
20
|
+
app.get(`${STORYBOOK_TOKEN_PREFIX}:token/*`, async (c) => await serveSigned(c));
|
|
21
|
+
}
|
|
22
|
+
async function serveSigned(c) {
|
|
23
|
+
const token = c.req.param("token") ?? "";
|
|
24
|
+
const rel = publishedRelativePath(c.req.path, storybookTokenPath(token));
|
|
25
|
+
if (!rel) return refuse(c, "bad path");
|
|
26
|
+
return await serveGranted(c, token, rel) ?? refuse(c, "no such file");
|
|
27
|
+
}
|
|
28
|
+
/** The file the token grants as a response, or null (for the debug log only: why not). */
|
|
29
|
+
async function serveGranted(c, token, rel) {
|
|
30
|
+
const { config, db } = getStore();
|
|
31
|
+
const verified = verifyStorybookToken(token, credentialKeys(config));
|
|
32
|
+
if (!verified.ok) return refuse(c, verified.reason);
|
|
33
|
+
const { projectId, buildId } = verified.claims;
|
|
34
|
+
const [project, build] = await Promise.all([new ProjectModel(db).get(projectId), new BuildModel(db).get(buildId)]);
|
|
35
|
+
const access = project && build ? await publishedAccess(build, project, verified.claims) : null;
|
|
36
|
+
if (access !== "token") return refuse(c, "unknown build");
|
|
37
|
+
return await servePublishedFile(c, {
|
|
38
|
+
projectId,
|
|
39
|
+
buildId,
|
|
40
|
+
rel,
|
|
41
|
+
access,
|
|
42
|
+
cacheControl: ""
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** The one answer for every refusal, so a caller cannot tell a bad signature from an expired token. */
|
|
46
|
+
function refuse(c, reason) {
|
|
47
|
+
getStore().logger.debug({ reason }, "signed storybook request refused");
|
|
48
|
+
return c.text("Not found", 404, SIGNED_URL_HEADERS);
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { registerSignedStorybook };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=signed-storybook.mjs.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { notFound } from "./helpers.mjs";
|
|
6
|
+
import { baselineChanged, findDriftedSnapshots, isOpenSnapshot } from "./baseline-guard.mjs";
|
|
7
|
+
import { approveSnapshot, refreshBuild } from "./builds.handlers.mjs";
|
|
8
|
+
import { SnapshotModel } from "@storyshelf/core/models";
|
|
9
|
+
//#region src/routers/snapshot-groups.ts
|
|
10
|
+
/** Shared (story, mode) review: one decision covers every browser's snapshot. */
|
|
11
|
+
/** Identity of a review group: the same story and mode across engines. */
|
|
12
|
+
function groupKey(snapshot) {
|
|
13
|
+
return `${snapshot.storyId}\u0000${snapshot.viewportName}`;
|
|
14
|
+
}
|
|
15
|
+
/** Open snapshots of one story/mode; 404 when the group has nothing left to decide. */
|
|
16
|
+
async function openMembers(build, storyId, modeKey) {
|
|
17
|
+
const members = (await new SnapshotModel(getStore().db).listByBuild(build.id)).filter((snapshot) => snapshot.storyId === storyId && snapshot.viewportName === modeKey && isOpenSnapshot(snapshot));
|
|
18
|
+
if (members.length === 0) notFound("No open snapshots for this story and mode");
|
|
19
|
+
return members;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Approve every open snapshot of a story/mode, promoting each engine's
|
|
23
|
+
* baseline. All-or-nothing on drift: when any member's baseline moved and
|
|
24
|
+
* `force` is off, nothing is written and the 409 names that snapshot.
|
|
25
|
+
*
|
|
26
|
+
* @returns The approved snapshot ids, primary engine first.
|
|
27
|
+
*/
|
|
28
|
+
async function approveGroup(project, build, target, userId, options = {}) {
|
|
29
|
+
const members = await openMembers(build, target.storyId, target.modeKey);
|
|
30
|
+
if (!options.force) {
|
|
31
|
+
const first = [...(await findDriftedSnapshots(project, build, members)).entries()][0];
|
|
32
|
+
if (first) throw baselineChanged(first[0], first[1]);
|
|
33
|
+
}
|
|
34
|
+
for (const member of members) await approveSnapshot(member.id, userId, {
|
|
35
|
+
force: options.force ?? false,
|
|
36
|
+
deferRefresh: true
|
|
37
|
+
});
|
|
38
|
+
await refreshBuild(build.id);
|
|
39
|
+
return members.map((member) => member.id);
|
|
40
|
+
}
|
|
41
|
+
/** Reject every open snapshot of a story/mode and refresh the build once. */
|
|
42
|
+
async function rejectGroup(build, target, userId) {
|
|
43
|
+
const members = await openMembers(build, target.storyId, target.modeKey);
|
|
44
|
+
const snapshots = new SnapshotModel(getStore().db);
|
|
45
|
+
await Promise.all(members.map(async (member) => await snapshots.review(member.id, "rejected", userId)));
|
|
46
|
+
await refreshBuild(build.id);
|
|
47
|
+
return members.map((member) => member.id);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Split open snapshots into approvable groups and groups skipped because some
|
|
51
|
+
* member drifted (the whole group is skipped, never half-approved).
|
|
52
|
+
*/
|
|
53
|
+
function splitByDrift(open, drifted) {
|
|
54
|
+
const blocked = new Set(open.filter((s) => drifted.has(s.id)).map((s) => groupKey(s)));
|
|
55
|
+
return {
|
|
56
|
+
approvable: open.filter((s) => !blocked.has(groupKey(s))),
|
|
57
|
+
skipped: open.filter((s) => blocked.has(groupKey(s)))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { approveGroup, groupKey, rejectGroup, splitByDrift };
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=snapshot-groups.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-groups.mjs","names":[],"sources":["../../src/routers/snapshot-groups.ts"],"sourcesContent":["/** Shared (story, mode) review: one decision covers every browser's snapshot. */\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { getStore } from \"../store.ts\";\nimport { baselineChanged, findDriftedSnapshots, isOpenSnapshot } from \"./baseline-guard.ts\";\nimport { approveSnapshot, refreshBuild } from \"./builds.handlers.ts\";\nimport { notFound } from \"./helpers.ts\";\n\n/** Identity of a review group: the same story and mode across engines. */\nexport function groupKey(snapshot: Pick<Snapshot, \"storyId\" | \"viewportName\">): string {\n return `${snapshot.storyId}\\u0000${snapshot.viewportName}`;\n}\n\n/** Open snapshots of one story/mode; 404 when the group has nothing left to decide. */\nasync function openMembers(build: Build, storyId: string, modeKey: string): Promise<Snapshot[]> {\n const all = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const members = all.filter(\n (snapshot) =>\n snapshot.storyId === storyId && snapshot.viewportName === modeKey && isOpenSnapshot(snapshot),\n );\n if (members.length === 0) {\n notFound(\"No open snapshots for this story and mode\");\n }\n return members;\n}\n\n/**\n * Approve every open snapshot of a story/mode, promoting each engine's\n * baseline. All-or-nothing on drift: when any member's baseline moved and\n * `force` is off, nothing is written and the 409 names that snapshot.\n *\n * @returns The approved snapshot ids, primary engine first.\n */\nexport async function approveGroup(\n project: Project,\n build: Build,\n target: { storyId: string; modeKey: string },\n userId: string | null,\n options: { force?: boolean } = {},\n): Promise<string[]> {\n const members = await openMembers(build, target.storyId, target.modeKey);\n if (!options.force) {\n const drifted = await findDriftedSnapshots(project, build, members);\n const first = [...drifted.entries()][0];\n if (first) {\n throw baselineChanged(first[0], first[1]);\n }\n }\n for (const member of members) {\n // oxlint-disable-next-line no-await-in-loop -- baselines are promoted engine by engine\n await approveSnapshot(member.id, userId, {\n force: options.force ?? false,\n deferRefresh: true,\n });\n }\n await refreshBuild(build.id);\n return members.map((member) => member.id);\n}\n\n/** Reject every open snapshot of a story/mode and refresh the build once. */\nexport async function rejectGroup(\n build: Build,\n target: { storyId: string; modeKey: string },\n userId: string | null,\n): Promise<string[]> {\n const members = await openMembers(build, target.storyId, target.modeKey);\n const snapshots = new SnapshotModel(getStore().db);\n await Promise.all(\n members.map(async (member) => await snapshots.review(member.id, \"rejected\", userId)),\n );\n await refreshBuild(build.id);\n return members.map((member) => member.id);\n}\n\n/**\n * Split open snapshots into approvable groups and groups skipped because some\n * member drifted (the whole group is skipped, never half-approved).\n */\nexport function splitByDrift(\n open: Snapshot[],\n drifted: ReadonlyMap<string, unknown>,\n): { approvable: Snapshot[]; skipped: Snapshot[] } {\n const blocked = new Set(open.filter((s) => drifted.has(s.id)).map((s) => groupKey(s)));\n return {\n approvable: open.filter((s) => !blocked.has(groupKey(s))),\n skipped: open.filter((s) => blocked.has(groupKey(s))),\n };\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAgB,SAAS,UAA8D;CACrF,OAAO,GAAG,SAAS,QAAQ,QAAQ,SAAS;AAC9C;;AAGA,eAAe,YAAY,OAAc,SAAiB,SAAsC;CAE9F,MAAM,WAAU,MADE,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,EAAA,CACnD,QACjB,aACC,SAAS,YAAY,WAAW,SAAS,iBAAiB,WAAW,eAAe,QAAQ,CAChG;CACA,IAAI,QAAQ,WAAW,GACrB,SAAS,2CAA2C;CAEtD,OAAO;AACT;;;;;;;;AASA,eAAsB,aACpB,SACA,OACA,QACA,QACA,UAA+B,CAAC,GACb;CACnB,MAAM,UAAU,MAAM,YAAY,OAAO,OAAO,SAAS,OAAO,OAAO;CACvE,IAAI,CAAC,QAAQ,OAAO;EAElB,MAAM,QAAQ,CAAC,IAAG,MADI,qBAAqB,SAAS,OAAO,OAAO,EAAA,CACxC,QAAQ,CAAC,CAAC,CAAC;EACrC,IAAI,OACF,MAAM,gBAAgB,MAAM,IAAI,MAAM,EAAE;CAE5C;CACA,KAAK,MAAM,UAAU,SAEnB,MAAM,gBAAgB,OAAO,IAAI,QAAQ;EACvC,OAAO,QAAQ,SAAS;EACxB,cAAc;CAChB,CAAC;CAEH,MAAM,aAAa,MAAM,EAAE;CAC3B,OAAO,QAAQ,KAAK,WAAW,OAAO,EAAE;AAC1C;;AAGA,eAAsB,YACpB,OACA,QACA,QACmB;CACnB,MAAM,UAAU,MAAM,YAAY,OAAO,OAAO,SAAS,OAAO,OAAO;CACvE,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,QAAQ,IACZ,QAAQ,IAAI,OAAO,WAAW,MAAM,UAAU,OAAO,OAAO,IAAI,YAAY,MAAM,CAAC,CACrF;CACA,MAAM,aAAa,MAAM,EAAE;CAC3B,OAAO,QAAQ,KAAK,WAAW,OAAO,EAAE;AAC1C;;;;;AAMA,SAAgB,aACd,MACA,SACiD;CACjD,MAAM,UAAU,IAAI,IAAI,KAAK,QAAQ,MAAM,QAAQ,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,MAAM,SAAS,CAAC,CAAC,CAAC;CACrF,OAAO;EACL,YAAY,KAAK,QAAQ,MAAM,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC;EACxD,SAAS,KAAK,QAAQ,MAAM,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC;CACtD;AACF"}
|
|
@@ -2,13 +2,14 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
|
+
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
5
6
|
import { hxRefresh, isHxRequest } from "./htmx.mjs";
|
|
6
7
|
import { flashHx } from "./flash.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import { approveAllResultSchema, baselineChangedSchema, errorSchema, notFound, okSchema, rediffResultSchema, snapshotSchema, unauthorized } from "./schemas.mjs";
|
|
8
|
+
import { approveAllResultSchema, baselineChangedSchema, errorSchema, groupDecisionResultSchema, notFound, okSchema, rediffResultSchema, snapshotSchema, unauthorized } from "./schemas.mjs";
|
|
9
9
|
import { findDriftedSnapshots, isBaselineChanged, isOpenSnapshot } from "./baseline-guard.mjs";
|
|
10
10
|
import { APPROVER_ROLES, VIEW_ROLES, approveSnapshot, buildForProject, refreshBuild, snapshotForBuild } from "./builds.handlers.mjs";
|
|
11
11
|
import { hxAdvance } from "./review-next.mjs";
|
|
12
|
+
import { approveGroup, rejectGroup, splitByDrift } from "./snapshot-groups.mjs";
|
|
12
13
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
13
14
|
import { REDIFFABLE_BUILD_STATUSES, rediffBuild } from "@storyshelf/core/capture";
|
|
14
15
|
import { SnapshotModel } from "@storyshelf/core/models";
|
|
@@ -95,6 +96,47 @@ const approveAllRoute = createRoute({
|
|
|
95
96
|
...notFound
|
|
96
97
|
}
|
|
97
98
|
});
|
|
99
|
+
const groupParams = z.object({
|
|
100
|
+
slug: z.string(),
|
|
101
|
+
buildId: z.string(),
|
|
102
|
+
storyId: z.string(),
|
|
103
|
+
modeKey: z.string()
|
|
104
|
+
});
|
|
105
|
+
const approveGroupRoute = createRoute({
|
|
106
|
+
method: "post",
|
|
107
|
+
tags: ["Review"],
|
|
108
|
+
summary: "Approve a story/mode across all browsers",
|
|
109
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/stories/{storyId}/modes/{modeKey}/approve",
|
|
110
|
+
request: {
|
|
111
|
+
params: groupParams,
|
|
112
|
+
query: z.object({ force: z.enum(["true", "false"]).optional() })
|
|
113
|
+
},
|
|
114
|
+
responses: {
|
|
115
|
+
200: {
|
|
116
|
+
content: { "application/json": { schema: groupDecisionResultSchema } },
|
|
117
|
+
description: "Every open snapshot of the story and mode approved and each engine's baseline promoted"
|
|
118
|
+
},
|
|
119
|
+
409: {
|
|
120
|
+
content: { "application/json": { schema: baselineChangedSchema } },
|
|
121
|
+
description: "A member's baseline changed after its diff; nothing was approved. Re-diff or pass force=true"
|
|
122
|
+
},
|
|
123
|
+
...notFound
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const rejectGroupRoute = createRoute({
|
|
127
|
+
method: "post",
|
|
128
|
+
tags: ["Review"],
|
|
129
|
+
summary: "Reject a story/mode across all browsers",
|
|
130
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/stories/{storyId}/modes/{modeKey}/reject",
|
|
131
|
+
request: { params: groupParams },
|
|
132
|
+
responses: {
|
|
133
|
+
200: {
|
|
134
|
+
content: { "application/json": { schema: groupDecisionResultSchema } },
|
|
135
|
+
description: "Every open snapshot of the story and mode rejected"
|
|
136
|
+
},
|
|
137
|
+
...notFound
|
|
138
|
+
}
|
|
139
|
+
});
|
|
98
140
|
const rediffRoute = createRoute({
|
|
99
141
|
method: "post",
|
|
100
142
|
tags: ["Review"],
|
|
@@ -191,15 +233,71 @@ function registerSnapshots(app) {
|
|
|
191
233
|
const userId = getStore().user?.id ?? null;
|
|
192
234
|
const forced = force === "true";
|
|
193
235
|
const drifted = forced ? /* @__PURE__ */ new Map() : await findDriftedSnapshots(project, build, snapshots);
|
|
194
|
-
|
|
195
|
-
|
|
236
|
+
const { approvable, skipped } = splitByDrift(snapshots.filter((snapshot) => isOpenSnapshot(snapshot)), drifted);
|
|
237
|
+
await Promise.all(approvable.map(async (snapshot) => {
|
|
238
|
+
await approveSnapshot(snapshot.id, userId, {
|
|
239
|
+
force: forced,
|
|
240
|
+
deferRefresh: true
|
|
241
|
+
});
|
|
196
242
|
}));
|
|
243
|
+
await refreshBuild(build.id);
|
|
197
244
|
hxRefresh(c);
|
|
198
245
|
return c.json({
|
|
199
246
|
ok: true,
|
|
200
|
-
skipped:
|
|
247
|
+
skipped: skipped.map((snapshot) => snapshot.id)
|
|
201
248
|
});
|
|
202
249
|
});
|
|
250
|
+
app.openapi(approveGroupRoute, async (c) => {
|
|
251
|
+
const { slug, buildId, storyId, modeKey } = c.req.valid("param");
|
|
252
|
+
const { force } = c.req.valid("query");
|
|
253
|
+
const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
|
|
254
|
+
const build = await buildForProject(project.id, buildId);
|
|
255
|
+
const userId = getStore().user?.id ?? null;
|
|
256
|
+
let snapshotIds;
|
|
257
|
+
try {
|
|
258
|
+
snapshotIds = await approveGroup(project, build, {
|
|
259
|
+
storyId,
|
|
260
|
+
modeKey
|
|
261
|
+
}, userId, { force: force === "true" });
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (!isHxRequest(c) || !isBaselineChanged(error)) throw error;
|
|
264
|
+
c.header("HX-Redirect", `/projects/${slug}/builds/${buildId}/diff`);
|
|
265
|
+
return c.json({
|
|
266
|
+
ok: true,
|
|
267
|
+
snapshotIds: []
|
|
268
|
+
}, 200);
|
|
269
|
+
}
|
|
270
|
+
const first = snapshotIds[0];
|
|
271
|
+
if (first) await hxAdvance(c, {
|
|
272
|
+
slug,
|
|
273
|
+
buildId,
|
|
274
|
+
snapshotId: first
|
|
275
|
+
});
|
|
276
|
+
return c.json({
|
|
277
|
+
ok: true,
|
|
278
|
+
snapshotIds
|
|
279
|
+
}, 200);
|
|
280
|
+
});
|
|
281
|
+
app.openapi(rejectGroupRoute, async (c) => {
|
|
282
|
+
const { slug, buildId, storyId, modeKey } = c.req.valid("param");
|
|
283
|
+
const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
|
|
284
|
+
const build = await buildForProject(project.id, buildId);
|
|
285
|
+
const userId = getStore().user?.id ?? null;
|
|
286
|
+
const snapshotIds = await rejectGroup(build, {
|
|
287
|
+
storyId,
|
|
288
|
+
modeKey
|
|
289
|
+
}, userId);
|
|
290
|
+
const first = snapshotIds[0];
|
|
291
|
+
if (first) await hxAdvance(c, {
|
|
292
|
+
slug,
|
|
293
|
+
buildId,
|
|
294
|
+
snapshotId: first
|
|
295
|
+
});
|
|
296
|
+
return c.json({
|
|
297
|
+
ok: true,
|
|
298
|
+
snapshotIds
|
|
299
|
+
}, 200);
|
|
300
|
+
});
|
|
203
301
|
app.openapi(rediffRoute, async (c) => {
|
|
204
302
|
const { slug, buildId } = c.req.valid("param");
|
|
205
303
|
const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshots.mjs","names":[],"sources":["../../src/routers/snapshots.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { REDIFFABLE_BUILD_STATUSES, rediffBuild } from \"@storyshelf/core/capture\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { findDriftedSnapshots, isBaselineChanged, isOpenSnapshot } from \"./baseline-guard.ts\";\nimport {\n VIEW_ROLES,\n APPROVER_ROLES,\n snapshotForBuild,\n refreshBuild,\n approveSnapshot,\n buildForProject,\n} from \"./builds.handlers.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh, isHxRequest } from \"./htmx.ts\";\nimport { hxAdvance } from \"./review-next.ts\";\nimport {\n snapshotSchema,\n okSchema,\n approveAllResultSchema,\n rediffResultSchema,\n baselineChangedSchema,\n errorSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\nconst listSnapshotsRoute = createRoute({\n method: \"get\",\n tags: [\"Review\"],\n summary: \"List snapshots\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: snapshotSchema.array() } },\n description: \"List snapshots for a build\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst approveSnapshotRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Approve a snapshot\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/approve\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }),\n query: z.object({ force: z.enum([\"true\", \"false\"]).optional() }),\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"Snapshot approved\",\n },\n 409: {\n content: { \"application/json\": { schema: baselineChangedSchema } },\n description:\n \"The baseline changed after this diff was computed; re-diff the build or pass force=true\",\n },\n ...notFound,\n },\n});\n\nconst rejectSnapshotRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Reject a snapshot\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/reject\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"Snapshot rejected\",\n },\n ...notFound,\n },\n});\n\nconst approveAllRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Approve all open snapshots\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/approve-all\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n query: z.object({ force: z.enum([\"true\", \"false\"]).optional() }),\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: approveAllResultSchema } },\n description: \"Open snapshots approved; snapshots with a changed baseline are skipped\",\n },\n ...notFound,\n },\n});\n\nconst rediffRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Re-diff against current baselines\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/rediff\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: rediffResultSchema } },\n description:\n \"Undecided snapshots re-diffed against the current baselines (no re-render). Decided snapshots are untouched.\",\n },\n 400: {\n content: { \"application/json\": { schema: errorSchema } },\n description: \"Default-branch builds are authoritative and are not re-diffed\",\n },\n 409: {\n content: { \"application/json\": { schema: errorSchema } },\n description: \"Build has not finished capturing or failed; retry capture instead\",\n },\n ...notFound,\n },\n});\n\nconst rejectAllRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Reject all open snapshots\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/reject-all\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"All snapshots rejected\",\n },\n ...notFound,\n },\n});\n\n/** Register the snapshot list, approve, reject, and bulk-review endpoints. */\nexport function registerSnapshots(app: ShelfRouter): void {\n app.openapi(listSnapshotsRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshots = new SnapshotModel(getStore().db).listByBuild(build.id);\n return c.json(await snapshots);\n });\n\n app.openapi(approveSnapshotRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const { force } = c.req.valid(\"query\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await snapshotForBuild(build, snapshotId);\n const userId = getStore().user?.id ?? null;\n try {\n await approveSnapshot(snapshotId, userId, { force: force === \"true\" });\n } catch (error) {\n if (!isHxRequest(c) || !isBaselineChanged(error)) {\n throw error;\n }\n // The review page recomputes staleness on load and offers re-diff / approve anyway.\n c.header(\"HX-Redirect\", `/projects/${slug}/builds/${buildId}/diff?snapshot=${snapshotId}`);\n return c.json({ ok: true }, 200);\n }\n await hxAdvance(c, { slug, buildId, snapshotId });\n return c.json({ ok: true }, 200);\n });\n\n app.openapi(rejectSnapshotRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshot = await snapshotForBuild(build, snapshotId);\n const userId = getStore().user?.id ?? null;\n await new SnapshotModel(getStore().db).review(snapshot.id, \"rejected\", userId);\n await refreshBuild(build.id);\n await hxAdvance(c, { slug, buildId, snapshotId });\n return c.json({ ok: true });\n });\n\n app.openapi(approveAllRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const { force } = c.req.valid(\"query\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const userId = getStore().user?.id ?? null;\n const forced = force === \"true\";\n const drifted = forced ? new Map() : await findDriftedSnapshots(project, build, snapshots);\n await Promise.all(\n snapshots\n .filter((snapshot) => isOpenSnapshot(snapshot) && !drifted.has(snapshot.id))\n .map(async (snapshot) => {\n await approveSnapshot(snapshot.id, userId, { force: forced });\n }),\n );\n hxRefresh(c);\n return c.json({ ok: true, skipped: [...drifted.keys()] });\n });\n\n app.openapi(rediffRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n if (build.isDefault) {\n throw new HTTPException(400, { message: \"Default-branch builds are not re-diffed\" });\n }\n if (!REDIFFABLE_BUILD_STATUSES.has(build.status)) {\n throw new HTTPException(409, {\n message: `Build is ${build.status}; wait for capture to finish or retry capture`,\n });\n }\n const { db, storage, logger } = getStore();\n const result = await rediffBuild({ db, storage, logger }, project, build);\n await refreshBuild(build.id);\n hxRefresh(c);\n flashHx(c, \"Re-diffed against current baselines\");\n return c.json({ ok: true, ...result }, 200);\n });\n\n app.openapi(rejectAllRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const userId = getStore().user?.id ?? null;\n await Promise.all(\n snapshots\n .filter((snapshot) => snapshot.status === \"new\" || snapshot.status === \"changed\")\n .map(async (snapshot) => {\n await new SnapshotModel(getStore().db).review(snapshot.id, \"rejected\", userId);\n }),\n );\n await refreshBuild(build.id);\n hxRefresh(c);\n return c.json({ ok: true });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"snapshots.mjs","names":[],"sources":["../../src/routers/snapshots.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { REDIFFABLE_BUILD_STATUSES, rediffBuild } from \"@storyshelf/core/capture\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { findDriftedSnapshots, isBaselineChanged, isOpenSnapshot } from \"./baseline-guard.ts\";\nimport {\n VIEW_ROLES,\n APPROVER_ROLES,\n snapshotForBuild,\n refreshBuild,\n approveSnapshot,\n buildForProject,\n} from \"./builds.handlers.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh, isHxRequest } from \"./htmx.ts\";\nimport { hxAdvance } from \"./review-next.ts\";\nimport {\n snapshotSchema,\n okSchema,\n approveAllResultSchema,\n groupDecisionResultSchema,\n rediffResultSchema,\n baselineChangedSchema,\n errorSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\nimport { approveGroup, rejectGroup, splitByDrift } from \"./snapshot-groups.ts\";\nconst listSnapshotsRoute = createRoute({\n method: \"get\",\n tags: [\"Review\"],\n summary: \"List snapshots\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: snapshotSchema.array() } },\n description: \"List snapshots for a build\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst approveSnapshotRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Approve a snapshot\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/approve\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }),\n query: z.object({ force: z.enum([\"true\", \"false\"]).optional() }),\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"Snapshot approved\",\n },\n 409: {\n content: { \"application/json\": { schema: baselineChangedSchema } },\n description:\n \"The baseline changed after this diff was computed; re-diff the build or pass force=true\",\n },\n ...notFound,\n },\n});\n\nconst rejectSnapshotRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Reject a snapshot\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/snapshots/{snapshotId}/reject\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), snapshotId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"Snapshot rejected\",\n },\n ...notFound,\n },\n});\n\nconst approveAllRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Approve all open snapshots\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/approve-all\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n query: z.object({ force: z.enum([\"true\", \"false\"]).optional() }),\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: approveAllResultSchema } },\n description: \"Open snapshots approved; snapshots with a changed baseline are skipped\",\n },\n ...notFound,\n },\n});\n\nconst groupParams = z.object({\n slug: z.string(),\n buildId: z.string(),\n storyId: z.string(),\n modeKey: z.string(),\n});\n\nconst approveGroupRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Approve a story/mode across all browsers\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/stories/{storyId}/modes/{modeKey}/approve\",\n request: {\n params: groupParams,\n query: z.object({ force: z.enum([\"true\", \"false\"]).optional() }),\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: groupDecisionResultSchema } },\n description:\n \"Every open snapshot of the story and mode approved and each engine's baseline promoted\",\n },\n 409: {\n content: { \"application/json\": { schema: baselineChangedSchema } },\n description:\n \"A member's baseline changed after its diff; nothing was approved. Re-diff or pass force=true\",\n },\n ...notFound,\n },\n});\n\nconst rejectGroupRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Reject a story/mode across all browsers\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/stories/{storyId}/modes/{modeKey}/reject\",\n request: { params: groupParams },\n responses: {\n 200: {\n content: { \"application/json\": { schema: groupDecisionResultSchema } },\n description: \"Every open snapshot of the story and mode rejected\",\n },\n ...notFound,\n },\n});\n\nconst rediffRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Re-diff against current baselines\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/rediff\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: rediffResultSchema } },\n description:\n \"Undecided snapshots re-diffed against the current baselines (no re-render). Decided snapshots are untouched.\",\n },\n 400: {\n content: { \"application/json\": { schema: errorSchema } },\n description: \"Default-branch builds are authoritative and are not re-diffed\",\n },\n 409: {\n content: { \"application/json\": { schema: errorSchema } },\n description: \"Build has not finished capturing or failed; retry capture instead\",\n },\n ...notFound,\n },\n});\n\nconst rejectAllRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Reject all open snapshots\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/reject-all\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: okSchema } },\n description: \"All snapshots rejected\",\n },\n ...notFound,\n },\n});\n\n/** Register the snapshot list, approve, reject, and bulk-review endpoints. */\nexport function registerSnapshots(app: ShelfRouter): void {\n app.openapi(listSnapshotsRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshots = new SnapshotModel(getStore().db).listByBuild(build.id);\n return c.json(await snapshots);\n });\n\n app.openapi(approveSnapshotRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const { force } = c.req.valid(\"query\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await snapshotForBuild(build, snapshotId);\n const userId = getStore().user?.id ?? null;\n try {\n await approveSnapshot(snapshotId, userId, { force: force === \"true\" });\n } catch (error) {\n if (!isHxRequest(c) || !isBaselineChanged(error)) {\n throw error;\n }\n // The review page recomputes staleness on load and offers re-diff / approve anyway.\n c.header(\"HX-Redirect\", `/projects/${slug}/builds/${buildId}/diff?snapshot=${snapshotId}`);\n return c.json({ ok: true }, 200);\n }\n await hxAdvance(c, { slug, buildId, snapshotId });\n return c.json({ ok: true }, 200);\n });\n\n app.openapi(rejectSnapshotRoute, async (c) => {\n const { slug, buildId, snapshotId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshot = await snapshotForBuild(build, snapshotId);\n const userId = getStore().user?.id ?? null;\n await new SnapshotModel(getStore().db).review(snapshot.id, \"rejected\", userId);\n await refreshBuild(build.id);\n await hxAdvance(c, { slug, buildId, snapshotId });\n return c.json({ ok: true });\n });\n\n app.openapi(approveAllRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const { force } = c.req.valid(\"query\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const userId = getStore().user?.id ?? null;\n const forced = force === \"true\";\n const drifted = forced ? new Map() : await findDriftedSnapshots(project, build, snapshots);\n // A story/mode is approved on every engine or on none: one drifted member skips its group.\n const { approvable, skipped } = splitByDrift(\n snapshots.filter((snapshot) => isOpenSnapshot(snapshot)),\n drifted,\n );\n await Promise.all(\n approvable.map(async (snapshot) => {\n await approveSnapshot(snapshot.id, userId, { force: forced, deferRefresh: true });\n }),\n );\n await refreshBuild(build.id);\n hxRefresh(c);\n return c.json({ ok: true, skipped: skipped.map((snapshot) => snapshot.id) });\n });\n\n app.openapi(approveGroupRoute, async (c) => {\n const { slug, buildId, storyId, modeKey } = c.req.valid(\"param\");\n const { force } = c.req.valid(\"query\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const userId = getStore().user?.id ?? null;\n let snapshotIds: string[];\n try {\n snapshotIds = await approveGroup(project, build, { storyId, modeKey }, userId, {\n force: force === \"true\",\n });\n } catch (error) {\n if (!isHxRequest(c) || !isBaselineChanged(error)) {\n throw error;\n }\n c.header(\"HX-Redirect\", `/projects/${slug}/builds/${buildId}/diff`);\n return c.json({ ok: true, snapshotIds: [] }, 200);\n }\n const first = snapshotIds[0];\n if (first) await hxAdvance(c, { slug, buildId, snapshotId: first });\n return c.json({ ok: true, snapshotIds }, 200);\n });\n\n app.openapi(rejectGroupRoute, async (c) => {\n const { slug, buildId, storyId, modeKey } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const userId = getStore().user?.id ?? null;\n const snapshotIds = await rejectGroup(build, { storyId, modeKey }, userId);\n const first = snapshotIds[0];\n if (first) await hxAdvance(c, { slug, buildId, snapshotId: first });\n return c.json({ ok: true, snapshotIds }, 200);\n });\n\n app.openapi(rediffRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n if (build.isDefault) {\n throw new HTTPException(400, { message: \"Default-branch builds are not re-diffed\" });\n }\n if (!REDIFFABLE_BUILD_STATUSES.has(build.status)) {\n throw new HTTPException(409, {\n message: `Build is ${build.status}; wait for capture to finish or retry capture`,\n });\n }\n const { db, storage, logger } = getStore();\n const result = await rediffBuild({ db, storage, logger }, project, build);\n await refreshBuild(build.id);\n hxRefresh(c);\n flashHx(c, \"Re-diffed against current baselines\");\n return c.json({ ok: true, ...result }, 200);\n });\n\n app.openapi(rejectAllRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const snapshots = await new SnapshotModel(getStore().db).listByBuild(build.id);\n const userId = getStore().user?.id ?? null;\n await Promise.all(\n snapshots\n .filter((snapshot) => snapshot.status === \"new\" || snapshot.status === \"changed\")\n .map(async (snapshot) => {\n await new SnapshotModel(getStore().db).review(snapshot.id, \"rejected\", userId);\n }),\n );\n await refreshBuild(build.id);\n hxRefresh(c);\n return c.json({ ok: true });\n });\n}\n"],"mappings":"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"}
|
|
@@ -2,55 +2,19 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { notFound } from "./helpers.mjs";
|
|
6
|
+
import { publishedAccess } from "./published-access.mjs";
|
|
7
|
+
import { publishedRelativePath, servePublishedFile } from "./published-files.mjs";
|
|
6
8
|
import { renderStorybookPage, renderStorybookPreparingPage } from "../pages/storybook.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
+
import { storybookDir } from "@storyshelf/core/utils";
|
|
10
|
+
import { BuildModel, LabelModel, ProjectModel } from "@storyshelf/core/models";
|
|
9
11
|
import { posix } from "node:path";
|
|
10
12
|
//#region src/routers/storybook.ts
|
|
11
|
-
const VIEW_ROLES = /* @__PURE__ */ new Set([
|
|
12
|
-
"viewer",
|
|
13
|
-
"developer",
|
|
14
|
-
"approver",
|
|
15
|
-
"admin"
|
|
16
|
-
]);
|
|
17
|
-
function contentTypeFor(path) {
|
|
18
|
-
return mimeFor(path);
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Decide whether the current request may view `build`'s published Storybook.
|
|
22
|
-
*
|
|
23
|
-
* Public builds (ADR 0011) are viewable without auth; every other build requires
|
|
24
|
-
* a logged-in session with at least `viewer` membership on the project.
|
|
25
|
-
*/
|
|
26
|
-
async function canViewBuild(build, project) {
|
|
27
|
-
if (isPublicBuild(project, build)) return true;
|
|
28
|
-
const { authEnabled } = getStore();
|
|
29
|
-
if (!authEnabled) return true;
|
|
30
|
-
const role = await currentProjectRole(project.id);
|
|
31
|
-
return Boolean(role && VIEW_ROLES.has(role));
|
|
32
|
-
}
|
|
33
13
|
/** Check whether a build's extracted statics are available for serving. */
|
|
34
14
|
async function staticsReady(projectId, buildId) {
|
|
35
15
|
if (await getStore().storage.exists(posix.join(storybookDir(projectId, buildId), "manifest.json"))) return true;
|
|
36
16
|
return await getStore().storage.exists(posix.join(storybookDir(projectId, buildId), "iframe.html"));
|
|
37
17
|
}
|
|
38
|
-
async function readWithManifestFallback(projectId, buildId, rel) {
|
|
39
|
-
try {
|
|
40
|
-
const manifestPath = posix.join(storybookDir(projectId, buildId), "manifest.json");
|
|
41
|
-
if (await getStore().storage.exists(manifestPath)) {
|
|
42
|
-
const manifestRaw = await getStore().storage.read(manifestPath);
|
|
43
|
-
const hash = JSON.parse(manifestRaw.toString("utf8"))[rel];
|
|
44
|
-
if (hash) {
|
|
45
|
-
const contentPath = `content/${hash}`;
|
|
46
|
-
if (await getStore().storage.exists(contentPath)) return await getStore().storage.read(contentPath);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
} catch {}
|
|
50
|
-
const legacyPath = posix.join(storybookDir(projectId, buildId), rel);
|
|
51
|
-
if (await getStore().storage.exists(legacyPath)) return await getStore().storage.read(legacyPath);
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
18
|
/** Register the published Storybook resolver and static-asset routes. */
|
|
55
19
|
function registerStorybook(app) {
|
|
56
20
|
app.get("/_", (c) => c.redirect("/", 302));
|
|
@@ -61,7 +25,7 @@ function registerStorybook(app) {
|
|
|
61
25
|
if (!project) notFound("Project not found");
|
|
62
26
|
const build = await new BuildModel(getStore().db).latestApprovedDefault(project);
|
|
63
27
|
if (!build) notFound("No approved build on the default branch yet");
|
|
64
|
-
if (!await
|
|
28
|
+
if (!await publishedAccess(build, project)) return c.redirect("/auth/login", 302);
|
|
65
29
|
return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);
|
|
66
30
|
});
|
|
67
31
|
app.get("/projects/:slug/storybook/:key/:value", async (c) => {
|
|
@@ -74,7 +38,7 @@ function registerStorybook(app) {
|
|
|
74
38
|
const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);
|
|
75
39
|
if (!buildId) notFound("No build carries that label");
|
|
76
40
|
const build = await new BuildModel(getStore().db).get(buildId);
|
|
77
|
-
if (!build || !await
|
|
41
|
+
if (!build || !await publishedAccess(build, project)) return c.redirect("/auth/login", 302);
|
|
78
42
|
return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);
|
|
79
43
|
});
|
|
80
44
|
app.get("/_/:id", async (c) => {
|
|
@@ -87,14 +51,8 @@ function registerStorybook(app) {
|
|
|
87
51
|
app.get("/_/:id/*", async (c) => {
|
|
88
52
|
const id = c.req.param("id");
|
|
89
53
|
const isUlid26 = id.length === 26 && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(id);
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
let rest;
|
|
93
|
-
try {
|
|
94
|
-
rest = decodeURIComponent(c.req.path.slice(base.length));
|
|
95
|
-
} catch {
|
|
96
|
-
return c.notFound();
|
|
97
|
-
}
|
|
54
|
+
const rest = publishedRelativePath(c.req.path, `/_/${id}/`);
|
|
55
|
+
if (rest === null) return c.notFound();
|
|
98
56
|
let project = null;
|
|
99
57
|
let build = null;
|
|
100
58
|
if (isUlid26) {
|
|
@@ -108,45 +66,31 @@ function registerStorybook(app) {
|
|
|
108
66
|
build = await new BuildModel(getStore().db).latestApprovedDefault(project);
|
|
109
67
|
if (!build) return c.notFound();
|
|
110
68
|
}
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
if (rest.split("/").some((segment) => !isSafeSegment(segment))) return c.notFound();
|
|
123
|
-
const buffer = await readWithManifestFallback(project.id, build.id, rest);
|
|
124
|
-
if (!buffer) {
|
|
125
|
-
if (rest === "index.html") return c.redirect(isUlid26 ? `/projects/${project.slug}/builds/${build.id}` : `/projects/${project.slug}`, 302);
|
|
126
|
-
return c.notFound();
|
|
127
|
-
}
|
|
128
|
-
const cacheControl = isUlid26 ? "public, max-age=31536000, immutable" : "public, max-age=3600";
|
|
129
|
-
return c.body(new Uint8Array(buffer), 200, {
|
|
130
|
-
"content-type": contentTypeFor(rest),
|
|
131
|
-
"cache-control": cacheControl
|
|
69
|
+
const access = await publishedAccess(build, project);
|
|
70
|
+
if (!access) return c.redirect("/auth/login", 302);
|
|
71
|
+
if (rest === "" && !await staticsReady(project.id, build.id)) return c.html(renderStorybookPreparingPage(project, build, project.slug), 200);
|
|
72
|
+
const fallback = isUlid26 ? `/projects/${project.slug}/builds/${build.id}` : `/projects/${project.slug}`;
|
|
73
|
+
const served = await servePublishedFile(c, {
|
|
74
|
+
projectId: project.id,
|
|
75
|
+
buildId: build.id,
|
|
76
|
+
rel: rest === "" ? "index.html" : rest,
|
|
77
|
+
access,
|
|
78
|
+
cacheControl: shortLinkCache(isUlid26, rest === "")
|
|
132
79
|
});
|
|
80
|
+
if (served) return served;
|
|
81
|
+
return rest === "" || rest === "index.html" ? c.redirect(fallback, 302) : c.notFound();
|
|
133
82
|
});
|
|
134
83
|
app.get("/projects/:slug/storybook/build/:buildId/*", async (c) => {
|
|
135
84
|
const slug = c.req.param("slug");
|
|
136
85
|
const buildId = c.req.param("buildId");
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
let rest;
|
|
140
|
-
try {
|
|
141
|
-
rest = decodeURIComponent(c.req.path.slice(base.length));
|
|
142
|
-
} catch {
|
|
143
|
-
return c.notFound();
|
|
144
|
-
}
|
|
86
|
+
const rest = publishedRelativePath(c.req.path, `/projects/${slug}/storybook/build/${buildId}/`);
|
|
87
|
+
if (rest === null) return c.notFound();
|
|
145
88
|
const project = await new ProjectModel(getStore().db).getBySlug(slug);
|
|
146
89
|
if (!project) notFound("Project not found");
|
|
147
90
|
const build = await new BuildModel(getStore().db).get(buildId);
|
|
148
91
|
if (!build || build.projectId !== project.id) notFound("Build not found");
|
|
149
|
-
|
|
92
|
+
const access = await publishedAccess(build, project);
|
|
93
|
+
if (!access) return c.redirect("/auth/login", 302);
|
|
150
94
|
if (rest === "") {
|
|
151
95
|
if (!await staticsReady(project.id, build.id)) return c.html(renderStorybookPreparingPage(project, build, slug), 200);
|
|
152
96
|
const storyId = c.req.query("storyId") ?? c.req.query("id") ?? void 0;
|
|
@@ -156,15 +100,20 @@ function registerStorybook(app) {
|
|
|
156
100
|
viewMode
|
|
157
101
|
}), 200);
|
|
158
102
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
});
|
|
103
|
+
return await servePublishedFile(c, {
|
|
104
|
+
projectId: project.id,
|
|
105
|
+
buildId: build.id,
|
|
106
|
+
rel: rest,
|
|
107
|
+
access,
|
|
108
|
+
cacheControl: "public, max-age=31536000, immutable"
|
|
109
|
+
}) ?? c.notFound();
|
|
166
110
|
});
|
|
167
111
|
}
|
|
112
|
+
/** Cache policy of the short link: a ULID names one build forever; a slug follows the latest. */
|
|
113
|
+
function shortLinkCache(isUlid26, isRoot) {
|
|
114
|
+
if (isUlid26) return "public, max-age=31536000, immutable";
|
|
115
|
+
return isRoot ? "public, max-age=60" : "public, max-age=3600";
|
|
116
|
+
}
|
|
168
117
|
//#endregion
|
|
169
118
|
export { registerStorybook };
|
|
170
119
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/routers/storybook.ts"],"sourcesContent":["import { isPublicBuild, BuildModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { Build } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { isSafeSegment, mimeFor, storybookDir } from \"@storyshelf/core/utils\";\nimport { posix } from \"node:path\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { renderStorybookPage, renderStorybookPreparingPage } from \"../pages/storybook.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { currentProjectRole, notFound } from \"./helpers.ts\";\nconst VIEW_ROLES: ReadonlySet<string> = new Set([\"viewer\", \"developer\", \"approver\", \"admin\"]);\n\nfunction contentTypeFor(path: string): string {\n return mimeFor(path);\n}\n\n/**\n * Decide whether the current request may view `build`'s published Storybook.\n *\n * Public builds (ADR 0011) are viewable without auth; every other build requires\n * a logged-in session with at least `viewer` membership on the project.\n */\nasync function canViewBuild(\n build: Pick<Build, \"public\" | \"gitBranch\">,\n project: Project,\n): Promise<boolean> {\n if (isPublicBuild(project, build)) {\n return true;\n }\n const { authEnabled } = getStore();\n if (!authEnabled) {\n return true;\n }\n const role = await currentProjectRole(project.id);\n return Boolean(role && VIEW_ROLES.has(role));\n}\n\n/** Check whether a build's extracted statics are available for serving. */\nasync function staticsReady(projectId: string, buildId: string): Promise<boolean> {\n // New path: manifest.json indicates content-addressed storage; fallback to legacy iframe.html\n if (\n await getStore().storage.exists(posix.join(storybookDir(projectId, buildId), \"manifest.json\"))\n ) {\n return true;\n }\n return await getStore().storage.exists(\n posix.join(storybookDir(projectId, buildId), \"iframe.html\"),\n );\n}\n\nasync function readWithManifestFallback(\n projectId: string,\n buildId: string,\n rel: string,\n): Promise<Buffer | null> {\n // Try manifest indirection first (content-addressed)\n try {\n const manifestPath = posix.join(storybookDir(projectId, buildId), \"manifest.json\");\n if (await getStore().storage.exists(manifestPath)) {\n const manifestRaw = await getStore().storage.read(manifestPath);\n const manifest = JSON.parse(manifestRaw.toString(\"utf8\")) as Record<string, string>;\n const hash = manifest[rel];\n if (hash) {\n const contentPath = `content/${hash}`;\n if (await getStore().storage.exists(contentPath)) {\n return await getStore().storage.read(contentPath);\n }\n }\n }\n } catch {\n // ignore — manifest may not exist for old builds\n }\n // Fallback to legacy per-build path\n const legacyPath = posix.join(storybookDir(projectId, buildId), rel);\n if (await getStore().storage.exists(legacyPath)) {\n return await getStore().storage.read(legacyPath);\n }\n return null;\n}\n\n/** Register the published Storybook resolver and static-asset routes. */\nexport function registerStorybook(app: ShelfRouter): void {\n app.get(\"/_\", (c) => c.redirect(\"/\", 302));\n app.get(\"/_/\", (c) => c.redirect(\"/\", 302));\n\n // Resolver: latest approved build on the default branch (never feature branches).\n app.get(\"/projects/:slug/storybook\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).latestApprovedDefault(project);\n if (!build) {\n notFound(\"No approved build on the default branch yet\");\n }\n if (!(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);\n });\n\n // Resolver: latest build bearing a label (`build` is a reserved label key).\n app.get(\"/projects/:slug/storybook/:key/:value\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n if (key === \"build\") {\n return c.notFound();\n }\n const value = c.req.param(\"value\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);\n if (!buildId) {\n notFound(\"No build carries that label\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || !(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);\n });\n\n // Short link canonical is /_/:id/ with trailing slash (so relative ./sb-manager/... resolves).\n // 26-char ULID → that exact build, whatever its status (visibility check only).\n // Any other id → project slug → latest approved build on the default branch, else 404\n // (no fallback to feature-branch or unreviewed builds). No fall-through on ULID.\n app.get(\"/_/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const isUlid26 = id.length === 26 && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(id);\n if (isUlid26) {\n const build = await new BuildModel(getStore().db).get(id);\n if (!build) notFound();\n } else {\n const project = await new ProjectModel(getStore().db).getBySlug(id);\n if (!project) notFound();\n }\n return c.redirect(`/_/${id}/`, 302);\n });\n\n app.get(\"/_/:id/*\", async (c) => {\n const id = c.req.param(\"id\");\n const isUlid26 = id.length === 26 && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(id);\n const base = `/_/${id}/`;\n if (!c.req.path.startsWith(base)) return c.notFound();\n let rest: string;\n try {\n rest = decodeURIComponent(c.req.path.slice(base.length));\n } catch {\n return c.notFound();\n }\n let project: Project | null = null;\n let build: Build | null = null;\n if (isUlid26) {\n build = await new BuildModel(getStore().db).get(id);\n if (!build) return c.notFound();\n project = await new ProjectModel(getStore().db).get(build.projectId);\n if (!project) return c.notFound();\n } else {\n project = await new ProjectModel(getStore().db).getBySlug(id);\n if (!project) return c.notFound();\n build = await new BuildModel(getStore().db).latestApprovedDefault(project);\n if (!build) return c.notFound();\n }\n if (!(await canViewBuild(build, project))) return c.redirect(\"/auth/login\", 302);\n if (rest === \"\") {\n if (!(await staticsReady(project.id, build.id))) {\n return c.html(renderStorybookPreparingPage(project, build, project.slug), 200);\n }\n const buffer = await readWithManifestFallback(project.id, build.id, \"index.html\");\n if (!buffer) {\n return c.redirect(\n isUlid26 ? `/projects/${project.slug}/builds/${build.id}` : `/projects/${project.slug}`,\n 302,\n );\n }\n const cacheControl = isUlid26 ? \"public, max-age=31536000, immutable\" : \"public, max-age=60\";\n return c.body(new Uint8Array(buffer), 200, {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": cacheControl,\n });\n }\n const segments = rest.split(\"/\");\n if (segments.some((segment) => !isSafeSegment(segment))) return c.notFound();\n const buffer = await readWithManifestFallback(project.id, build.id, rest);\n if (!buffer) {\n if (rest === \"index.html\") {\n return c.redirect(\n isUlid26 ? `/projects/${project.slug}/builds/${build.id}` : `/projects/${project.slug}`,\n 302,\n );\n }\n return c.notFound();\n }\n const cacheControl = isUlid26 ? \"public, max-age=31536000, immutable\" : \"public, max-age=3600\";\n return c.body(new Uint8Array(buffer), 200, {\n \"content-type\": contentTypeFor(rest),\n \"cache-control\": cacheControl,\n });\n });\n\n // Canonical build route: serves the static Storybook and the landing page.\n app.get(\"/projects/:slug/storybook/build/:buildId/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // remainder from the raw request path (un-encoded) and decode it ourselves.\n const base = `/projects/${slug}/storybook/build/${buildId}/`;\n if (!c.req.path.startsWith(base)) {\n return c.notFound();\n }\n let rest: string;\n try {\n rest = decodeURIComponent(c.req.path.slice(base.length));\n } catch {\n return c.notFound();\n }\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n notFound(\"Build not found\");\n }\n if (!(await canViewBuild(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n\n if (rest === \"\") {\n if (!(await staticsReady(project.id, build.id))) {\n return c.html(renderStorybookPreparingPage(project, build, slug), 200);\n }\n const storyId = c.req.query(\"storyId\") ?? c.req.query(\"id\") ?? undefined;\n const viewMode = c.req.query(\"viewMode\") === \"docs\" ? \"docs\" : undefined;\n return c.html(renderStorybookPage(project, build, slug, { storyId, viewMode }), 200);\n }\n\n const segments = rest.split(\"/\");\n if (segments.some((segment) => !isSafeSegment(segment))) {\n return c.notFound();\n }\n const buffer = await readWithManifestFallback(project.id, build.id, rest);\n if (!buffer) return c.notFound();\n return c.body(new Uint8Array(buffer), 200, {\n \"content-type\": contentTypeFor(rest),\n \"cache-control\": \"public, max-age=31536000, immutable\",\n });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"storybook.mjs","names":[],"sources":["../../src/routers/storybook.ts"],"sourcesContent":["import { BuildModel } from \"@storyshelf/core/models\";\nimport { LabelModel } from \"@storyshelf/core/models\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport type { Build } from \"@storyshelf/core/schema\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { storybookDir } from \"@storyshelf/core/utils\";\nimport { posix } from \"node:path\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { renderStorybookPage, renderStorybookPreparingPage } from \"../pages/storybook.tsx\";\nimport { getStore } from \"../store.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { publishedAccess } from \"./published-access.ts\";\nimport { publishedRelativePath, servePublishedFile } from \"./published-files.ts\";\n\n/** Check whether a build's extracted statics are available for serving. */\nasync function staticsReady(projectId: string, buildId: string): Promise<boolean> {\n // New path: manifest.json indicates content-addressed storage; fallback to legacy iframe.html\n if (\n await getStore().storage.exists(posix.join(storybookDir(projectId, buildId), \"manifest.json\"))\n ) {\n return true;\n }\n return await getStore().storage.exists(\n posix.join(storybookDir(projectId, buildId), \"iframe.html\"),\n );\n}\n\n/** Register the published Storybook resolver and static-asset routes. */\nexport function registerStorybook(app: ShelfRouter): void {\n app.get(\"/_\", (c) => c.redirect(\"/\", 302));\n app.get(\"/_/\", (c) => c.redirect(\"/\", 302));\n\n // Resolver: latest approved build on the default branch (never feature branches).\n app.get(\"/projects/:slug/storybook\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).latestApprovedDefault(project);\n if (!build) {\n notFound(\"No approved build on the default branch yet\");\n }\n if (!(await publishedAccess(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);\n });\n\n // Resolver: latest build bearing a label (`build` is a reserved label key).\n app.get(\"/projects/:slug/storybook/:key/:value\", async (c) => {\n const slug = c.req.param(\"slug\");\n const key = c.req.param(\"key\");\n if (key === \"build\") {\n return c.notFound();\n }\n const value = c.req.param(\"value\");\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);\n if (!buildId) {\n notFound(\"No build carries that label\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || !(await publishedAccess(build, project))) {\n return c.redirect(\"/auth/login\", 302);\n }\n return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);\n });\n\n // Short link canonical is /_/:id/ with trailing slash (so relative ./sb-manager/... resolves).\n // 26-char ULID → that exact build, whatever its status (visibility check only).\n // Any other id → project slug → latest approved build on the default branch, else 404\n // (no fallback to feature-branch or unreviewed builds). No fall-through on ULID.\n app.get(\"/_/:id\", async (c) => {\n const id = c.req.param(\"id\");\n const isUlid26 = id.length === 26 && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(id);\n if (isUlid26) {\n const build = await new BuildModel(getStore().db).get(id);\n if (!build) notFound();\n } else {\n const project = await new ProjectModel(getStore().db).getBySlug(id);\n if (!project) notFound();\n }\n return c.redirect(`/_/${id}/`, 302);\n });\n\n app.get(\"/_/:id/*\", async (c) => {\n const id = c.req.param(\"id\");\n const isUlid26 = id.length === 26 && /^[0-7][0-9A-HJKMNP-TV-Z]{25}$/u.test(id);\n const rest = publishedRelativePath(c.req.path, `/_/${id}/`);\n if (rest === null) return c.notFound();\n let project: Project | null = null;\n let build: Build | null = null;\n if (isUlid26) {\n build = await new BuildModel(getStore().db).get(id);\n if (!build) return c.notFound();\n project = await new ProjectModel(getStore().db).get(build.projectId);\n if (!project) return c.notFound();\n } else {\n project = await new ProjectModel(getStore().db).getBySlug(id);\n if (!project) return c.notFound();\n build = await new BuildModel(getStore().db).latestApprovedDefault(project);\n if (!build) return c.notFound();\n }\n const access = await publishedAccess(build, project);\n if (!access) return c.redirect(\"/auth/login\", 302);\n if (rest === \"\" && !(await staticsReady(project.id, build.id))) {\n return c.html(renderStorybookPreparingPage(project, build, project.slug), 200);\n }\n const fallback = isUlid26\n ? `/projects/${project.slug}/builds/${build.id}`\n : `/projects/${project.slug}`;\n const served = await servePublishedFile(c, {\n projectId: project.id,\n buildId: build.id,\n rel: rest === \"\" ? \"index.html\" : rest,\n access,\n cacheControl: shortLinkCache(isUlid26, rest === \"\"),\n });\n if (served) return served;\n return rest === \"\" || rest === \"index.html\" ? c.redirect(fallback, 302) : c.notFound();\n });\n\n // Canonical build route: serves the static Storybook and the landing page.\n app.get(\"/projects/:slug/storybook/build/:buildId/*\", async (c) => {\n const slug = c.req.param(\"slug\");\n const buildId = c.req.param(\"buildId\");\n // Hono's bare `/*` wildcard is not exposed through `param()`, so derive the\n // remainder from the raw request path (un-encoded) and decode it ourselves.\n const rest = publishedRelativePath(c.req.path, `/projects/${slug}/storybook/build/${buildId}/`);\n if (rest === null) {\n return c.notFound();\n }\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== project.id) {\n notFound(\"Build not found\");\n }\n const access = await publishedAccess(build, project);\n if (!access) {\n return c.redirect(\"/auth/login\", 302);\n }\n\n if (rest === \"\") {\n if (!(await staticsReady(project.id, build.id))) {\n return c.html(renderStorybookPreparingPage(project, build, slug), 200);\n }\n const storyId = c.req.query(\"storyId\") ?? c.req.query(\"id\") ?? undefined;\n const viewMode = c.req.query(\"viewMode\") === \"docs\" ? \"docs\" : undefined;\n return c.html(renderStorybookPage(project, build, slug, { storyId, viewMode }), 200);\n }\n\n return (\n (await servePublishedFile(c, {\n projectId: project.id,\n buildId: build.id,\n rel: rest,\n access,\n cacheControl: \"public, max-age=31536000, immutable\",\n })) ?? c.notFound()\n );\n });\n}\n\n/** Cache policy of the short link: a ULID names one build forever; a slug follows the latest. */\nfunction shortLinkCache(isUlid26: boolean, isRoot: boolean): string {\n if (isUlid26) {\n return \"public, max-age=31536000, immutable\";\n }\n return isRoot ? \"public, max-age=60\" : \"public, max-age=3600\";\n}\n"],"mappings":"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"}
|