@storyshelf/app 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +2 -6
- package/dist/asset-manifest.mjs +14 -7
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +9 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +9 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +79 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +17 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +74 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +45 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +58 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +96 -119
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +33 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +57 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/credential-keys.mjs +1 -2
- package/dist/credentials.mjs +1 -2
- package/dist/git-status.mjs +45 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +23 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -5
- package/dist/index.mjs +204 -127
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +48 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +20 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +21 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +39 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +102 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +58 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +97 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +27 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +112 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +20 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +59 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +47 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +13 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +50 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +131 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +93 -22
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +5 -4
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/csrf.mjs +1 -2
- package/dist/middleware/rate-limit.mjs +42 -35
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +4 -4
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +50 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +40 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/server-timing.mjs +1 -2
- package/dist/middleware/setup-gate.mjs +1 -2
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +13 -4
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +32 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +3 -4
- package/dist/notify.mjs +79 -41
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +115 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +77 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +132 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +52 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +64 -15
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +105 -78
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +88 -35
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +17 -16
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-default.mjs +53 -0
- package/dist/pages/build-diff-default.mjs.map +1 -0
- package/dist/pages/build-diff-environment.mjs +81 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +23 -6
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +112 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +74 -12
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +29 -14
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-reference.mjs +84 -0
- package/dist/pages/build-diff-reference.mjs.map +1 -0
- package/dist/pages/build-diff-shortcuts.mjs +6 -3
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-stale.mjs +1 -2
- package/dist/pages/build-diff-usage.mjs +62 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +97 -14
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +37 -22
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +73 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +248 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +167 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +14 -28
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +181 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +52 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +125 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/invite.mjs +1 -2
- package/dist/pages/label-detail.mjs +1 -2
- package/dist/pages/library-grid.mjs +104 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +46 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +129 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +113 -220
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/login.mjs +1 -2
- package/dist/pages/notifications-paused.mjs +21 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/passkey-ceremony.mjs +1 -2
- package/dist/pages/profile.mjs +54 -50
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +3 -4
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +52 -23
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -2
- package/dist/pages/project-settings.mjs +52 -14
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +15 -3
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +7 -4
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +115 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +102 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +101 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/root.mjs +1 -2
- package/dist/pages/settings-ai.mjs +68 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +123 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +98 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +178 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +47 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +47 -23
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +44 -4
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +111 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-members.mjs +1 -2
- package/dist/pages/settings-notifications-git.mjs +127 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +80 -44
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +34 -4
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs +1 -2
- package/dist/pages/settings-webhooks.mjs +1 -2
- package/dist/pages/storybook.mjs +1 -2
- package/dist/pages/target-choices.mjs +38 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +88 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +12 -12
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +47 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +46 -5
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +92 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/admin.mjs +8 -4
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +10 -4
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/attempts.mjs +1 -2
- package/dist/routers/auth-sync.mjs +1 -2
- package/dist/routers/auth.mjs +2 -3
- package/dist/routers/baseline-guard.mjs +4 -5
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baseline-reference.mjs +72 -0
- package/dist/routers/baseline-reference.mjs.map +1 -0
- package/dist/routers/baselines.handlers.mjs +63 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +124 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +67 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +69 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +85 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +73 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +65 -60
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +77 -19
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-cache.mjs +29 -0
- package/dist/routers/capture-cache.mjs.map +1 -0
- package/dist/routers/capture-config.mjs +34 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +42 -17
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/comparison-image.mjs +29 -0
- package/dist/routers/comparison-image.mjs.map +1 -0
- package/dist/routers/component-catalog.handlers.mjs +70 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +120 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +117 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +43 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +113 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/flash.mjs +1 -2
- package/dist/routers/health-report.mjs +3 -5
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +5 -5
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +20 -5
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +1 -2
- package/dist/routers/insights-ui.mjs +63 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +44 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +188 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/labels.mjs +1 -2
- package/dist/routers/links.handlers.mjs +79 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +138 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +30 -7
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +3 -3
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +18 -25
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/notification-subscriptions.mjs +1 -2
- package/dist/routers/oauth-state.mjs +44 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +37 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +50 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +4 -3
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +18 -3
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +85 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +51 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-compression.mjs +109 -0
- package/dist/routers/published-compression.mjs.map +1 -0
- package/dist/routers/published-files.mjs +69 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +71 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-next.mjs +1 -2
- package/dist/routers/review-section.handlers.mjs +84 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +28 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +141 -24
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +21 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +34 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +76 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +102 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +56 -12
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +76 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +28 -3
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +62 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +5 -9
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +16 -12
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +3 -7
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +8 -26
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +108 -28
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +17 -6
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +55 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +62 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +104 -7
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +57 -104
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +3 -7
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +38 -7
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +6 -13
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +12 -6
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +92 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +37 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +21 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +103 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +19 -15
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +4 -0
- package/dist/store.mjs +20 -3
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +45 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/avatar.mjs +1 -2
- package/dist/ui/buttons.mjs +17 -5
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/code-block.mjs +1 -2
- package/dist/ui/compare-stage.mjs +86 -54
- package/dist/ui/compare-stage.mjs.map +1 -1
- package/dist/ui/csrf-field.mjs +1 -2
- package/dist/ui/css.mjs +1 -2
- package/dist/ui/design-reference.mjs +96 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +2 -4
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/dropdown.mjs +1 -2
- package/dist/ui/feedback.mjs +1 -2
- package/dist/ui/filter-input.mjs +1 -2
- package/dist/ui/forms.mjs +7 -7
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/icon.mjs +1 -2
- package/dist/ui/icons/paths.mjs +2 -2
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/icons/sprite.mjs +1 -2
- package/dist/ui/kbd.mjs +1 -2
- package/dist/ui/layout.mjs +1 -2
- package/dist/ui/preview-embed.mjs +81 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/progress.mjs +1 -2
- package/dist/ui/segmented.mjs +1 -2
- package/dist/ui/shell/account.mjs +1 -2
- package/dist/ui/shell/bare.mjs +1 -2
- package/dist/ui/shell/boot-script.mjs +1 -2
- package/dist/ui/shell/client-script.mjs +86 -2
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +13 -10
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +25 -3
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +3 -4
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/dist/ui/shell/topbar.mjs +1 -2
- package/dist/ui/stacks.mjs +1 -2
- package/dist/ui/styles/base.mjs +1 -2
- package/dist/ui/styles/behavior.mjs +1 -2
- package/dist/ui/styles/review-layout.mjs +1 -2
- package/dist/ui/styles/review.mjs +1 -2
- package/dist/ui/styles/tokens.mjs +1 -2
- package/dist/ui/styles.mjs +1 -2
- package/dist/ui/subnav.mjs +1 -2
- package/dist/ui/table.mjs +1 -2
- package/dist/ui/theme.mjs +1 -2
- package/dist/ui/thumbnail.mjs +1 -2
- package/dist/ui/time.mjs +1 -2
- package/package.json +14 -12
- 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
package/dist/routers/media.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "../credential-keys.mjs";
|
|
4
5
|
import { getStore } from "../store.mjs";
|
|
5
6
|
import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
|
|
6
|
-
import {
|
|
7
|
-
|
|
7
|
+
import { readComparisonImage, validFrozenReference } from "./comparison-image.mjs";
|
|
8
|
+
import { BaselineModel, BuildModel, DesignCredentialModel, DesignNodeModel, SnapshotModel, baselineStatus } from "@storyshelf/core/models";
|
|
9
|
+
// src/routers/media.ts
|
|
8
10
|
const VIEW_ROLES = [
|
|
9
11
|
"viewer",
|
|
10
12
|
"developer",
|
|
@@ -12,12 +14,14 @@ const VIEW_ROLES = [
|
|
|
12
14
|
"admin"
|
|
13
15
|
];
|
|
14
16
|
const PNG = "image/png";
|
|
15
|
-
function imageResponse(buffer) {
|
|
17
|
+
function imageResponse(buffer, cacheControl = "private, max-age=3600") {
|
|
16
18
|
return new Response(new Uint8Array(buffer), { headers: {
|
|
17
19
|
"content-type": PNG,
|
|
18
|
-
"cache-control":
|
|
20
|
+
"cache-control": cacheControl
|
|
19
21
|
} });
|
|
20
22
|
}
|
|
23
|
+
/** Design renders are addressed by file version (`?v=`): immutable once the version matches the row. */
|
|
24
|
+
const RENDER_IMMUTABLE = "private, max-age=31536000, immutable";
|
|
21
25
|
/** Register the PNG image, diff, and baseline media endpoints for snapshots. */
|
|
22
26
|
function registerMedia(app) {
|
|
23
27
|
app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/image", async (c) => {
|
|
@@ -31,15 +35,35 @@ function registerMedia(app) {
|
|
|
31
35
|
if (!snapshot.diffPath) notFound("No diff for this snapshot");
|
|
32
36
|
return imageResponse(await getStore().storage.read(snapshot.diffPath));
|
|
33
37
|
});
|
|
38
|
+
app.get("/api/v1/projects/:slug/design/renders/:renderId", async (c) => {
|
|
39
|
+
const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
|
|
40
|
+
const { db, config, storage, design } = getStore();
|
|
41
|
+
const credentials = new DesignCredentialModel(db, credentialKeys(config));
|
|
42
|
+
const nodes = new DesignNodeModel(db, credentials, design, { storage });
|
|
43
|
+
const found = await nodes.get(project.id, c.req.param("renderId"));
|
|
44
|
+
const node = found && (found.viewerId === "" || found.viewerId === getStore().user?.id) ? found : null;
|
|
45
|
+
const png = node ? await nodes.readRender(node) : null;
|
|
46
|
+
if (!png) notFound("No design render");
|
|
47
|
+
return imageResponse(png, node?.renderVersion && c.req.query("v") === node.renderVersion ? RENDER_IMMUTABLE : void 0);
|
|
48
|
+
});
|
|
34
49
|
app.get("/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/baseline", async (c) => {
|
|
35
50
|
const project = await resolveAuthorizedProject(c, c.req.param("slug"), ...VIEW_ROLES);
|
|
36
51
|
const snapshot = await findSnapshot(c.req.param("buildId"), c.req.param("snapshotId"), project.id);
|
|
37
52
|
const build = await new BuildModel(getStore().db).get(snapshot.buildId);
|
|
38
53
|
if (!build) notFound("Build not found");
|
|
54
|
+
if (c.req.query("reference") !== "default" && (snapshot.comparisonImagePath || snapshot.comparisonMetadata)) {
|
|
55
|
+
const frozen = validFrozenReference(snapshot, project.id, build.id);
|
|
56
|
+
if (!frozen) notFound("Original baseline is unavailable");
|
|
57
|
+
const image = await readComparisonImage(frozen.path, frozen.reference.imageSha256);
|
|
58
|
+
if (!image) notFound("Original baseline image is unavailable");
|
|
59
|
+
return imageResponse(image, "private, no-store");
|
|
60
|
+
}
|
|
39
61
|
const baselines = new BaselineModel(getStore().db, void 0, getStore().storage);
|
|
40
|
-
const baseline = await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch);
|
|
62
|
+
const baseline = c.req.query("reference") === "default" ? await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, project.gitDefaultBranch, snapshot.browser) : await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser);
|
|
63
|
+
if (c.req.query("reference") === "default" && (baseline?.id !== c.req.query("id") || baseline?.updatedAt !== c.req.query("version"))) notFound("Default baseline changed; reload the comparison");
|
|
41
64
|
if (!baseline) notFound("No baseline for this snapshot");
|
|
42
|
-
|
|
65
|
+
if (c.req.query("reference") !== "default" && snapshot.baselineVersion !== null && baselineStatus(snapshot, baseline) !== "current") notFound("Original baseline is no longer available");
|
|
66
|
+
return imageResponse(await baselines.read(baseline), "private, no-store");
|
|
43
67
|
});
|
|
44
68
|
}
|
|
45
69
|
async function findSnapshot(buildId, snapshotId, projectId) {
|
|
@@ -47,7 +71,6 @@ async function findSnapshot(buildId, snapshotId, projectId) {
|
|
|
47
71
|
if (!snapshot || snapshot.buildId !== buildId || snapshot.projectId !== projectId) notFound("Snapshot not found");
|
|
48
72
|
return snapshot;
|
|
49
73
|
}
|
|
50
|
-
//#endregion
|
|
51
74
|
export { registerMedia };
|
|
52
75
|
|
|
53
76
|
//# sourceMappingURL=media.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.mjs","names":[],"sources":["../../src/routers/media.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"media.mjs","names":[],"sources":["../../src/routers/media.ts"],"sourcesContent":["import {\n BaselineModel,\n DesignCredentialModel,\n DesignNodeModel,\n baselineStatus,\n} from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { readComparisonImage, validFrozenReference } from \"./comparison-image.ts\";\nimport { notFound, resolveAuthorizedProject } from \"./helpers.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n\nconst PNG = \"image/png\";\n\nfunction imageResponse(buffer: Buffer, cacheControl = \"private, max-age=3600\"): Response {\n return new Response(new Uint8Array(buffer), {\n headers: { \"content-type\": PNG, \"cache-control\": cacheControl },\n });\n}\n\n/** Design renders are addressed by file version (`?v=`): immutable once the version matches the row. */\nconst RENDER_IMMUTABLE = \"private, max-age=31536000, immutable\";\n\n/** Register the PNG image, diff, and baseline media endpoints for snapshots. */\nexport function registerMedia(app: ShelfRouter): void {\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/image\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n return imageResponse(await getStore().storage.read(snapshot.screenshotPath));\n });\n\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/diff\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n if (!snapshot.diffPath) {\n notFound(\"No diff for this snapshot\");\n }\n return imageResponse(await getStore().storage.read(snapshot.diffPath));\n });\n\n // `:renderId` is the internal design_nodes row id (a ULID), not a provider node id.\n app.get(\"/api/v1/projects/:slug/design/renders/:renderId\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const { db, config, storage, design } = getStore();\n const credentials = new DesignCredentialModel(db, credentialKeys(config));\n const nodes = new DesignNodeModel(db, credentials, design, { storage });\n const found = await nodes.get(project.id, c.req.param(\"renderId\"));\n // A render fetched with a member's own connection is theirs alone (it may show files others cannot open).\n const node =\n found && (found.viewerId === \"\" || found.viewerId === getStore().user?.id) ? found : null;\n const png = node ? await nodes.readRender(node) : null;\n if (!png) {\n notFound(\"No design render\");\n }\n const versioned = node?.renderVersion && c.req.query(\"v\") === node.renderVersion;\n return imageResponse(png, versioned ? RENDER_IMMUTABLE : undefined);\n });\n\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/baseline\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n const build = await new BuildModel(getStore().db).get(snapshot.buildId);\n if (!build) {\n notFound(\"Build not found\");\n }\n if (\n c.req.query(\"reference\") !== \"default\" &&\n (snapshot.comparisonImagePath || snapshot.comparisonMetadata)\n ) {\n const frozen = validFrozenReference(snapshot, project.id, build.id);\n if (!frozen) {\n notFound(\"Original baseline is unavailable\");\n }\n const image = await readComparisonImage(frozen.path, frozen.reference.imageSha256);\n if (!image) {\n notFound(\"Original baseline image is unavailable\");\n }\n return imageResponse(image, \"private, no-store\");\n }\n const baselines = new BaselineModel(getStore().db, undefined, getStore().storage);\n const baseline =\n c.req.query(\"reference\") === \"default\"\n ? await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n project.gitDefaultBranch,\n snapshot.browser,\n )\n : await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n if (\n c.req.query(\"reference\") === \"default\" &&\n (baseline?.id !== c.req.query(\"id\") || baseline?.updatedAt !== c.req.query(\"version\"))\n ) {\n notFound(\"Default baseline changed; reload the comparison\");\n }\n if (!baseline) {\n notFound(\"No baseline for this snapshot\");\n }\n if (\n c.req.query(\"reference\") !== \"default\" &&\n snapshot.baselineVersion !== null &&\n baselineStatus(snapshot, baseline) !== \"current\"\n ) {\n notFound(\"Original baseline is no longer available\");\n }\n return imageResponse(await baselines.read(baseline), \"private, no-store\");\n });\n}\n\nasync function findSnapshot(\n buildId: string,\n snapshotId: string,\n projectId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== buildId || snapshot.projectId !== projectId) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n"],"mappings":"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"}
|
package/dist/routers/members.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { groupMappingCreateSchema, groupMappingSchema, memberRoleSchema, memberS
|
|
|
7
7
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
8
|
import { MemberModel, ProjectGroupMappingModel } from "@storyshelf/core/models";
|
|
9
9
|
import { HTTPException } from "hono/http-exception";
|
|
10
|
-
|
|
10
|
+
// src/routers/members.ts
|
|
11
11
|
const VIEW_ROLES = [
|
|
12
12
|
"viewer",
|
|
13
13
|
"developer",
|
|
@@ -161,7 +161,8 @@ function registerGroupMappingRoutes(app) {
|
|
|
161
161
|
app.openapi(createGroupMappingRoute, async (c) => {
|
|
162
162
|
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
163
163
|
const body = c.req.valid("json");
|
|
164
|
-
|
|
164
|
+
const inputError = ProjectGroupMappingModel.inputError(body.groupName, body.role);
|
|
165
|
+
if (inputError) throw new HTTPException(400, { message: inputError });
|
|
165
166
|
return c.json(await new ProjectGroupMappingModel(getStore().db).create(project.id, body.groupName, body.role), 201);
|
|
166
167
|
});
|
|
167
168
|
app.openapi(deleteGroupMappingRoute, async (c) => {
|
|
@@ -171,7 +172,6 @@ function registerGroupMappingRoutes(app) {
|
|
|
171
172
|
return c.body(null, 204);
|
|
172
173
|
});
|
|
173
174
|
}
|
|
174
|
-
//#endregion
|
|
175
175
|
export { registerMembers };
|
|
176
176
|
|
|
177
177
|
//# sourceMappingURL=members.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"members.mjs","names":[],"sources":["../../src/routers/members.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n groupMappingCreateSchema,\n groupMappingSchema,\n memberRoleSchema,\n memberSchema,\n memberSetSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listMembersRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List members\",\n path: \"/api/v1/projects/{slug}/members\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema.array() } },\n description: \"List project members\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst setMemberRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Add or update a member\",\n path: \"/api/v1/projects/{slug}/members\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: memberSetSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Added or updated member\",\n },\n ...notFound,\n },\n});\n\nconst updateMemberRoute = createRoute({\n method: \"patch\",\n tags: [\"Members\"],\n summary: \"Change a member's role\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: {\n params: z.object({ slug: z.string(), userId: z.string() }),\n body: { content: { \"application/json\": { schema: memberRoleSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Updated member role\",\n },\n ...notFound,\n },\n});\n\nconst deleteMemberRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a member\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: { params: z.object({ slug: z.string(), userId: z.string() }) },\n responses: {\n 204: { description: \"Removed member\" },\n ...notFound,\n },\n});\n\nconst listGroupMappingsRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List group mappings\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: groupMappingSchema.array() } },\n description: \"List identity-provider group mappings\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createGroupMappingRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Create a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: groupMappingCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: groupMappingSchema } },\n description: \"Created group mapping (exact group-name match)\",\n },\n ...notFound,\n },\n});\n\nconst deleteGroupMappingRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings/{mappingId}\",\n request: { params: z.object({ slug: z.string(), mappingId: z.string() }) },\n responses: {\n 204: { description: \"Removed group mapping\" },\n ...notFound,\n },\n});\n\n/** Register the project member list, upsert, and removal endpoints. */\nexport function registerMembers(app: ShelfRouter): void {\n app.openapi(listMembersRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new MemberModel(getStore().db).list(project.id));\n });\n\n app.openapi(setMemberRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(\n await new MemberModel(getStore().db).set(project.id, body.userId, body.role),\n 201,\n );\n });\n\n app.openapi(updateMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new MemberModel(getStore().db).set(project.id, userId, body.role));\n });\n\n app.openapi(deleteMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new MemberModel(getStore().db).remove(project.id, userId);\n return c.body(null, 204);\n });\n\n registerGroupMappingRoutes(app);\n}\n\n/** Register identity-provider group mapping endpoints. */\nfunction registerGroupMappingRoutes(app: ShelfRouter): void {\n app.openapi(listGroupMappingsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new ProjectGroupMappingModel(getStore().db).list(project.id));\n });\n\n app.openapi(createGroupMappingRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n if (body.groupName.includes(\"*\")) {\n throw new HTTPException(400, {\n message:\n \"Group names match exactly; wildcards are not expanded. A broad pattern can silently grant org-wide access.\",\n });\n }\n return c.json(\n await new ProjectGroupMappingModel(getStore().db).create(\n project.id,\n body.groupName,\n body.role,\n ),\n 201,\n );\n });\n\n app.openapi(deleteGroupMappingRoute, async (c) => {\n const { slug, mappingId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new ProjectGroupMappingModel(getStore().db).remove(project.id, mappingId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"members.mjs","names":[],"sources":["../../src/routers/members.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { MemberModel, ProjectGroupMappingModel } from \"@storyshelf/core/models\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n groupMappingCreateSchema,\n groupMappingSchema,\n memberRoleSchema,\n memberSchema,\n memberSetSchema,\n notFound,\n unauthorized,\n} from \"./schemas.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listMembersRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List members\",\n path: \"/api/v1/projects/{slug}/members\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema.array() } },\n description: \"List project members\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst setMemberRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Add or update a member\",\n path: \"/api/v1/projects/{slug}/members\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: memberSetSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Added or updated member\",\n },\n ...notFound,\n },\n});\n\nconst updateMemberRoute = createRoute({\n method: \"patch\",\n tags: [\"Members\"],\n summary: \"Change a member's role\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: {\n params: z.object({ slug: z.string(), userId: z.string() }),\n body: { content: { \"application/json\": { schema: memberRoleSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: memberSchema } },\n description: \"Updated member role\",\n },\n ...notFound,\n },\n});\n\nconst deleteMemberRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a member\",\n path: \"/api/v1/projects/{slug}/members/{userId}\",\n request: { params: z.object({ slug: z.string(), userId: z.string() }) },\n responses: {\n 204: { description: \"Removed member\" },\n ...notFound,\n },\n});\n\nconst listGroupMappingsRoute = createRoute({\n method: \"get\",\n tags: [\"Members\"],\n summary: \"List group mappings\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: groupMappingSchema.array() } },\n description: \"List identity-provider group mappings\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createGroupMappingRoute = createRoute({\n method: \"post\",\n tags: [\"Members\"],\n summary: \"Create a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: groupMappingCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: groupMappingSchema } },\n description: \"Created group mapping (exact group-name match)\",\n },\n ...notFound,\n },\n});\n\nconst deleteGroupMappingRoute = createRoute({\n method: \"delete\",\n tags: [\"Members\"],\n summary: \"Remove a group mapping\",\n path: \"/api/v1/projects/{slug}/group-mappings/{mappingId}\",\n request: { params: z.object({ slug: z.string(), mappingId: z.string() }) },\n responses: {\n 204: { description: \"Removed group mapping\" },\n ...notFound,\n },\n});\n\n/** Register the project member list, upsert, and removal endpoints. */\nexport function registerMembers(app: ShelfRouter): void {\n app.openapi(listMembersRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new MemberModel(getStore().db).list(project.id));\n });\n\n app.openapi(setMemberRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(\n await new MemberModel(getStore().db).set(project.id, body.userId, body.role),\n 201,\n );\n });\n\n app.openapi(updateMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new MemberModel(getStore().db).set(project.id, userId, body.role));\n });\n\n app.openapi(deleteMemberRoute, async (c) => {\n const { slug, userId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new MemberModel(getStore().db).remove(project.id, userId);\n return c.body(null, 204);\n });\n\n registerGroupMappingRoutes(app);\n}\n\n/** Register identity-provider group mapping endpoints. */\nfunction registerGroupMappingRoutes(app: ShelfRouter): void {\n app.openapi(listGroupMappingsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new ProjectGroupMappingModel(getStore().db).list(project.id));\n });\n\n app.openapi(createGroupMappingRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const inputError = ProjectGroupMappingModel.inputError(body.groupName, body.role);\n if (inputError) {\n throw new HTTPException(400, { message: inputError });\n }\n return c.json(\n await new ProjectGroupMappingModel(getStore().db).create(\n project.id,\n body.groupName,\n body.role,\n ),\n 201,\n );\n });\n\n app.openapi(deleteGroupMappingRoute, async (c) => {\n const { slug, mappingId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new ProjectGroupMappingModel(getStore().db).remove(project.id, mappingId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
4
|
import { getStore } from "../store.mjs";
|
|
6
5
|
import { notFound, requireSiteAdmin, resolveAuthorizedProject } from "./helpers.mjs";
|
|
7
6
|
import { notFound as notFound$1, notificationChannelCreateSchema, notificationChannelSchema, unauthorized } from "./schemas.mjs";
|
|
7
|
+
import { notificationChannelModel } from "./scoped-models.mjs";
|
|
8
8
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
9
|
import { NotificationChannelModel } from "@storyshelf/core/models";
|
|
10
|
-
|
|
10
|
+
// src/routers/notification-channels.ts
|
|
11
11
|
const ADMIN_ROLES = ["admin"];
|
|
12
12
|
/** Public channel view (secret redacted, config/events decoded). */
|
|
13
13
|
function toPublic(row) {
|
|
@@ -19,24 +19,12 @@ function toPublic(row) {
|
|
|
19
19
|
hasSecret: row.secretEncrypted !== null,
|
|
20
20
|
events: NotificationChannelModel.eventsOf(row),
|
|
21
21
|
enabled: row.enabled,
|
|
22
|
+
lastError: row.lastError,
|
|
23
|
+
lastErrorAt: row.lastErrorAt,
|
|
22
24
|
createdAt: row.createdAt,
|
|
23
25
|
updatedAt: row.updatedAt
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
|
-
/** Validate channel input against the provider descriptor. */
|
|
27
|
-
function validateChannelInput(provider, config) {
|
|
28
|
-
const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);
|
|
29
|
-
if (!descriptor) return {
|
|
30
|
-
ok: false,
|
|
31
|
-
message: `Unknown provider: ${provider}`
|
|
32
|
-
};
|
|
33
|
-
const parsed = descriptor.metadata.schema.safeParse(config);
|
|
34
|
-
if (!parsed.success) return {
|
|
35
|
-
ok: false,
|
|
36
|
-
message: parsed.error.message
|
|
37
|
-
};
|
|
38
|
-
return { ok: true };
|
|
39
|
-
}
|
|
40
28
|
const listChannelsRoute = createRoute({
|
|
41
29
|
method: "get",
|
|
42
30
|
tags: ["Notifications"],
|
|
@@ -91,15 +79,18 @@ const deleteChannelRoute = createRoute({
|
|
|
91
79
|
function registerNotificationChannels(app) {
|
|
92
80
|
app.openapi(listChannelsRoute, async (c) => {
|
|
93
81
|
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
94
|
-
const rows = await
|
|
82
|
+
const rows = await notificationChannelModel().list(project.id);
|
|
95
83
|
return c.json(rows.map((row) => toPublic(row)));
|
|
96
84
|
});
|
|
97
85
|
app.openapi(createChannelRoute, async (c) => {
|
|
98
86
|
const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...ADMIN_ROLES);
|
|
99
87
|
const body = c.req.valid("json");
|
|
100
|
-
const valid =
|
|
88
|
+
const valid = NotificationChannelModel.validateInput(getStore().notifiers, body.provider, body.config, {
|
|
89
|
+
projectId: project.id,
|
|
90
|
+
hasSecret: body.secret !== void 0
|
|
91
|
+
});
|
|
101
92
|
if (!valid.ok) return c.json({ message: valid.message }, 400);
|
|
102
|
-
const row = await
|
|
93
|
+
const row = await notificationChannelModel().create({
|
|
103
94
|
projectId: project.id,
|
|
104
95
|
...body
|
|
105
96
|
});
|
|
@@ -108,7 +99,7 @@ function registerNotificationChannels(app) {
|
|
|
108
99
|
app.openapi(deleteChannelRoute, async (c) => {
|
|
109
100
|
const { slug, channelId } = c.req.valid("param");
|
|
110
101
|
const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
|
|
111
|
-
const model =
|
|
102
|
+
const model = notificationChannelModel();
|
|
112
103
|
if ((await model.get(channelId))?.projectId !== project.id) notFound("Notification channel not found");
|
|
113
104
|
await model.remove(channelId);
|
|
114
105
|
return c.body(null, 204);
|
|
@@ -161,15 +152,18 @@ const deleteSystemRoute = createRoute({
|
|
|
161
152
|
function registerSystemChannels(app) {
|
|
162
153
|
app.openapi(listSystemRoute, async (c) => {
|
|
163
154
|
requireSiteAdmin(c);
|
|
164
|
-
const rows = await
|
|
155
|
+
const rows = await notificationChannelModel().listSystem();
|
|
165
156
|
return c.json(rows.map((row) => toPublic(row)));
|
|
166
157
|
});
|
|
167
158
|
app.openapi(createSystemRoute, async (c) => {
|
|
168
159
|
requireSiteAdmin(c);
|
|
169
160
|
const body = c.req.valid("json");
|
|
170
|
-
const valid =
|
|
161
|
+
const valid = NotificationChannelModel.validateInput(getStore().notifiers, body.provider, body.config, {
|
|
162
|
+
projectId: null,
|
|
163
|
+
hasSecret: body.secret !== void 0
|
|
164
|
+
});
|
|
171
165
|
if (!valid.ok) return c.json({ message: valid.message }, 400);
|
|
172
|
-
const row = await
|
|
166
|
+
const row = await notificationChannelModel().create({
|
|
173
167
|
projectId: null,
|
|
174
168
|
...body
|
|
175
169
|
});
|
|
@@ -178,13 +172,12 @@ function registerSystemChannels(app) {
|
|
|
178
172
|
app.openapi(deleteSystemRoute, async (c) => {
|
|
179
173
|
requireSiteAdmin(c);
|
|
180
174
|
const { channelId } = c.req.valid("param");
|
|
181
|
-
const model =
|
|
175
|
+
const model = notificationChannelModel();
|
|
182
176
|
if ((await model.get(channelId))?.projectId !== null) notFound("Notification channel not found");
|
|
183
177
|
await model.remove(channelId);
|
|
184
178
|
return c.body(null, 204);
|
|
185
179
|
});
|
|
186
180
|
}
|
|
187
|
-
//#endregion
|
|
188
181
|
export { registerNotificationChannels, registerSystemChannels };
|
|
189
182
|
|
|
190
183
|
//# sourceMappingURL=notification-channels.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notification-channels.mjs","names":["notFoundResponse"],"sources":["../../src/routers/notification-channels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport type { NotificationChannelRow } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { notFound, requireSiteAdmin, resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n notificationChannelCreateSchema,\n notificationChannelSchema,\n unauthorized,\n} from \"./schemas.ts\";\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\n/** Public channel view (secret redacted, config/events decoded). */\nfunction toPublic(row: NotificationChannelRow): {\n id: string;\n projectId: string | null;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n createdAt: string;\n updatedAt: string;\n} {\n return {\n id: row.id,\n projectId: row.projectId,\n provider: row.provider,\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\n/** Validate channel input against the provider descriptor. */\nfunction validateChannelInput(\n provider: string,\n config: unknown,\n): { ok: true } | { ok: false; message: string } {\n const descriptor = getStore().notifiers.find((candidate) => candidate.metadata.kind === provider);\n if (!descriptor) {\n return { ok: false, message: `Unknown provider: ${provider}` };\n }\n const parsed = descriptor.metadata.schema.safeParse(config);\n if (!parsed.success) {\n return { ok: false, message: parsed.error.message };\n }\n return { ok: true };\n}\n\nconst listChannelsRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List notification channels\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List notification channels\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createChannelRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteChannelRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels/{channelId}\",\n request: { params: z.object({ slug: z.string(), channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project notification-channel list, create, and delete endpoints. */\nexport function registerNotificationChannels(app: ShelfRouter): void {\n app.openapi(listChannelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const rows = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createChannelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const valid = validateChannelInput(body.provider, body.config);\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({ projectId: project.id, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteChannelRoute, async (c) => {\n const { slug, channelId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const existing = await model.get(channelId);\n if (existing?.projectId !== project.id) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n\nconst listSystemRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List site-wide channels\",\n path: \"/api/v1/admin/notification-channels\",\n request: {},\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List site-wide notification channels\",\n },\n ...unauthorized,\n },\n});\n\nconst createSystemRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a site-wide channel\",\n path: \"/api/v1/admin/notification-channels\",\n request: {\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created site-wide notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...unauthorized,\n },\n});\n\nconst deleteSystemRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a site-wide channel\",\n path: \"/api/v1/admin/notification-channels/{channelId}\",\n request: { params: z.object({ channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted site-wide notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the site-admin (project-less, `sys:*`) channel endpoints. */\nexport function registerSystemChannels(app: ShelfRouter): void {\n app.openapi(listSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const rows = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).listSystem();\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const body = c.req.valid(\"json\");\n const valid = validateChannelInput(body.provider, body.config);\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n ).create({ projectId: null, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const { channelId } = c.req.valid(\"param\");\n const model = new NotificationChannelModel(\n getStore().db,\n undefined,\n credentialKeys(getStore().config),\n );\n const existing = await model.get(channelId);\n if (existing?.projectId !== null) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n"],"mappings":";;;;;;;;;;AAcA,MAAM,cAAsC,CAAC,OAAO;;AAGpD,SAAS,SAAS,KAUhB;CACA,OAAO;EACL,IAAI,IAAI;EACR,WAAW,IAAI;EACf,UAAU,IAAI;EACd,QAAQ,yBAAyB,SAAS,GAAG;EAC7C,WAAW,IAAI,oBAAoB;EACnC,QAAQ,yBAAyB,SAAS,GAAG;EAC7C,SAAS,IAAI;EACb,WAAW,IAAI;EACf,WAAW,IAAI;CACjB;AACF;;AAGA,SAAS,qBACP,UACA,QAC+C;CAC/C,MAAM,aAAa,SAAS,CAAC,CAAC,UAAU,MAAM,cAAc,UAAU,SAAS,SAAS,QAAQ;CAChG,IAAI,CAAC,YACH,OAAO;EAAE,IAAI;EAAO,SAAS,qBAAqB;CAAW;CAE/D,MAAM,SAAS,WAAW,SAAS,OAAO,UAAU,MAAM;CAC1D,IAAI,CAAC,OAAO,SACV,OAAO;EAAE,IAAI;EAAO,SAAS,OAAO,MAAM;CAAQ;CAEpD,OAAO,EAAE,IAAI,KAAK;AACpB;AAEA,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM,CAAC,eAAe;CACtB,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,0BAA0B,MAAM,EAAE,EAAE;GAC7E,aAAa;EACf;EACA,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM,CAAC,eAAe;CACtB,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gCAAgC,EAAE,EAAE;CACvF;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,0BAA0B,EAAE;GACrE,aAAa;EACf;EACA,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;GAC7E,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM,CAAC,eAAe;CACtB,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CACzE,WAAW;EACT,KAAK,EAAE,aAAa,+BAA+B;EACnD,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,6BAA6B,KAAwB;CACnE,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,MAAM,IAAI,yBACrB,SAAS,CAAC,CAAC,IACX,KAAA,GACA,eAAe,SAAS,CAAC,CAAC,MAAM,CAClC,CAAC,CAAC,KAAK,QAAQ,EAAE;EACjB,OAAO,EAAE,KAAK,KAAK,KAAK,QAAQ,SAAS,GAAG,CAAC,CAAC;CAChD,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,QAAQ,qBAAqB,KAAK,UAAU,KAAK,MAAM;EAC7D,IAAI,CAAC,MAAM,IACT,OAAO,EAAE,KAAK,EAAE,SAAS,MAAM,QAAQ,GAAG,GAAG;EAE/C,MAAM,MAAM,MAAM,IAAI,yBACpB,SAAS,CAAC,CAAC,IACX,KAAA,GACA,eAAe,SAAS,CAAC,CAAC,MAAM,CAClC,CAAC,CAAC,OAAO;GAAE,WAAW,QAAQ;GAAI,GAAG;EAAK,CAAC;EAC3C,OAAO,EAAE,KAAK,SAAS,GAAG,GAAG,GAAG;CAClC,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,EAAE,MAAM,cAAc,EAAE,IAAI,MAAM,OAAO;EAC/C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,QAAQ,IAAI,yBAChB,SAAS,CAAC,CAAC,IACX,KAAA,GACA,eAAe,SAAS,CAAC,CAAC,MAAM,CAClC;EAEA,KAAI,MADmB,MAAM,IAAI,SAAS,EAAA,EAC5B,cAAc,QAAQ,IAClC,SAAS,gCAAgC;EAE3C,MAAM,MAAM,OAAO,SAAS;EAC5B,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH;AAEA,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM,CAAC,eAAe;CACtB,SAAS;CACT,MAAM;CACN,SAAS,CAAC;CACV,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,0BAA0B,MAAM,EAAE,EAAE;GAC7E,aAAa;EACf;EACA,GAAG;CACL;AACF,CAAC;AAED,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM,CAAC,eAAe;CACtB,SAAS;CACT,MAAM;CACN,SAAS,EACP,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gCAAgC,EAAE,EAAE,EACvF;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,0BAA0B,EAAE;GACrE,aAAa;EACf;EACA,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;GAC7E,aAAa;EACf;EACA,GAAG;CACL;AACF,CAAC;AAED,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM,CAAC,eAAe;CACtB,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE;CACvD,WAAW;EACT,KAAK,EAAE,aAAa,yCAAyC;EAC7D,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,uBAAuB,KAAwB;CAC7D,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,iBAAiB,CAAC;EAClB,MAAM,OAAO,MAAM,IAAI,yBACrB,SAAS,CAAC,CAAC,IACX,KAAA,GACA,eAAe,SAAS,CAAC,CAAC,MAAM,CAClC,CAAC,CAAC,WAAW;EACb,OAAO,EAAE,KAAK,KAAK,KAAK,QAAQ,SAAS,GAAG,CAAC,CAAC;CAChD,CAAC;CAED,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,iBAAiB,CAAC;EAClB,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,MAAM,QAAQ,qBAAqB,KAAK,UAAU,KAAK,MAAM;EAC7D,IAAI,CAAC,MAAM,IACT,OAAO,EAAE,KAAK,EAAE,SAAS,MAAM,QAAQ,GAAG,GAAG;EAE/C,MAAM,MAAM,MAAM,IAAI,yBACpB,SAAS,CAAC,CAAC,IACX,KAAA,GACA,eAAe,SAAS,CAAC,CAAC,MAAM,CAClC,CAAC,CAAC,OAAO;GAAE,WAAW;GAAM,GAAG;EAAK,CAAC;EACrC,OAAO,EAAE,KAAK,SAAS,GAAG,GAAG,GAAG;CAClC,CAAC;CAED,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,iBAAiB,CAAC;EAClB,MAAM,EAAE,cAAc,EAAE,IAAI,MAAM,OAAO;EACzC,MAAM,QAAQ,IAAI,yBAChB,SAAS,CAAC,CAAC,IACX,KAAA,GACA,eAAe,SAAS,CAAC,CAAC,MAAM,CAClC;EAEA,KAAI,MADmB,MAAM,IAAI,SAAS,EAAA,EAC5B,cAAc,MAC1B,SAAS,gCAAgC;EAE3C,MAAM,MAAM,OAAO,SAAS;EAC5B,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"notification-channels.mjs","names":["notFoundResponse"],"sources":["../../src/routers/notification-channels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { NotificationChannelModel } from \"@storyshelf/core/models\";\nimport type { NotificationChannelRow } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { notFound, requireSiteAdmin, resolveAuthorizedProject } from \"./helpers.ts\";\nimport {\n notFound as notFoundResponse,\n notificationChannelCreateSchema,\n notificationChannelSchema,\n unauthorized,\n} from \"./schemas.ts\";\nimport { notificationChannelModel } from \"./scoped-models.ts\";\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\n/** Public channel view (secret redacted, config/events decoded). */\nfunction toPublic(row: NotificationChannelRow): {\n id: string;\n projectId: string | null;\n provider: string;\n config: Record<string, unknown>;\n hasSecret: boolean;\n events: string[];\n enabled: boolean;\n lastError: string | null;\n lastErrorAt: string | null;\n createdAt: string;\n updatedAt: string;\n} {\n return {\n id: row.id,\n projectId: row.projectId,\n provider: row.provider,\n config: NotificationChannelModel.configOf(row),\n hasSecret: row.secretEncrypted !== null,\n events: NotificationChannelModel.eventsOf(row),\n enabled: row.enabled,\n lastError: row.lastError,\n lastErrorAt: row.lastErrorAt,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n };\n}\n\nconst listChannelsRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List notification channels\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List notification channels\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createChannelRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteChannelRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a notification channel\",\n path: \"/api/v1/projects/{slug}/notification-channels/{channelId}\",\n request: { params: z.object({ slug: z.string(), channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the project notification-channel list, create, and delete endpoints. */\nexport function registerNotificationChannels(app: ShelfRouter): void {\n app.openapi(listChannelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const rows = await notificationChannelModel().list(project.id);\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createChannelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n const valid = NotificationChannelModel.validateInput(\n getStore().notifiers,\n body.provider,\n body.config,\n { projectId: project.id, hasSecret: body.secret !== undefined },\n );\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await notificationChannelModel().create({ projectId: project.id, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteChannelRoute, async (c) => {\n const { slug, channelId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const model = notificationChannelModel();\n const existing = await model.get(channelId);\n if (existing?.projectId !== project.id) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n\nconst listSystemRoute = createRoute({\n method: \"get\",\n tags: [\"Notifications\"],\n summary: \"List site-wide channels\",\n path: \"/api/v1/admin/notification-channels\",\n request: {},\n responses: {\n 200: {\n content: { \"application/json\": { schema: notificationChannelSchema.array() } },\n description: \"List site-wide notification channels\",\n },\n ...unauthorized,\n },\n});\n\nconst createSystemRoute = createRoute({\n method: \"post\",\n tags: [\"Notifications\"],\n summary: \"Create a site-wide channel\",\n path: \"/api/v1/admin/notification-channels\",\n request: {\n body: { content: { \"application/json\": { schema: notificationChannelCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: notificationChannelSchema } },\n description: \"Created site-wide notification channel\",\n },\n 400: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"Bad request\",\n },\n ...unauthorized,\n },\n});\n\nconst deleteSystemRoute = createRoute({\n method: \"delete\",\n tags: [\"Notifications\"],\n summary: \"Delete a site-wide channel\",\n path: \"/api/v1/admin/notification-channels/{channelId}\",\n request: { params: z.object({ channelId: z.string() }) },\n responses: {\n 204: { description: \"Deleted site-wide notification channel\" },\n ...notFoundResponse,\n },\n});\n\n/** Register the site-admin (project-less, `sys:*`) channel endpoints. */\nexport function registerSystemChannels(app: ShelfRouter): void {\n app.openapi(listSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const rows = await notificationChannelModel().listSystem();\n return c.json(rows.map((row) => toPublic(row)));\n });\n\n app.openapi(createSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const body = c.req.valid(\"json\");\n const valid = NotificationChannelModel.validateInput(\n getStore().notifiers,\n body.provider,\n body.config,\n { projectId: null, hasSecret: body.secret !== undefined },\n );\n if (!valid.ok) {\n return c.json({ message: valid.message }, 400);\n }\n const row = await notificationChannelModel().create({ projectId: null, ...body });\n return c.json(toPublic(row), 201);\n });\n\n app.openapi(deleteSystemRoute, async (c) => {\n requireSiteAdmin(c);\n const { channelId } = c.req.valid(\"param\");\n const model = notificationChannelModel();\n const existing = await model.get(channelId);\n if (existing?.projectId !== null) {\n notFound(\"Notification channel not found\");\n }\n await model.remove(channelId);\n return c.body(null, 204);\n });\n}\n"],"mappings":"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"}
|
|
@@ -6,7 +6,7 @@ import { resolveAuthorizedProject, unauthorized } from "./helpers.mjs";
|
|
|
6
6
|
import { notFound, notificationSubscriptionPutSchema, notificationSubscriptionSchema, unauthorized as unauthorized$1 } from "./schemas.mjs";
|
|
7
7
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
8
|
import { NotificationSubscriptionModel } from "@storyshelf/core/models";
|
|
9
|
-
|
|
9
|
+
// src/routers/notification-subscriptions.ts
|
|
10
10
|
const VIEW_ROLES = [
|
|
11
11
|
"viewer",
|
|
12
12
|
"developer",
|
|
@@ -114,7 +114,6 @@ function registerNotificationSubscriptions(app) {
|
|
|
114
114
|
return c.json(rows.map((row) => toPublic(row)));
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
-
//#endregion
|
|
118
117
|
export { registerNotificationSubscriptions };
|
|
119
118
|
|
|
120
119
|
//# sourceMappingURL=notification-subscriptions.mjs.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { timingSafeEqualString } from "@storyshelf/core/utils";
|
|
5
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
6
|
+
// src/routers/oauth-state.ts
|
|
7
|
+
/** Signed, expiring OAuth `state` for "connect your account" flows (binds user, service and browser). */
|
|
8
|
+
/** State lifetime: long enough to sign in at the service, short enough to limit replay. */
|
|
9
|
+
const STATE_TTL_MS = 6e5;
|
|
10
|
+
/** Issue a signed state for a new flow, with a fresh nonce. */
|
|
11
|
+
function signState(secret, fields, now = Date.now()) {
|
|
12
|
+
const nonce = randomBytes(16).toString("hex");
|
|
13
|
+
const body = {
|
|
14
|
+
...fields,
|
|
15
|
+
nonce,
|
|
16
|
+
expiresAt: now + STATE_TTL_MS
|
|
17
|
+
};
|
|
18
|
+
const payload = Buffer.from(JSON.stringify(body)).toString("base64url");
|
|
19
|
+
return {
|
|
20
|
+
state: `${payload}.${mac(secret, payload)}`,
|
|
21
|
+
nonce
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Parse and verify a state; null when forged, malformed or expired. */
|
|
25
|
+
function verifyState(secret, state, now = Date.now()) {
|
|
26
|
+
const [payload, signature] = state.split(".");
|
|
27
|
+
if (!payload || !signature || !timingSafeEqualString(signature, mac(secret, payload))) return null;
|
|
28
|
+
try {
|
|
29
|
+
const body = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
30
|
+
return body.expiresAt > now ? body : null;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** A same-site path (never an absolute or protocol-relative URL), else the fallback. */
|
|
36
|
+
function safeReturnPath(value, fallback) {
|
|
37
|
+
return value && /^\/(?![/\\])/u.test(value) ? value : fallback;
|
|
38
|
+
}
|
|
39
|
+
function mac(secret, payload) {
|
|
40
|
+
return createHmac("sha256", secret).update(`oauth-connect:${payload}`).digest("base64url");
|
|
41
|
+
}
|
|
42
|
+
export { safeReturnPath, signState, verifyState };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=oauth-state.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-state.mjs","names":[],"sources":["../../src/routers/oauth-state.ts"],"sourcesContent":["/** Signed, expiring OAuth `state` for \"connect your account\" flows (binds user, service and browser). */\nimport { timingSafeEqualString } from \"@storyshelf/core/utils\";\nimport { createHmac, randomBytes } from \"node:crypto\";\n\n/** State lifetime: long enough to sign in at the service, short enough to limit replay. */\nconst STATE_TTL_MS = 10 * 60_000;\n\n/** What a state asserts about the flow that issued it. */\nexport interface OAuthState {\n /** User who started the flow. */\n userId: string;\n /** Service `metadata.kind`. */\n service: string;\n /** Local path to return to afterwards. */\n returnTo: string;\n /** Random value also kept in a cookie, binding the state to one browser. */\n nonce: string;\n expiresAt: number;\n}\n\n/** Issue a signed state for a new flow, with a fresh nonce. */\nexport function signState(\n secret: string,\n fields: Omit<OAuthState, \"nonce\" | \"expiresAt\">,\n now = Date.now(),\n): { state: string; nonce: string } {\n const nonce = randomBytes(16).toString(\"hex\");\n const body: OAuthState = { ...fields, nonce, expiresAt: now + STATE_TTL_MS };\n const payload = Buffer.from(JSON.stringify(body)).toString(\"base64url\");\n return { state: `${payload}.${mac(secret, payload)}`, nonce };\n}\n\n/** Parse and verify a state; null when forged, malformed or expired. */\nexport function verifyState(secret: string, state: string, now = Date.now()): OAuthState | null {\n const [payload, signature] = state.split(\".\");\n if (!payload || !signature || !timingSafeEqualString(signature, mac(secret, payload))) {\n return null;\n }\n try {\n const body = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\")) as OAuthState;\n return body.expiresAt > now ? body : null;\n } catch {\n return null;\n }\n}\n\n/** A same-site path (never an absolute or protocol-relative URL), else the fallback. */\nexport function safeReturnPath(value: string | undefined, fallback: string): string {\n return value && /^\\/(?![/\\\\])/u.test(value) ? value : fallback;\n}\n\nfunction mac(secret: string, payload: string): string {\n return createHmac(\"sha256\", secret).update(`oauth-connect:${payload}`).digest(\"base64url\");\n}\n"],"mappings":";;;;;;;;AAKA,MAAM,eAAe;;AAgBrB,SAAgB,UACd,QACA,QACA,MAAM,KAAK,IAAI,GACmB;CAClC,MAAM,QAAQ,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;CAC5C,MAAM,OAAmB;EAAE,GAAG;EAAQ;EAAO,WAAW,MAAM;CAAa;CAC3E,MAAM,UAAU,OAAO,KAAK,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,SAAS,WAAW;CACtE,OAAO;EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,QAAQ,OAAO;EAAK;CAAM;AAC9D;;AAGA,SAAgB,YAAY,QAAgB,OAAe,MAAM,KAAK,IAAI,GAAsB;CAC9F,MAAM,CAAC,SAAS,aAAa,MAAM,MAAM,GAAG;CAC5C,IAAI,CAAC,WAAW,CAAC,aAAa,CAAC,sBAAsB,WAAW,IAAI,QAAQ,OAAO,CAAC,GAClF,OAAO;CAET,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,OAAO,KAAK,SAAS,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;EAC1E,OAAO,KAAK,YAAY,MAAM,OAAO;CACvC,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAgB,eAAe,OAA2B,UAA0B;CAClF,OAAO,SAAS,gBAAgB,KAAK,KAAK,IAAI,QAAQ;AACxD;AAEA,SAAS,IAAI,QAAgB,SAAyB;CACpD,OAAO,WAAW,UAAU,MAAM,CAAC,CAAC,OAAO,iBAAiB,SAAS,CAAC,CAAC,OAAO,WAAW;AAC3F"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
|
+
import { getStore, testingEnabled } from "../store.mjs";
|
|
6
|
+
import { isAnonymouslyViewable, publishedAccess } from "./published-access.mjs";
|
|
7
|
+
import { STORYBOOK_TOKEN_PREFIX, signStorybookToken } from "@storyshelf/core/utils";
|
|
8
|
+
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
9
|
+
// src/routers/preview-base.ts
|
|
10
|
+
/** Lifetime of the token in a page: long enough to open the frame, short enough to be harmless. */
|
|
11
|
+
const PREVIEW_TOKEN_TTL = 3600;
|
|
12
|
+
/**
|
|
13
|
+
* How often a page asks for a fresh frame token (seconds): well inside {@link PREVIEW_TOKEN_TTL},
|
|
14
|
+
* so a frame on a page left open never holds an expired token. The token stays short-lived.
|
|
15
|
+
*/
|
|
16
|
+
const PREVIEW_TOKEN_REFRESH = 2400;
|
|
17
|
+
/** Whether a frame root is a signed `/_s/<token>` URL, the only kind that expires. */
|
|
18
|
+
function isSignedPreviewBase(base) {
|
|
19
|
+
return base.startsWith(STORYBOOK_TOKEN_PREFIX);
|
|
20
|
+
}
|
|
21
|
+
/** The frame's root URL (no trailing slash), or null when this viewer cannot be given one. */
|
|
22
|
+
async function previewBaseOf(project, build) {
|
|
23
|
+
const { config } = getStore();
|
|
24
|
+
if (isAnonymouslyViewable(build, project)) return createUrlBuilder("/", config.publishedBaseDomain).storybookBuild(project.slug, build.id);
|
|
25
|
+
const secret = credentialKeys(config);
|
|
26
|
+
if (!testingEnabled() || !secret || !await publishedAccess(build, project)) return null;
|
|
27
|
+
const token = signStorybookToken({
|
|
28
|
+
secret,
|
|
29
|
+
projectId: project.id,
|
|
30
|
+
buildId: build.id,
|
|
31
|
+
ttlSeconds: PREVIEW_TOKEN_TTL
|
|
32
|
+
});
|
|
33
|
+
return `${STORYBOOK_TOKEN_PREFIX}${token}`;
|
|
34
|
+
}
|
|
35
|
+
export { PREVIEW_TOKEN_REFRESH, PREVIEW_TOKEN_TTL, isSignedPreviewBase, previewBaseOf };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=preview-base.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-base.mjs","names":[],"sources":["../../src/routers/preview-base.ts"],"sourcesContent":["/**\n * The root a component page's preview frame loads a build's Storybook from (ADR 0006, decision 9).\n * A sandboxed frame has no cookies, so only a URL that needs none works in it: the build's own URL\n * when anyone who reaches the app may open it, otherwise a signed `/_s/<token>` URL minted for a\n * viewer who already has access (ADR 0007, decision 5; needs `testing` and a `secret`).\n */\nimport type { Build, Project } from \"@storyshelf/core/schema\";\nimport { createUrlBuilder } from \"@storyshelf/core/urls\";\nimport {\n signStorybookToken,\n STORYBOOK_TOKEN_PREFIX,\n type SecretInput,\n} from \"@storyshelf/core/utils\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore, testingEnabled } from \"../store.ts\";\nimport { isAnonymouslyViewable, publishedAccess } from \"./published-access.ts\";\n\n/** Lifetime of the token in a page: long enough to open the frame, short enough to be harmless. */\nexport const PREVIEW_TOKEN_TTL = 3600;\n\n/**\n * How often a page asks for a fresh frame token (seconds): well inside {@link PREVIEW_TOKEN_TTL},\n * so a frame on a page left open never holds an expired token. The token stays short-lived.\n */\nexport const PREVIEW_TOKEN_REFRESH = 2400;\n\n/** Whether a frame root is a signed `/_s/<token>` URL, the only kind that expires. */\nexport function isSignedPreviewBase(base: string): boolean {\n return base.startsWith(STORYBOOK_TOKEN_PREFIX);\n}\n\n/** The frame's root URL (no trailing slash), or null when this viewer cannot be given one. */\nexport async function previewBaseOf(project: Project, build: Build): Promise<string | null> {\n const { config } = getStore();\n if (isAnonymouslyViewable(build, project)) {\n return createUrlBuilder(\"/\", config.publishedBaseDomain).storybookBuild(project.slug, build.id);\n }\n const secret: SecretInput = credentialKeys(config);\n if (!testingEnabled() || !secret || !(await publishedAccess(build, project))) {\n return null;\n }\n const token = signStorybookToken({\n secret,\n projectId: project.id,\n buildId: build.id,\n ttlSeconds: PREVIEW_TOKEN_TTL,\n });\n return `${STORYBOOK_TOKEN_PREFIX}${token}`;\n}\n"],"mappings":";;;;;;;;;;AAkBA,MAAa,oBAAoB;;;;;AAMjC,MAAa,wBAAwB;;AAGrC,SAAgB,oBAAoB,MAAuB;CACzD,OAAO,KAAK,WAAW,sBAAsB;AAC/C;;AAGA,eAAsB,cAAc,SAAkB,OAAsC;CAC1F,MAAM,EAAE,WAAW,SAAS;CAC5B,IAAI,sBAAsB,OAAO,OAAO,GACtC,OAAO,iBAAiB,KAAK,OAAO,mBAAmB,CAAC,CAAC,eAAe,QAAQ,MAAM,MAAM,EAAE;CAEhG,MAAM,SAAsB,eAAe,MAAM;CACjD,IAAI,CAAC,eAAe,KAAK,CAAC,UAAU,CAAE,MAAM,gBAAgB,OAAO,OAAO,GACxE,OAAO;CAET,MAAM,QAAQ,mBAAmB;EAC/B;EACA,WAAW,QAAQ;EACnB,SAAS,MAAM;EACf,YAAY;CACd,CAAC;CACD,OAAO,GAAG,yBAAyB;AACrC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { isSignedPreviewBase, previewBaseOf } from "./preview-base.mjs";
|
|
6
|
+
import { SIGNED_URL_HEADERS } from "./published-files.mjs";
|
|
7
|
+
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
|
+
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
9
|
+
// src/routers/preview-token.ts
|
|
10
|
+
/**
|
|
11
|
+
* `GET /api/v1/projects/:slug/builds/:buildId/preview-token`: a fresh root for a component page's preview frame
|
|
12
|
+
* (ADR 0006, decision 9). A frame holding a signed `/_s/<token>` URL (ADR 0007, decision 5) outlives
|
|
13
|
+
* the token's hour on a page left open, so the page's script asks for a new root before then and
|
|
14
|
+
* re-points the frame. Answers `{ base }` for a viewer who may open the build, 404 otherwise or when
|
|
15
|
+
* the build needs no token (anyone may open it). Never cached: the answer carries a token.
|
|
16
|
+
*/
|
|
17
|
+
const previewTokenRoute = createRoute({
|
|
18
|
+
method: "get",
|
|
19
|
+
tags: ["Review"],
|
|
20
|
+
summary: "Get a fresh signed root for a build's preview frame",
|
|
21
|
+
path: "/api/v1/projects/{slug}/builds/{buildId}/preview-token",
|
|
22
|
+
request: { params: z.object({
|
|
23
|
+
slug: z.string(),
|
|
24
|
+
buildId: z.string()
|
|
25
|
+
}) },
|
|
26
|
+
responses: {
|
|
27
|
+
200: {
|
|
28
|
+
content: { "application/json": { schema: z.object({ base: z.string() }) } },
|
|
29
|
+
description: "The signed root (`/_s/<token>`) to frame the build's Storybook under"
|
|
30
|
+
},
|
|
31
|
+
404: {
|
|
32
|
+
content: { "text/plain": { schema: z.string() } },
|
|
33
|
+
description: "Not found, or the build needs no signed URL"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
/** Register the preview token refresh route. */
|
|
38
|
+
function registerPreviewToken(app) {
|
|
39
|
+
app.openapi(previewTokenRoute, async (c) => {
|
|
40
|
+
const { db } = getStore();
|
|
41
|
+
const project = await new ProjectModel(db).getBySlug(c.req.param("slug"));
|
|
42
|
+
const build = await new BuildModel(db).get(c.req.param("buildId"));
|
|
43
|
+
const base = project && build?.projectId === project.id ? await previewBaseOf(project, build) : null;
|
|
44
|
+
if (base === null || !isSignedPreviewBase(base)) return c.text("Not found", 404, SIGNED_URL_HEADERS);
|
|
45
|
+
return c.json({ base }, 200, SIGNED_URL_HEADERS);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export { registerPreviewToken };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=preview-token.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview-token.mjs","names":[],"sources":["../../src/routers/preview-token.ts"],"sourcesContent":["/**\n * `GET /api/v1/projects/:slug/builds/:buildId/preview-token`: a fresh root for a component page's preview frame\n * (ADR 0006, decision 9). A frame holding a signed `/_s/<token>` URL (ADR 0007, decision 5) outlives\n * the token's hour on a page left open, so the page's script asks for a new root before then and\n * re-points the frame. Answers `{ base }` for a viewer who may open the build, 404 otherwise or when\n * the build needs no token (anyone may open it). Never cached: the answer carries a token.\n */\nimport { createRoute, z } from \"@hono/zod-openapi\";\nimport { BuildModel, ProjectModel } from \"@storyshelf/core/models\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { isSignedPreviewBase, previewBaseOf } from \"./preview-base.ts\";\nimport { SIGNED_URL_HEADERS } from \"./published-files.ts\";\n\nconst previewTokenRoute = createRoute({\n method: \"get\",\n tags: [\"Review\"],\n summary: \"Get a fresh signed root for a build's preview frame\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/preview-token\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ base: z.string() }) } },\n description: \"The signed root (`/_s/<token>`) to frame the build's Storybook under\",\n },\n 404: {\n content: { \"text/plain\": { schema: z.string() } },\n description: \"Not found, or the build needs no signed URL\",\n },\n },\n});\n\n/** Register the preview token refresh route. */\nexport function registerPreviewToken(app: ShelfRouter): void {\n app.openapi(previewTokenRoute, async (c) => {\n const { db } = getStore();\n const project = await new ProjectModel(db).getBySlug(c.req.param(\"slug\"));\n const build = await new BuildModel(db).get(c.req.param(\"buildId\"));\n const base =\n project && build?.projectId === project.id ? await previewBaseOf(project, build) : null;\n if (base === null || !isSignedPreviewBase(base)) {\n return c.text(\"Not found\", 404, SIGNED_URL_HEADERS);\n }\n return c.json({ base }, 200, SIGNED_URL_HEADERS);\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAcA,MAAM,oBAAoB,YAAY;CACpC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;CAAE,CAAC,EAAE;CACvE,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;GAC1E,aAAa;EACf;EACA,KAAK;GACH,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;GAChD,aAAa;EACf;CACF;AACF,CAAC;;AAGD,SAAgB,qBAAqB,KAAwB;CAC3D,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,MAAM,EAAE,OAAO,SAAS;EACxB,MAAM,UAAU,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,UAAU,EAAE,IAAI,MAAM,MAAM,CAAC;EACxE,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,MAAM,SAAS,CAAC;EACjE,MAAM,OACJ,WAAW,OAAO,cAAc,QAAQ,KAAK,MAAM,cAAc,SAAS,KAAK,IAAI;EACrF,IAAI,SAAS,QAAQ,CAAC,oBAAoB,IAAI,GAC5C,OAAO,EAAE,KAAK,aAAa,KAAK,kBAAkB;EAEpD,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG,KAAK,kBAAkB;CACjD,CAAC;AACH"}
|
package/dist/routers/profile.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
|
+
import { notificationsPaused } from "../notifications-pause.mjs";
|
|
5
6
|
import { hxRedirect } from "./htmx.mjs";
|
|
6
7
|
import { flash } from "./flash.mjs";
|
|
7
8
|
import { forwardToEngine } from "./auth.mjs";
|
|
@@ -10,7 +11,7 @@ import { NotificationSubscriptionModel, ProjectModel, UserModel } from "@storysh
|
|
|
10
11
|
import { SESSION_COOKIE } from "@storyshelf/core/types";
|
|
11
12
|
import { MIN_PASSWORD_LENGTH, unsignedToken } from "@storyshelf/core/auth";
|
|
12
13
|
import { eq, getTableColumns } from "@storyshelf/core/orm";
|
|
13
|
-
|
|
14
|
+
// src/routers/profile.ts
|
|
14
15
|
function field(form, name) {
|
|
15
16
|
const raw = form.get(name);
|
|
16
17
|
return typeof raw === "string" ? raw : "";
|
|
@@ -89,7 +90,8 @@ async function profileView(c, user, auth, extra) {
|
|
|
89
90
|
subscribedSlugs,
|
|
90
91
|
isLocal: extra?.local ?? security.hasPassword,
|
|
91
92
|
security,
|
|
92
|
-
error: extra?.error
|
|
93
|
+
error: extra?.error,
|
|
94
|
+
notificationsPaused: getStore().notifiers.length > 0 && await notificationsPaused()
|
|
93
95
|
});
|
|
94
96
|
if (extra?.success) flash(c, extra.success);
|
|
95
97
|
c.header("Cache-Control", "no-store");
|
|
@@ -270,7 +272,6 @@ function readPasswordChange(form) {
|
|
|
270
272
|
next
|
|
271
273
|
};
|
|
272
274
|
}
|
|
273
|
-
//#endregion
|
|
274
275
|
export { registerProfile };
|
|
275
276
|
|
|
276
277
|
//# sourceMappingURL=profile.mjs.map
|