@storyshelf/app 0.6.2 → 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/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 +203 -125
- 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 +91 -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 +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 +3 -1
- 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 -9
- 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,52 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { currentProjectRole } from "./helpers.mjs";
|
|
6
|
+
import { isPublicBuild } from "@storyshelf/core/models";
|
|
7
|
+
//#region src/routers/published-access.ts
|
|
8
|
+
/**
|
|
9
|
+
* The one policy for reading a build's published files (ADR 0007, decision 5; ADR 0011): a signed
|
|
10
|
+
* token naming the build, or a build anyone may open (public, or an instance without sign-in), or
|
|
11
|
+
* a signed-in viewer of the project. Every route that serves published files asks
|
|
12
|
+
* {@link publishedAccess} and takes its CORS headers from {@link publishedCorsHeaders}.
|
|
13
|
+
*/
|
|
14
|
+
const VIEW_ROLES = /* @__PURE__ */ new Set([
|
|
15
|
+
"viewer",
|
|
16
|
+
"developer",
|
|
17
|
+
"approver",
|
|
18
|
+
"admin"
|
|
19
|
+
]);
|
|
20
|
+
/**
|
|
21
|
+
* Whether the request may read `build`'s published files, and how. A `grant` (verified claims of a
|
|
22
|
+
* signed URL) decides alone: it opens exactly the build it names and nothing falls through to the
|
|
23
|
+
* other rules, so a token that does not fit answers like a missing one.
|
|
24
|
+
*/
|
|
25
|
+
async function publishedAccess(build, project, grant) {
|
|
26
|
+
if (grant) return grant.projectId === project.id && grant.buildId === build.id && build.projectId === project.id ? "token" : null;
|
|
27
|
+
if (isAnonymouslyViewable(build, project)) return "public";
|
|
28
|
+
const role = await currentProjectRole(project.id);
|
|
29
|
+
return role && VIEW_ROLES.has(role) ? "session" : null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether anyone who can reach the app may fetch the build's published Storybook with no
|
|
33
|
+
* credentials: the instance has no sign-in, or the build is public (ADR 0011).
|
|
34
|
+
*/
|
|
35
|
+
function isAnonymouslyViewable(build, project) {
|
|
36
|
+
return !getStore().authEnabled || isPublicBuild(project, build);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* CORS headers of a published file. A sandboxed preview frame runs on an opaque origin, so its
|
|
40
|
+
* module scripts are cross-origin requests without cookies: they load only when the file answers
|
|
41
|
+
* `Access-Control-Allow-Origin: *`. That is offered exactly where no credentials are involved, for
|
|
42
|
+
* a public build and for a signed URL (which is itself the credential, never sent as a cookie or
|
|
43
|
+
* header, and never with `Allow-Credentials`). A signed-in viewer's private build answers without
|
|
44
|
+
* it: only same-origin pages may read it.
|
|
45
|
+
*/
|
|
46
|
+
function publishedCorsHeaders(access) {
|
|
47
|
+
return access === "session" ? {} : { "access-control-allow-origin": "*" };
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { isAnonymouslyViewable, publishedAccess, publishedCorsHeaders };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=published-access.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"published-access.mjs","names":[],"sources":["../../src/routers/published-access.ts"],"sourcesContent":["/**\n * The one policy for reading a build's published files (ADR 0007, decision 5; ADR 0011): a signed\n * token naming the build, or a build anyone may open (public, or an instance without sign-in), or\n * a signed-in viewer of the project. Every route that serves published files asks\n * {@link publishedAccess} and takes its CORS headers from {@link publishedCorsHeaders}.\n */\nimport { isPublicBuild } from \"@storyshelf/core/models\";\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport type { StorybookTokenClaims } from \"@storyshelf/core/utils\";\nimport { getStore } from \"../store.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\n\n/** How a request may read a build: by its token, because the build is open, or by a session. */\nexport type PublishedAccess = \"token\" | \"public\" | \"session\";\n\nconst VIEW_ROLES: ReadonlySet<string> = new Set([\"viewer\", \"developer\", \"approver\", \"admin\"]);\n\n/**\n * Whether the request may read `build`'s published files, and how. A `grant` (verified claims of a\n * signed URL) decides alone: it opens exactly the build it names and nothing falls through to the\n * other rules, so a token that does not fit answers like a missing one.\n */\nexport async function publishedAccess(\n build: Pick<Build, \"id\" | \"projectId\" | \"public\" | \"gitBranch\">,\n project: Project,\n grant?: StorybookTokenClaims,\n): Promise<PublishedAccess | null> {\n if (grant) {\n return grant.projectId === project.id &&\n grant.buildId === build.id &&\n build.projectId === project.id\n ? \"token\"\n : null;\n }\n if (isAnonymouslyViewable(build, project)) {\n return \"public\";\n }\n const role = await currentProjectRole(project.id);\n return role && VIEW_ROLES.has(role) ? \"session\" : null;\n}\n\n/**\n * Whether anyone who can reach the app may fetch the build's published Storybook with no\n * credentials: the instance has no sign-in, or the build is public (ADR 0011).\n */\nexport function isAnonymouslyViewable(\n build: Pick<Build, \"public\" | \"gitBranch\">,\n project: Project,\n): boolean {\n return !getStore().authEnabled || isPublicBuild(project, build);\n}\n\n/**\n * CORS headers of a published file. A sandboxed preview frame runs on an opaque origin, so its\n * module scripts are cross-origin requests without cookies: they load only when the file answers\n * `Access-Control-Allow-Origin: *`. That is offered exactly where no credentials are involved, for\n * a public build and for a signed URL (which is itself the credential, never sent as a cookie or\n * header, and never with `Allow-Credentials`). A signed-in viewer's private build answers without\n * it: only same-origin pages may read it.\n */\nexport function publishedCorsHeaders(access: PublishedAccess): Record<string, string> {\n return access === \"session\" ? {} : { \"access-control-allow-origin\": \"*\" };\n}\n"],"mappings":";;;;;;;;;;;;;AAeA,MAAM,6BAAkC,IAAI,IAAI;CAAC;CAAU;CAAa;CAAY;AAAO,CAAC;;;;;;AAO5F,eAAsB,gBACpB,OACA,SACA,OACiC;CACjC,IAAI,OACF,OAAO,MAAM,cAAc,QAAQ,MACjC,MAAM,YAAY,MAAM,MACxB,MAAM,cAAc,QAAQ,KAC1B,UACA;CAEN,IAAI,sBAAsB,OAAO,OAAO,GACtC,OAAO;CAET,MAAM,OAAO,MAAM,mBAAmB,QAAQ,EAAE;CAChD,OAAO,QAAQ,WAAW,IAAI,IAAI,IAAI,YAAY;AACpD;;;;;AAMA,SAAgB,sBACd,OACA,SACS;CACT,OAAO,CAAC,SAAS,CAAC,CAAC,eAAe,cAAc,SAAS,KAAK;AAChE;;;;;;;;;AAUA,SAAgB,qBAAqB,QAAiD;CACpF,OAAO,WAAW,YAAY,CAAC,IAAI,EAAE,+BAA+B,IAAI;AAC1E"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { publishedCorsHeaders } from "./published-access.mjs";
|
|
6
|
+
import { isSafeSegment, mimeFor } from "@storyshelf/core/utils";
|
|
7
|
+
import { readPublishedFile } from "@storyshelf/core/capture";
|
|
8
|
+
//#region src/routers/published-files.ts
|
|
9
|
+
/**
|
|
10
|
+
* Serving a build's published files (ADR 0010, ADR 0007 decision 5): the single place that decodes
|
|
11
|
+
* and checks a request path, reads the file through the build's manifest and sets the headers.
|
|
12
|
+
* The published Storybook routes (`/projects/:slug/storybook/build/:id/*`, `/_/:id/*`) and the
|
|
13
|
+
* signed URL route (`/_s/:token/*`) all end here, each bringing the access {@link publishedAccess}
|
|
14
|
+
* granted.
|
|
15
|
+
*/
|
|
16
|
+
/** Headers of every signed URL answer: a token URL must not be cached, indexed or leaked by `Referer`. */
|
|
17
|
+
const SIGNED_URL_HEADERS = {
|
|
18
|
+
"cache-control": "private, no-store",
|
|
19
|
+
"referrer-policy": "no-referrer",
|
|
20
|
+
"x-robots-tag": "noindex, nofollow",
|
|
21
|
+
"x-content-type-options": "nosniff"
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The file path after `base`, decoded once. `""` is the root; `null` when the path is not under
|
|
25
|
+
* `base` or could leave the build: empty, `.` or `..` segments, backslashes and NUL are refused.
|
|
26
|
+
*/
|
|
27
|
+
function publishedRelativePath(path, base) {
|
|
28
|
+
if (!path.startsWith(base)) return null;
|
|
29
|
+
let rest;
|
|
30
|
+
try {
|
|
31
|
+
rest = decodeURIComponent(path.slice(base.length));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return rest === "" || rest.split("/").every((segment) => segment !== "" && segment !== "." && !segment.includes("\0") && isSafeSegment(segment)) ? rest : null;
|
|
36
|
+
}
|
|
37
|
+
/** The file as a response, or `null` when the build has no such file. */
|
|
38
|
+
async function servePublishedFile(c, request) {
|
|
39
|
+
const { projectId, buildId, rel, access } = request;
|
|
40
|
+
const file = await readPublishedFile(getStore().storage, projectId, buildId, rel);
|
|
41
|
+
if (!file) return null;
|
|
42
|
+
const headers = access === "token" ? SIGNED_URL_HEADERS : { "cache-control": request.cacheControl };
|
|
43
|
+
return c.body(new Uint8Array(file), 200, {
|
|
44
|
+
...headers,
|
|
45
|
+
...publishedCorsHeaders(access),
|
|
46
|
+
"content-type": mimeFor(rel)
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { SIGNED_URL_HEADERS, publishedRelativePath, servePublishedFile };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=published-files.mjs.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { resolveAuthorizedProject, resolveReviewer } from "./helpers.mjs";
|
|
5
|
+
import { hxRefresh } from "./htmx.mjs";
|
|
6
|
+
import { flash } from "./flash.mjs";
|
|
7
|
+
import { buildReviewCreateSchema, commentCreateSchema } from "./schemas.mjs";
|
|
8
|
+
import { APPROVER_ROLES, DEVELOPER_ROLES, buildForProject } from "./builds.handlers.mjs";
|
|
9
|
+
import { giveVerdict } from "./build-reviews.handlers.mjs";
|
|
10
|
+
import { createComment, resolveBuildComment } from "./comments.mjs";
|
|
11
|
+
import { HTTPException } from "hono/http-exception";
|
|
12
|
+
//#region src/routers/review-forms.ts
|
|
13
|
+
/** Register the form routes of review (`review` class). */
|
|
14
|
+
function registerReviewForms(app) {
|
|
15
|
+
const base = "/projects/:slug/builds/:buildId";
|
|
16
|
+
app.post(`${base}/comments`, async (c) => {
|
|
17
|
+
const { project, build } = await target(c, DEVELOPER_ROLES);
|
|
18
|
+
const input = commentCreateSchema.safeParse(await fields(c));
|
|
19
|
+
if (!input.success) throw new HTTPException(400, { message: "A comment needs some text (up to 4000 characters)" });
|
|
20
|
+
await createComment(project, build.id, input.data);
|
|
21
|
+
return done(c, build.id, "Comment added");
|
|
22
|
+
});
|
|
23
|
+
app.post(`${base}/comments/:commentId/resolve`, async (c) => {
|
|
24
|
+
const { build } = await target(c, DEVELOPER_ROLES);
|
|
25
|
+
await resolveBuildComment(build.id, c.req.param("commentId"));
|
|
26
|
+
return done(c, build.id, "Comment resolved");
|
|
27
|
+
});
|
|
28
|
+
app.post(`${base}/reviews`, async (c) => {
|
|
29
|
+
const { project, build } = await target(c, APPROVER_ROLES);
|
|
30
|
+
const input = buildReviewCreateSchema.safeParse(await fields(c));
|
|
31
|
+
if (!input.success) throw new HTTPException(400, { message: "Choose approve or reject" });
|
|
32
|
+
await giveVerdict(project, build, await resolveReviewer(c), input.data);
|
|
33
|
+
return done(c, build.id, "Verdict recorded");
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async function target(c, roles) {
|
|
37
|
+
const project = await resolveAuthorizedProject(c, c.req.param("slug") ?? "", ...roles);
|
|
38
|
+
return {
|
|
39
|
+
project,
|
|
40
|
+
build: await buildForProject(project.id, c.req.param("buildId") ?? "")
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** The form's text fields, blanks left out (an empty `storyId` means no story). */
|
|
44
|
+
async function fields(c) {
|
|
45
|
+
const form = await c.req.formData();
|
|
46
|
+
const values = {};
|
|
47
|
+
for (const [key, value] of form.entries()) if (typeof value === "string" && key !== "csrf_token" && value.trim() !== "") values[key] = value.trim();
|
|
48
|
+
return values;
|
|
49
|
+
}
|
|
50
|
+
/** HTMX reloads the page it posted from; a plain form goes back to the page, else the build. */
|
|
51
|
+
function done(c, buildId, message) {
|
|
52
|
+
flash(c, message);
|
|
53
|
+
if (c.req.header("HX-Request") === "true") {
|
|
54
|
+
hxRefresh(c);
|
|
55
|
+
return c.body(null, 204);
|
|
56
|
+
}
|
|
57
|
+
return c.redirect(returnPath(c, buildId), 303);
|
|
58
|
+
}
|
|
59
|
+
/** The path the form was posted from when it is on this host, else the build page. */
|
|
60
|
+
function returnPath(c, buildId) {
|
|
61
|
+
const fallback = `/projects/${c.req.param("slug") ?? ""}/builds/${buildId}`;
|
|
62
|
+
try {
|
|
63
|
+
const referer = new URL(c.req.header("referer") ?? "", c.req.url);
|
|
64
|
+
return referer.origin === new URL(c.req.url).origin ? `${referer.pathname}${referer.search}` : fallback;
|
|
65
|
+
} catch {
|
|
66
|
+
return fallback;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { registerReviewForms };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=review-forms.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-forms.mjs","names":[],"sources":["../../src/routers/review-forms.ts"],"sourcesContent":["/**\n * The review forms of the build, diff and component pages. HTMX and plain forms post\n * `application/x-www-form-urlencoded`, which the JSON endpoints under `/api/v1` refuse (415), so\n * each form has a page route that reads the form and calls the same handlers as the API. All\n * three are `review` routes: they answer 404 for a project with review off.\n */\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { giveVerdict } from \"./build-reviews.handlers.ts\";\nimport { APPROVER_ROLES, DEVELOPER_ROLES, buildForProject } from \"./builds.handlers.ts\";\nimport { createComment, resolveBuildComment } from \"./comments.ts\";\nimport { flash } from \"./flash.ts\";\nimport { resolveAuthorizedProject, resolveReviewer } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport { buildReviewCreateSchema, commentCreateSchema } from \"./schemas.ts\";\n\n/** Register the form routes of review (`review` class). */\nexport function registerReviewForms(app: ShelfRouter): void {\n const base = \"/projects/:slug/builds/:buildId\";\n app.post(`${base}/comments`, async (c) => {\n const { project, build } = await target(c, DEVELOPER_ROLES);\n const input = commentCreateSchema.safeParse(await fields(c));\n if (!input.success) {\n throw new HTTPException(400, {\n message: \"A comment needs some text (up to 4000 characters)\",\n });\n }\n await createComment(project, build.id, input.data);\n return done(c, build.id, \"Comment added\");\n });\n\n app.post(`${base}/comments/:commentId/resolve`, async (c) => {\n const { build } = await target(c, DEVELOPER_ROLES);\n await resolveBuildComment(build.id, c.req.param(\"commentId\"));\n return done(c, build.id, \"Comment resolved\");\n });\n\n app.post(`${base}/reviews`, async (c) => {\n const { project, build } = await target(c, APPROVER_ROLES);\n const input = buildReviewCreateSchema.safeParse(await fields(c));\n if (!input.success) {\n throw new HTTPException(400, { message: \"Choose approve or reject\" });\n }\n await giveVerdict(project, build, await resolveReviewer(c), input.data);\n return done(c, build.id, \"Verdict recorded\");\n });\n}\n\nasync function target(\n c: Context,\n roles: readonly ProjectRole[],\n): Promise<{ project: Project; build: Build }> {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\") ?? \"\", ...roles);\n const build = await buildForProject(project.id, c.req.param(\"buildId\") ?? \"\");\n return { project, build };\n}\n\n/** The form's text fields, blanks left out (an empty `storyId` means no story). */\nasync function fields(c: Context): Promise<Record<string, string>> {\n const form = await c.req.formData();\n const values: Record<string, string> = {};\n for (const [key, value] of form.entries()) {\n if (typeof value === \"string\" && key !== \"csrf_token\" && value.trim() !== \"\") {\n values[key] = value.trim();\n }\n }\n return values;\n}\n\n/** HTMX reloads the page it posted from; a plain form goes back to the page, else the build. */\nfunction done(c: Context, buildId: string, message: string): Response {\n flash(c, message);\n if (c.req.header(\"HX-Request\") === \"true\") {\n hxRefresh(c);\n return c.body(null, 204);\n }\n return c.redirect(returnPath(c, buildId), 303);\n}\n\n/** The path the form was posted from when it is on this host, else the build page. */\nfunction returnPath(c: Context, buildId: string): string {\n const slug = c.req.param(\"slug\") ?? \"\";\n const fallback = `/projects/${slug}/builds/${buildId}`;\n try {\n const referer = new URL(c.req.header(\"referer\") ?? \"\", c.req.url);\n return referer.origin === new URL(c.req.url).origin\n ? `${referer.pathname}${referer.search}`\n : fallback;\n } catch {\n return fallback;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,SAAgB,oBAAoB,KAAwB;CAC1D,MAAM,OAAO;CACb,IAAI,KAAK,GAAG,KAAK,YAAY,OAAO,MAAM;EACxC,MAAM,EAAE,SAAS,UAAU,MAAM,OAAO,GAAG,eAAe;EAC1D,MAAM,QAAQ,oBAAoB,UAAU,MAAM,OAAO,CAAC,CAAC;EAC3D,IAAI,CAAC,MAAM,SACT,MAAM,IAAI,cAAc,KAAK,EAC3B,SAAS,oDACX,CAAC;EAEH,MAAM,cAAc,SAAS,MAAM,IAAI,MAAM,IAAI;EACjD,OAAO,KAAK,GAAG,MAAM,IAAI,eAAe;CAC1C,CAAC;CAED,IAAI,KAAK,GAAG,KAAK,+BAA+B,OAAO,MAAM;EAC3D,MAAM,EAAE,UAAU,MAAM,OAAO,GAAG,eAAe;EACjD,MAAM,oBAAoB,MAAM,IAAI,EAAE,IAAI,MAAM,WAAW,CAAC;EAC5D,OAAO,KAAK,GAAG,MAAM,IAAI,kBAAkB;CAC7C,CAAC;CAED,IAAI,KAAK,GAAG,KAAK,WAAW,OAAO,MAAM;EACvC,MAAM,EAAE,SAAS,UAAU,MAAM,OAAO,GAAG,cAAc;EACzD,MAAM,QAAQ,wBAAwB,UAAU,MAAM,OAAO,CAAC,CAAC;EAC/D,IAAI,CAAC,MAAM,SACT,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,2BAA2B,CAAC;EAEtE,MAAM,YAAY,SAAS,OAAO,MAAM,gBAAgB,CAAC,GAAG,MAAM,IAAI;EACtE,OAAO,KAAK,GAAG,MAAM,IAAI,kBAAkB;CAC7C,CAAC;AACH;AAEA,eAAe,OACb,GACA,OAC6C;CAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,MAAM,KAAK,IAAI,GAAG,KAAK;CAErF,OAAO;EAAE;EAAS,OAAA,MADE,gBAAgB,QAAQ,IAAI,EAAE,IAAI,MAAM,SAAS,KAAK,EAAE;CACpD;AAC1B;;AAGA,eAAe,OAAO,GAA6C;CACjE,MAAM,OAAO,MAAM,EAAE,IAAI,SAAS;CAClC,MAAM,SAAiC,CAAC;CACxC,KAAK,MAAM,CAAC,KAAK,UAAU,KAAK,QAAQ,GACtC,IAAI,OAAO,UAAU,YAAY,QAAQ,gBAAgB,MAAM,KAAK,MAAM,IACxE,OAAO,OAAO,MAAM,KAAK;CAG7B,OAAO;AACT;;AAGA,SAAS,KAAK,GAAY,SAAiB,SAA2B;CACpE,MAAM,GAAG,OAAO;CAChB,IAAI,EAAE,IAAI,OAAO,YAAY,MAAM,QAAQ;EACzC,UAAU,CAAC;EACX,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB;CACA,OAAO,EAAE,SAAS,WAAW,GAAG,OAAO,GAAG,GAAG;AAC/C;;AAGA,SAAS,WAAW,GAAY,SAAyB;CAEvD,MAAM,WAAW,aADJ,EAAE,IAAI,MAAM,MAAM,KAAK,GACD,UAAU;CAC7C,IAAI;EACF,MAAM,UAAU,IAAI,IAAI,EAAE,IAAI,OAAO,SAAS,KAAK,IAAI,EAAE,IAAI,GAAG;EAChE,OAAO,QAAQ,WAAW,IAAI,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC,SACzC,GAAG,QAAQ,WAAW,QAAQ,WAC9B;CACN,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { currentProjectRole } from "./helpers.mjs";
|
|
6
|
+
import { reviewStateOf } from "./build-reviews.handlers.mjs";
|
|
7
|
+
import { publishingOf } from "@storyshelf/core/capture";
|
|
8
|
+
import { CommentModel, UserModel, changesetOf } from "@storyshelf/core/models";
|
|
9
|
+
//#region src/routers/review-section.handlers.ts
|
|
10
|
+
/** Data behind the review section of the build page (ADR 0013): verdicts, changeset and comments. */
|
|
11
|
+
/** Whether review is on for the project (a row without the column counts as on). */
|
|
12
|
+
function reviewIsOn(project) {
|
|
13
|
+
return project.reviewEnabled !== false;
|
|
14
|
+
}
|
|
15
|
+
/** The viewer's review rights: everything with auth off, else by their role on the project. */
|
|
16
|
+
async function reviewCapabilities(project) {
|
|
17
|
+
if (!getStore().authEnabled) return {
|
|
18
|
+
canComment: true,
|
|
19
|
+
canVerdict: true
|
|
20
|
+
};
|
|
21
|
+
const role = await currentProjectRole(project.id);
|
|
22
|
+
return {
|
|
23
|
+
canComment: role === "developer" || role === "approver" || role === "admin",
|
|
24
|
+
canVerdict: role === "approver" || role === "admin"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The review section of a build; null when the project has review off. */
|
|
28
|
+
async function loadReviewSection(project, build, catalog) {
|
|
29
|
+
if (!reviewIsOn(project)) return null;
|
|
30
|
+
const { db, testing } = getStore();
|
|
31
|
+
const [state, changeset, comments, capabilities, publishing] = await Promise.all([
|
|
32
|
+
reviewStateOf(project, build),
|
|
33
|
+
changesetOf(db, project, build),
|
|
34
|
+
new CommentModel(db).listByBuild(build.id),
|
|
35
|
+
reviewCapabilities(project),
|
|
36
|
+
publishingOrPending(build)
|
|
37
|
+
]);
|
|
38
|
+
const shown = comments.filter((comment) => comment.snapshotId === null);
|
|
39
|
+
return {
|
|
40
|
+
project,
|
|
41
|
+
build,
|
|
42
|
+
state,
|
|
43
|
+
changeset,
|
|
44
|
+
firstStory: firstStoryOf(catalog),
|
|
45
|
+
comments: shown,
|
|
46
|
+
names: await namesOf([...state.latest, ...shown].map((item) => item.userId)),
|
|
47
|
+
testing,
|
|
48
|
+
publishing,
|
|
49
|
+
...capabilities
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** How publishing stands; a storage probe that fails reads as the storage-independent state. */
|
|
53
|
+
async function publishingOrPending(build) {
|
|
54
|
+
try {
|
|
55
|
+
return await publishingOf(getStore().storage, build);
|
|
56
|
+
} catch {
|
|
57
|
+
return await publishingOf(void 0, build);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Display names of the given users; ids that no longer resolve show as the id. */
|
|
61
|
+
async function namesOf(userIds) {
|
|
62
|
+
const users = new UserModel(getStore().db);
|
|
63
|
+
const ids = [...new Set(userIds.filter((id) => id !== null))];
|
|
64
|
+
const found = await Promise.all(ids.map(async (id) => [id, await users.get(id)]));
|
|
65
|
+
return new Map(found.map(([id, user]) => [id, user?.name ?? user?.email ?? id]));
|
|
66
|
+
}
|
|
67
|
+
/** Story file -> id of its first story. */
|
|
68
|
+
function firstStoryOf(catalog) {
|
|
69
|
+
const first = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const entry of catalog?.entries ?? []) if (entry.type === "story" && !first.has(entry.importPath)) first.set(entry.importPath, entry.id);
|
|
71
|
+
return first;
|
|
72
|
+
}
|
|
73
|
+
/** Story comments of `build` for a component page; null when the project has review off. */
|
|
74
|
+
async function loadComponentReview(project, build) {
|
|
75
|
+
if (!reviewIsOn(project)) return null;
|
|
76
|
+
const [comments, capabilities] = await Promise.all([new CommentModel(getStore().db).listByBuild(build.id), reviewCapabilities(project)]);
|
|
77
|
+
return {
|
|
78
|
+
comments: comments.filter((comment) => comment.storyId !== null),
|
|
79
|
+
canComment: capabilities.canComment
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { loadComponentReview, loadReviewSection, namesOf, reviewCapabilities, reviewIsOn };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=review-section.handlers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-section.handlers.mjs","names":[],"sources":["../../src/routers/review-section.handlers.ts"],"sourcesContent":["/** Data behind the review section of the build page (ADR 0013): verdicts, changeset and comments. */\nimport { publishingOf, type Catalog } from \"@storyshelf/core/capture\";\nimport { changesetOf, CommentModel, UserModel, type BuildChangeset } from \"@storyshelf/core/models\";\nimport type { Build, Comment, Project } from \"@storyshelf/core/schema\";\nimport { getStore } from \"../store.ts\";\nimport { reviewStateOf, type ReviewState } from \"./build-reviews.handlers.ts\";\nimport { currentProjectRole } from \"./helpers.ts\";\n\n/** What the viewer may do in review. */\nexport interface ReviewCapabilities {\n /** Write build, story and snapshot comments (developer and above). */\n canComment: boolean;\n /** Give a verdict (approver and admin). */\n canVerdict: boolean;\n}\n\n/** Everything the review cards render. */\nexport interface ReviewSectionData extends ReviewCapabilities {\n project: Project;\n build: Build;\n /** The verdicts and what they make of the build. */\n state: ReviewState;\n changeset: BuildChangeset;\n /** First story of each story file in the build, to link a component to its published story. */\n firstStory: ReadonlyMap<string, string>;\n /** Build and story comments (snapshot comments belong to the diff view). */\n comments: Comment[];\n /** Display names of the users behind verdicts and comments, by user id. */\n names: ReadonlyMap<string, string>;\n /** Whether the snapshot rollup decides the build, so the verdict is a sign-off only. */\n testing: boolean;\n /** Whether the build's Storybook is published; a verdict waits for `success` (ADR 0013). */\n publishing: Awaited<ReturnType<typeof publishingOf>>;\n}\n\n/** Whether review is on for the project (a row without the column counts as on). */\nexport function reviewIsOn(project: Pick<Project, \"reviewEnabled\">): boolean {\n return project.reviewEnabled !== false;\n}\n\n/** The viewer's review rights: everything with auth off, else by their role on the project. */\nexport async function reviewCapabilities(project: Project): Promise<ReviewCapabilities> {\n if (!getStore().authEnabled) {\n return { canComment: true, canVerdict: true };\n }\n const role = await currentProjectRole(project.id);\n return {\n canComment: role === \"developer\" || role === \"approver\" || role === \"admin\",\n canVerdict: role === \"approver\" || role === \"admin\",\n };\n}\n\n/** The review section of a build; null when the project has review off. */\nexport async function loadReviewSection(\n project: Project,\n build: Build,\n catalog: Catalog | null,\n): Promise<ReviewSectionData | null> {\n if (!reviewIsOn(project)) {\n return null;\n }\n const { db, testing } = getStore();\n const [state, changeset, comments, capabilities, publishing] = await Promise.all([\n reviewStateOf(project, build),\n changesetOf(db, project, build),\n new CommentModel(db).listByBuild(build.id),\n reviewCapabilities(project),\n publishingOrPending(build),\n ]);\n const shown = comments.filter((comment) => comment.snapshotId === null);\n return {\n project,\n build,\n state,\n changeset,\n firstStory: firstStoryOf(catalog),\n comments: shown,\n names: await namesOf([...state.latest, ...shown].map((item) => item.userId)),\n testing,\n publishing,\n ...capabilities,\n };\n}\n\n/** How publishing stands; a storage probe that fails reads as the storage-independent state. */\nasync function publishingOrPending(build: Build): Promise<ReviewSectionData[\"publishing\"]> {\n try {\n return await publishingOf(getStore().storage, build);\n } catch {\n return await publishingOf(undefined, build);\n }\n}\n\n/** Display names of the given users; ids that no longer resolve show as the id. */\nexport async function namesOf(userIds: readonly (string | null)[]): Promise<Map<string, string>> {\n const users = new UserModel(getStore().db);\n const ids = [...new Set(userIds.filter((id): id is string => id !== null))];\n const found = await Promise.all(ids.map(async (id) => [id, await users.get(id)] as const));\n return new Map(found.map(([id, user]) => [id, user?.name ?? user?.email ?? id]));\n}\n\n/** Story file -> id of its first story. */\nfunction firstStoryOf(catalog: Catalog | null): Map<string, string> {\n const first = new Map<string, string>();\n for (const entry of catalog?.entries ?? []) {\n if (entry.type === \"story\" && !first.has(entry.importPath)) {\n first.set(entry.importPath, entry.id);\n }\n }\n return first;\n}\n\n/** What a component page needs of review: the story comments of the build it shows. */\nexport interface ComponentReview {\n comments: Comment[];\n canComment: boolean;\n}\n\n/** Story comments of `build` for a component page; null when the project has review off. */\nexport async function loadComponentReview(\n project: Project,\n build: Build,\n): Promise<ComponentReview | null> {\n if (!reviewIsOn(project)) {\n return null;\n }\n const [comments, capabilities] = await Promise.all([\n new CommentModel(getStore().db).listByBuild(build.id),\n reviewCapabilities(project),\n ]);\n return {\n comments: comments.filter((comment) => comment.storyId !== null),\n canComment: capabilities.canComment,\n };\n}\n"],"mappings":";;;;;;;;;;;AAoCA,SAAgB,WAAW,SAAkD;CAC3E,OAAO,QAAQ,kBAAkB;AACnC;;AAGA,eAAsB,mBAAmB,SAA+C;CACtF,IAAI,CAAC,SAAS,CAAC,CAAC,aACd,OAAO;EAAE,YAAY;EAAM,YAAY;CAAK;CAE9C,MAAM,OAAO,MAAM,mBAAmB,QAAQ,EAAE;CAChD,OAAO;EACL,YAAY,SAAS,eAAe,SAAS,cAAc,SAAS;EACpE,YAAY,SAAS,cAAc,SAAS;CAC9C;AACF;;AAGA,eAAsB,kBACpB,SACA,OACA,SACmC;CACnC,IAAI,CAAC,WAAW,OAAO,GACrB,OAAO;CAET,MAAM,EAAE,IAAI,YAAY,SAAS;CACjC,MAAM,CAAC,OAAO,WAAW,UAAU,cAAc,cAAc,MAAM,QAAQ,IAAI;EAC/E,cAAc,SAAS,KAAK;EAC5B,YAAY,IAAI,SAAS,KAAK;EAC9B,IAAI,aAAa,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE;EACzC,mBAAmB,OAAO;EAC1B,oBAAoB,KAAK;CAC3B,CAAC;CACD,MAAM,QAAQ,SAAS,QAAQ,YAAY,QAAQ,eAAe,IAAI;CACtE,OAAO;EACL;EACA;EACA;EACA;EACA,YAAY,aAAa,OAAO;EAChC,UAAU;EACV,OAAO,MAAM,QAAQ,CAAC,GAAG,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,KAAK,SAAS,KAAK,MAAM,CAAC;EAC3E;EACA;EACA,GAAG;CACL;AACF;;AAGA,eAAe,oBAAoB,OAAwD;CACzF,IAAI;EACF,OAAO,MAAM,aAAa,SAAS,CAAC,CAAC,SAAS,KAAK;CACrD,QAAQ;EACN,OAAO,MAAM,aAAa,KAAA,GAAW,KAAK;CAC5C;AACF;;AAGA,eAAsB,QAAQ,SAAmE;CAC/F,MAAM,QAAQ,IAAI,UAAU,SAAS,CAAC,CAAC,EAAE;CACzC,MAAM,MAAM,CAAC,GAAG,IAAI,IAAI,QAAQ,QAAQ,OAAqB,OAAO,IAAI,CAAC,CAAC;CAC1E,MAAM,QAAQ,MAAM,QAAQ,IAAI,IAAI,IAAI,OAAO,OAAO,CAAC,IAAI,MAAM,MAAM,IAAI,EAAE,CAAC,CAAU,CAAC;CACzF,OAAO,IAAI,IAAI,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,IAAI,MAAM,QAAQ,MAAM,SAAS,EAAE,CAAC,CAAC;AACjF;;AAGA,SAAS,aAAa,SAA8C;CAClE,MAAM,wBAAQ,IAAI,IAAoB;CACtC,KAAK,MAAM,SAAS,SAAS,WAAW,CAAC,GACvC,IAAI,MAAM,SAAS,WAAW,CAAC,MAAM,IAAI,MAAM,UAAU,GACvD,MAAM,IAAI,MAAM,YAAY,MAAM,EAAE;CAGxC,OAAO;AACT;;AASA,eAAsB,oBACpB,SACA,OACiC;CACjC,IAAI,CAAC,WAAW,OAAO,GACrB,OAAO;CAET,MAAM,CAAC,UAAU,gBAAgB,MAAM,QAAQ,IAAI,CACjD,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,GACpD,mBAAmB,OAAO,CAC5B,CAAC;CACD,OAAO;EACL,UAAU,SAAS,QAAQ,YAAY,QAAQ,YAAY,IAAI;EAC/D,YAAY,aAAa;CAC3B;AACF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { requestBuildState } from "../git-status.mjs";
|
|
6
|
+
import { acceptPendingWithoutWork } from "@storyshelf/core/models";
|
|
7
|
+
//#region src/routers/review-switch-off.ts
|
|
8
|
+
/** The switch-off pass at the app level (ADR 0013, decision 1): accept the queue review leaves behind. */
|
|
9
|
+
/**
|
|
10
|
+
* When a save turned review off (`before` had it on), accept the project's pending builds that no
|
|
11
|
+
* capture worked on, in one pass. It raises no approval event (nobody approved anything), but each
|
|
12
|
+
* build's git channels are asked to show its new state. Returns the number of builds accepted.
|
|
13
|
+
*/
|
|
14
|
+
async function runSwitchOffPass(before, after) {
|
|
15
|
+
if (before.reviewEnabled === false || after.reviewEnabled !== false) return 0;
|
|
16
|
+
const accepted = await acceptPendingWithoutWork(getStore().db, after.id);
|
|
17
|
+
await Promise.all(accepted.map(async (build) => {
|
|
18
|
+
await requestBuildState(build.id);
|
|
19
|
+
}));
|
|
20
|
+
getStore().logger.info({
|
|
21
|
+
projectId: after.id,
|
|
22
|
+
accepted: accepted.length
|
|
23
|
+
}, "review turned off: accepted pending builds");
|
|
24
|
+
return accepted.length;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { runSwitchOffPass };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=review-switch-off.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-switch-off.mjs","names":[],"sources":["../../src/routers/review-switch-off.ts"],"sourcesContent":["/** The switch-off pass at the app level (ADR 0013, decision 1): accept the queue review leaves behind. */\nimport { acceptPendingWithoutWork } from \"@storyshelf/core/models\";\nimport type { Project } from \"@storyshelf/core/schema\";\nimport { requestBuildState } from \"../git-status.ts\";\nimport { getStore } from \"../store.ts\";\n\n/**\n * When a save turned review off (`before` had it on), accept the project's pending builds that no\n * capture worked on, in one pass. It raises no approval event (nobody approved anything), but each\n * build's git channels are asked to show its new state. Returns the number of builds accepted.\n */\nexport async function runSwitchOffPass(before: Project, after: Project): Promise<number> {\n if (before.reviewEnabled === false || after.reviewEnabled !== false) {\n return 0;\n }\n const accepted = await acceptPendingWithoutWork(getStore().db, after.id);\n await Promise.all(\n accepted.map(async (build) => {\n await requestBuildState(build.id);\n }),\n );\n getStore().logger.info(\n { projectId: after.id, accepted: accepted.length },\n \"review turned off: accepted pending builds\",\n );\n return accepted.length;\n}\n"],"mappings":";;;;;;;;;;;;;AAWA,eAAsB,iBAAiB,QAAiB,OAAiC;CACvF,IAAI,OAAO,kBAAkB,SAAS,MAAM,kBAAkB,OAC5D,OAAO;CAET,MAAM,WAAW,MAAM,yBAAyB,SAAS,CAAC,CAAC,IAAI,MAAM,EAAE;CACvE,MAAM,QAAQ,IACZ,SAAS,IAAI,OAAO,UAAU;EAC5B,MAAM,kBAAkB,MAAM,EAAE;CAClC,CAAC,CACH;CACA,SAAS,CAAC,CAAC,OAAO,KAChB;EAAE,WAAW,MAAM;EAAI,UAAU,SAAS;CAAO,GACjD,4CACF;CACA,OAAO,SAAS;AAClB"}
|
package/dist/routers/schemas.mjs
CHANGED
|
@@ -2,9 +2,22 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { z } from "@hono/zod-openapi";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { storybookUrlTtlOverrideError } from "@storyshelf/core/utils";
|
|
6
|
+
import { WebhookModel } from "@storyshelf/core/models";
|
|
7
|
+
import { BUILD_STATUSES, PROJECT_ROLES, REVIEW_VERDICTS, SNAPSHOT_STATUSES } from "@storyshelf/core/types";
|
|
8
|
+
import { BROWSER_NAMES, TARGET_NAME_PATTERN } from "@storyshelf/core/adapter/capture-runner";
|
|
7
9
|
//#region src/routers/schemas.ts
|
|
10
|
+
/** A capture target: an engine (`chromium`) or a remote one, `<engine>@<name>` (ADR 0007). */
|
|
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
|
+
const reviewApprovalsSchema = z.number().int().min(1).max(20);
|
|
13
|
+
/** Seconds a signed Storybook URL stays valid (ADR 0007, decision 5). */
|
|
14
|
+
const storybookUrlTtlSchema = z.number().int().superRefine((seconds, ctx) => {
|
|
15
|
+
const message = storybookUrlTtlOverrideError(seconds);
|
|
16
|
+
if (message !== void 0) ctx.addIssue({
|
|
17
|
+
code: "custom",
|
|
18
|
+
message
|
|
19
|
+
});
|
|
20
|
+
});
|
|
8
21
|
const storybookMetaSchema = z.record(z.string(), z.unknown()).nullable().optional();
|
|
9
22
|
/** OpenAPI schema for a project record. */
|
|
10
23
|
const projectSchema = z.object({
|
|
@@ -19,7 +32,23 @@ const projectSchema = z.object({
|
|
|
19
32
|
storybookMeta: z.string().nullable().optional(),
|
|
20
33
|
executePlay: z.boolean(),
|
|
21
34
|
playTimeoutMs: z.number(),
|
|
35
|
+
/** Axe accessibility checks run during capture (default on; results never block a build). */
|
|
36
|
+
runA11y: z.boolean().optional(),
|
|
37
|
+
/** Storybook 10 `.test()` entries run as interaction tests during capture (default off). */
|
|
38
|
+
runTests: z.boolean().optional(),
|
|
22
39
|
browser: z.string().optional(),
|
|
40
|
+
/** JSON array of capture targets, primary first, primary first; null = just `browser`. */
|
|
41
|
+
browsers: z.string().nullable().optional(),
|
|
42
|
+
/** JSON array of mode names to capture; null = every declared mode. */
|
|
43
|
+
modesAllowlist: z.string().nullable().optional(),
|
|
44
|
+
/** AI profile gate: null/absent = AI off for this project. */
|
|
45
|
+
aiProfile: z.string().nullable().optional(),
|
|
46
|
+
/** Review is on for this project (ADR 0013, decision 1). */
|
|
47
|
+
reviewEnabled: z.boolean().optional(),
|
|
48
|
+
/** Approvals a build needs when `testing` is off. */
|
|
49
|
+
reviewApprovals: z.number().optional(),
|
|
50
|
+
/** Seconds the signed Storybook URLs of a render stay valid; null derives it from the stories. */
|
|
51
|
+
storybookUrlTtlSeconds: z.number().nullable().optional(),
|
|
23
52
|
createdAt: z.string(),
|
|
24
53
|
updatedAt: z.string()
|
|
25
54
|
}).openapi("Project");
|
|
@@ -30,7 +59,13 @@ const projectCreateSchema = z.object({
|
|
|
30
59
|
gitDefaultBranch: z.string().optional(),
|
|
31
60
|
storybookMeta: storybookMetaSchema,
|
|
32
61
|
executePlay: z.boolean().optional(),
|
|
33
|
-
playTimeoutMs: z.number().int().min(1e3).max(3e4).optional()
|
|
62
|
+
playTimeoutMs: z.number().int().min(1e3).max(3e4).optional(),
|
|
63
|
+
/** Axe accessibility checks during capture; on when omitted. */
|
|
64
|
+
runA11y: z.boolean().optional(),
|
|
65
|
+
/** Run Storybook 10 `.test()` entries as interaction tests during capture; off when omitted. */
|
|
66
|
+
runTests: z.boolean().optional(),
|
|
67
|
+
reviewEnabled: z.boolean().optional(),
|
|
68
|
+
reviewApprovals: reviewApprovalsSchema.optional()
|
|
34
69
|
}).openapi("ProjectCreateInput");
|
|
35
70
|
/** OpenAPI schema for patching a project. */
|
|
36
71
|
const projectUpdateSchema = z.object({
|
|
@@ -43,7 +78,24 @@ const projectUpdateSchema = z.object({
|
|
|
43
78
|
storybookMeta: storybookMetaSchema,
|
|
44
79
|
executePlay: z.boolean().optional(),
|
|
45
80
|
playTimeoutMs: z.number().int().min(1e3).max(3e4).optional(),
|
|
46
|
-
|
|
81
|
+
/** Turn axe accessibility checks during capture on or off (admins). */
|
|
82
|
+
runA11y: z.boolean().optional(),
|
|
83
|
+
/** Turn the Storybook 10 `.test()` runs on or off (admins). */
|
|
84
|
+
runTests: z.boolean().optional(),
|
|
85
|
+
/** Single capture target; clears `browsers`. */
|
|
86
|
+
browser: captureTargetSchema.optional(),
|
|
87
|
+
/** Capture targets, primary first (max 3); null returns to single `browser`. */
|
|
88
|
+
browsers: z.array(captureTargetSchema).min(1).max(3).nullable().optional(),
|
|
89
|
+
/** Mode names to capture; null runs every declared mode. */
|
|
90
|
+
modesAllowlist: z.array(z.string().min(1)).max(50).nullable().optional(),
|
|
91
|
+
/** Site admins only: a configured AI profile name, or null to turn AI off. */
|
|
92
|
+
aiProfile: z.string().min(1).nullable().optional(),
|
|
93
|
+
/** Turn review (comments and build-level verdicts) on or off for the project. */
|
|
94
|
+
reviewEnabled: z.boolean().optional(),
|
|
95
|
+
/** Approvals a build needs before it is accepted when `testing` is off. */
|
|
96
|
+
reviewApprovals: reviewApprovalsSchema.optional(),
|
|
97
|
+
/** Seconds (at least one minimum slice plus grace, 1050, to 86400) the signed Storybook URLs of a render stay valid; null derives it from the stories. */
|
|
98
|
+
storybookUrlTtlSeconds: storybookUrlTtlSchema.nullable().optional()
|
|
47
99
|
}).openapi("ProjectUpdateInput");
|
|
48
100
|
/** OpenAPI schema for a build record. */
|
|
49
101
|
const buildSchema = z.object({
|
|
@@ -61,10 +113,13 @@ const buildSchema = z.object({
|
|
|
61
113
|
changedCount: z.number(),
|
|
62
114
|
approvedCount: z.number(),
|
|
63
115
|
rejectedCount: z.number(),
|
|
116
|
+
inheritedCount: z.number(),
|
|
64
117
|
affectedOnly: z.boolean(),
|
|
65
118
|
baselineSha: z.string().nullable(),
|
|
66
119
|
changedFiles: z.string().nullable(),
|
|
67
120
|
affectedImportPaths: z.string().nullable(),
|
|
121
|
+
/** JSON array of the build's Storybook `.test()` results (id, title, name, status, error); null when none ran. */
|
|
122
|
+
testResults: z.string().nullable().optional(),
|
|
68
123
|
createdAt: z.string(),
|
|
69
124
|
updatedAt: z.string()
|
|
70
125
|
}).openapi("Build");
|
|
@@ -78,6 +133,8 @@ const snapshotSchema = z.object({
|
|
|
78
133
|
storyTitle: z.string(),
|
|
79
134
|
storyImportPath: z.string().nullable(),
|
|
80
135
|
viewportName: z.string(),
|
|
136
|
+
/** Globals JSON the capture's mode applied; null for plain viewports. */
|
|
137
|
+
mode: z.string().nullable().optional(),
|
|
81
138
|
viewportWidth: z.number(),
|
|
82
139
|
viewportHeight: z.number(),
|
|
83
140
|
screenshotPath: z.string(),
|
|
@@ -89,6 +146,8 @@ const snapshotSchema = z.object({
|
|
|
89
146
|
reviewedBy: z.string().nullable(),
|
|
90
147
|
reviewedAt: z.string().nullable(),
|
|
91
148
|
inherited: z.boolean(),
|
|
149
|
+
/** JSON array of the axe violations found on this capture (non-blocking); null when none. */
|
|
150
|
+
a11yViolations: z.string().nullable().optional(),
|
|
92
151
|
createdAt: z.string(),
|
|
93
152
|
updatedAt: z.string()
|
|
94
153
|
}).openapi("Snapshot");
|
|
@@ -133,6 +192,8 @@ const commentSchema = z.object({
|
|
|
133
192
|
projectId: z.string(),
|
|
134
193
|
buildId: z.string(),
|
|
135
194
|
snapshotId: z.string().nullable(),
|
|
195
|
+
/** Storybook story id of a story-anchored comment. */
|
|
196
|
+
storyId: z.string().nullable().optional(),
|
|
136
197
|
userId: z.string().nullable(),
|
|
137
198
|
body: z.string(),
|
|
138
199
|
parentId: z.string().nullable(),
|
|
@@ -143,9 +204,39 @@ const commentSchema = z.object({
|
|
|
143
204
|
/** OpenAPI schema for creating a build comment. */
|
|
144
205
|
const commentCreateSchema = z.object({
|
|
145
206
|
body: z.string().min(1),
|
|
207
|
+
/** Anchor to a snapshot; not together with `storyId`. */
|
|
146
208
|
snapshotId: z.string().optional(),
|
|
209
|
+
/** Anchor to a story of the build; not together with `snapshotId`. */
|
|
210
|
+
storyId: z.string().min(1).optional(),
|
|
147
211
|
parentId: z.string().optional()
|
|
148
212
|
}).openapi("CommentCreateInput");
|
|
213
|
+
/** OpenAPI schema for a build-level review verdict. */
|
|
214
|
+
const buildReviewSchema = z.object({
|
|
215
|
+
id: z.string(),
|
|
216
|
+
buildId: z.string(),
|
|
217
|
+
userId: z.string().nullable(),
|
|
218
|
+
verdict: z.enum(REVIEW_VERDICTS),
|
|
219
|
+
body: z.string().nullable(),
|
|
220
|
+
createdAt: z.string()
|
|
221
|
+
}).openapi("BuildReview");
|
|
222
|
+
/** OpenAPI schema for giving a build a verdict. */
|
|
223
|
+
const buildReviewCreateSchema = z.object({
|
|
224
|
+
verdict: z.enum(REVIEW_VERDICTS),
|
|
225
|
+
body: z.string().max(4e3).optional()
|
|
226
|
+
}).openapi("BuildReviewInput");
|
|
227
|
+
/** OpenAPI schema for a build's review state: the verdicts and what they make of the build. */
|
|
228
|
+
const buildReviewStateSchema = z.object({
|
|
229
|
+
/** Build status now. */
|
|
230
|
+
status: z.enum(BUILD_STATUSES),
|
|
231
|
+
/** Approvals the project requires. */
|
|
232
|
+
approvalsRequired: z.number(),
|
|
233
|
+
/** Approvals among the reviewers' latest verdicts. */
|
|
234
|
+
approvals: z.number(),
|
|
235
|
+
/** Every reviewer's latest verdict. */
|
|
236
|
+
latest: buildReviewSchema.array(),
|
|
237
|
+
/** Every verdict given, oldest first. */
|
|
238
|
+
history: buildReviewSchema.array()
|
|
239
|
+
}).openapi("BuildReviewState");
|
|
149
240
|
/** OpenAPI schema for a project label type. */
|
|
150
241
|
const labelTypeSchema = z.object({
|
|
151
242
|
id: z.string(),
|
|
@@ -218,9 +309,10 @@ const webhookPublicSchema = z.object({
|
|
|
218
309
|
url: z.string(),
|
|
219
310
|
events: z.array(z.string())
|
|
220
311
|
}).openapi("Webhook");
|
|
312
|
+
const webhookUrlBase = z.string().url();
|
|
221
313
|
/** OpenAPI schema for creating a webhook. */
|
|
222
314
|
const webhookCreateSchema = z.object({
|
|
223
|
-
url:
|
|
315
|
+
url: webhookUrlBase.refine((url) => WebhookModel.urlError(url) === null, { message: "URL must use http or https" }),
|
|
224
316
|
events: z.array(z.string().min(1)).optional()
|
|
225
317
|
}).openapi("WebhookCreateInput");
|
|
226
318
|
/** OpenAPI schema for a newly created webhook including its signing secret. */
|
|
@@ -230,22 +322,6 @@ const webhookCreatedSchema = z.object({
|
|
|
230
322
|
events: z.array(z.string()),
|
|
231
323
|
secret: z.string()
|
|
232
324
|
}).openapi("WebhookCreated");
|
|
233
|
-
/** OpenAPI schema for a git status-check configuration. */
|
|
234
|
-
const statusConfigSchema = z.object({
|
|
235
|
-
id: z.string(),
|
|
236
|
-
projectId: z.string(),
|
|
237
|
-
provider: z.string(),
|
|
238
|
-
config: z.record(z.string(), z.unknown()),
|
|
239
|
-
hasToken: z.boolean(),
|
|
240
|
-
createdAt: z.string(),
|
|
241
|
-
updatedAt: z.string()
|
|
242
|
-
}).openapi("StatusConfig");
|
|
243
|
-
/** OpenAPI schema for creating a git status-check configuration. */
|
|
244
|
-
const statusConfigCreateSchema = z.object({
|
|
245
|
-
provider: z.string().min(1),
|
|
246
|
-
config: z.record(z.string(), z.unknown()),
|
|
247
|
-
token: z.string().min(1)
|
|
248
|
-
}).openapi("StatusConfigCreateInput");
|
|
249
325
|
/** OpenAPI schema for a public notification channel (secret omitted). */
|
|
250
326
|
const notificationChannelSchema = z.object({
|
|
251
327
|
id: z.string(),
|
|
@@ -255,6 +331,8 @@ const notificationChannelSchema = z.object({
|
|
|
255
331
|
hasSecret: z.boolean(),
|
|
256
332
|
events: z.array(z.string()),
|
|
257
333
|
enabled: z.boolean(),
|
|
334
|
+
lastError: z.string().nullable(),
|
|
335
|
+
lastErrorAt: z.string().nullable(),
|
|
258
336
|
createdAt: z.string(),
|
|
259
337
|
updatedAt: z.string()
|
|
260
338
|
}).openapi("NotificationChannel");
|
|
@@ -287,6 +365,11 @@ const approveAllResultSchema = z.object({
|
|
|
287
365
|
ok: z.boolean(),
|
|
288
366
|
skipped: z.array(z.string())
|
|
289
367
|
}).openapi("ApproveAllResult");
|
|
368
|
+
/** Group decision result: the snapshots one story/mode decision covered. */
|
|
369
|
+
const groupDecisionResultSchema = z.object({
|
|
370
|
+
ok: z.boolean(),
|
|
371
|
+
snapshotIds: z.array(z.string())
|
|
372
|
+
}).openapi("GroupDecisionResult");
|
|
290
373
|
/** Result of re-diffing a build against the baselines that apply now. */
|
|
291
374
|
const rediffResultSchema = z.object({
|
|
292
375
|
ok: z.boolean(),
|
|
@@ -331,7 +414,42 @@ const notFound = { 404: {
|
|
|
331
414
|
content: { "application/json": { schema: errorSchema } },
|
|
332
415
|
description: "Not found"
|
|
333
416
|
} };
|
|
417
|
+
/** OpenAPI schema for an insight (build triage or project health). */
|
|
418
|
+
const insightSchema = z.object({
|
|
419
|
+
id: z.string(),
|
|
420
|
+
kind: z.enum(["triage", "health"]),
|
|
421
|
+
buildId: z.string().nullable(),
|
|
422
|
+
window: z.string().nullable(),
|
|
423
|
+
status: z.enum([
|
|
424
|
+
"pending",
|
|
425
|
+
"running",
|
|
426
|
+
"done",
|
|
427
|
+
"failed"
|
|
428
|
+
]),
|
|
429
|
+
profile: z.string(),
|
|
430
|
+
model: z.string(),
|
|
431
|
+
promptVersion: z.string(),
|
|
432
|
+
verdict: z.string().nullable(),
|
|
433
|
+
summary: z.string().nullable(),
|
|
434
|
+
output: z.unknown().nullable(),
|
|
435
|
+
meta: z.unknown().nullable(),
|
|
436
|
+
errorCode: z.string().nullable(),
|
|
437
|
+
createdAt: z.string(),
|
|
438
|
+
startedAt: z.string().nullable(),
|
|
439
|
+
finishedAt: z.string().nullable()
|
|
440
|
+
}).openapi("Insight");
|
|
441
|
+
/** Request body to (re)generate a build triage. */
|
|
442
|
+
const insightRequestSchema = z.object({
|
|
443
|
+
task: z.literal("triage").optional(),
|
|
444
|
+
force: z.boolean().optional(),
|
|
445
|
+
profile: z.string().min(1).optional()
|
|
446
|
+
}).openapi("InsightRequest");
|
|
447
|
+
/** Request body to (re)generate a project-health digest. */
|
|
448
|
+
const healthRequestSchema = z.object({
|
|
449
|
+
window: z.string().optional(),
|
|
450
|
+
force: z.boolean().optional()
|
|
451
|
+
}).openapi("HealthInsightRequest");
|
|
334
452
|
//#endregion
|
|
335
|
-
export { approveAllResultSchema, attemptLogSchema, attemptSchema, badRequest, baselineChangedSchema, buildSchema, commentCreateSchema, commentSchema, errorSchema, forbidden, groupMappingCreateSchema, groupMappingSchema, labelTypeCreateSchema, labelTypeSchema, labelTypeUpdateSchema, memberRoleSchema, memberSchema, memberSetSchema, notFound, notificationChannelCreateSchema, notificationChannelSchema, notificationSubscriptionPutSchema, notificationSubscriptionSchema, okSchema, projectCreateSchema, projectSchema, projectUpdateSchema, purgeInputSchema, purgeSchema, rediffResultSchema, snapshotSchema,
|
|
453
|
+
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 };
|
|
336
454
|
|
|
337
455
|
//# sourceMappingURL=schemas.mjs.map
|