@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
|
@@ -2,7 +2,8 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
|
-
|
|
5
|
+
import { STORYBOOK_TOKEN_PREFIX } from "@storyshelf/core/utils";
|
|
6
|
+
// src/middleware/auth-gate.ts
|
|
6
7
|
/** Resolve the request user from the auth adapter (null when auth is off or a token is used). */
|
|
7
8
|
async function resolveRequestUser(c, auth) {
|
|
8
9
|
if (!auth) return null;
|
|
@@ -13,13 +14,14 @@ async function resolveRequestUser(c, auth) {
|
|
|
13
14
|
* Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML
|
|
14
15
|
* requests redirect to login. API/auth routes answer 401/403 themselves, and
|
|
15
16
|
* published-Storybook routes enforce their own auth (public builds are
|
|
16
|
-
* viewable without a session, ADR 0011)
|
|
17
|
+
* viewable without a session, ADR 0011), and `/_s/<token>` is answered by its signed token
|
|
18
|
+
* alone (ADR 0007).
|
|
17
19
|
*/
|
|
18
20
|
function authGate() {
|
|
19
21
|
return async (c, next) => {
|
|
20
22
|
const { user, authEnabled: isAuthEnabled } = getStore();
|
|
21
23
|
const { path } = c.req;
|
|
22
|
-
if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith("/.well-known/") || path.startsWith("/projects/") && path.includes("/storybook") || /^\/projects\/[^/]+\/?$/u.test(path)) {
|
|
24
|
+
if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith(STORYBOOK_TOKEN_PREFIX) || path.startsWith("/.well-known/") || path.startsWith("/projects/") && path.includes("/storybook") || /^\/projects\/[^/]+\/?$/u.test(path)) {
|
|
23
25
|
await next();
|
|
24
26
|
return;
|
|
25
27
|
}
|
|
@@ -30,7 +32,6 @@ function authGate() {
|
|
|
30
32
|
return c.redirect("/auth/login", 302);
|
|
31
33
|
};
|
|
32
34
|
}
|
|
33
|
-
//#endregion
|
|
34
35
|
export { authGate, resolveRequestUser };
|
|
35
36
|
|
|
36
37
|
//# sourceMappingURL=auth-gate.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-gate.mjs","names":[],"sources":["../../src/middleware/auth-gate.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/core/auth\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { Context, Next } from \"hono\";\nimport { getStore } from \"../store.ts\";\n\n/** Resolve the request user from the auth adapter (null when auth is off or a token is used). */\nexport async function resolveRequestUser(\n c: Pick<Context, \"req\">,\n auth?: Auth,\n): Promise<AuthUser | null> {\n if (!auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await auth.check(c.req.raw);\n}\n\n/**\n * Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n * requests redirect to login. API/auth routes answer 401/403 themselves, and\n * published-Storybook routes enforce their own auth (public builds are\n * viewable without a session, ADR 0011).\n */\nexport function authGate() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const { user, authEnabled: isAuthEnabled } = getStore();\n const { path } = c.req;\n if (\n !isAuthEnabled ||\n user ||\n path.startsWith(\"/api/\") ||\n path.startsWith(\"/auth/\") ||\n path.startsWith(\"/assets/\") ||\n path.startsWith(\"/.well-known/\") ||\n (path.startsWith(\"/projects/\") && path.includes(\"/storybook\")) ||\n /^\\/projects\\/[^/]+\\/?$/u.test(path)\n ) {\n await next();\n return;\n }\n if (c.req.header(\"HX-Request\") === \"true\") {\n c.header(\"HX-Redirect\", \"/auth/login\");\n return c.body(null, 204);\n }\n return c.redirect(\"/auth/login\", 302);\n };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"auth-gate.mjs","names":[],"sources":["../../src/middleware/auth-gate.ts"],"sourcesContent":["import type { Auth } from \"@storyshelf/core/auth\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport { STORYBOOK_TOKEN_PREFIX } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport { getStore } from \"../store.ts\";\n\n/** Resolve the request user from the auth adapter (null when auth is off or a token is used). */\nexport async function resolveRequestUser(\n c: Pick<Context, \"req\">,\n auth?: Auth,\n): Promise<AuthUser | null> {\n if (!auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await auth.check(c.req.raw);\n}\n\n/**\n * Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n * requests redirect to login. API/auth routes answer 401/403 themselves, and\n * published-Storybook routes enforce their own auth (public builds are\n * viewable without a session, ADR 0011), and `/_s/<token>` is answered by its signed token\n * alone (ADR 0007).\n */\nexport function authGate() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const { user, authEnabled: isAuthEnabled } = getStore();\n const { path } = c.req;\n if (\n !isAuthEnabled ||\n user ||\n path.startsWith(\"/api/\") ||\n path.startsWith(\"/auth/\") ||\n path.startsWith(\"/assets/\") ||\n path.startsWith(STORYBOOK_TOKEN_PREFIX) ||\n path.startsWith(\"/.well-known/\") ||\n (path.startsWith(\"/projects/\") && path.includes(\"/storybook\")) ||\n /^\\/projects\\/[^/]+\\/?$/u.test(path)\n ) {\n await next();\n return;\n }\n if (c.req.header(\"HX-Request\") === \"true\") {\n c.header(\"HX-Redirect\", \"/auth/login\");\n return c.body(null, 204);\n }\n return c.redirect(\"/auth/login\", 302);\n };\n}\n"],"mappings":";;;;;;;AAOA,eAAsB,mBACpB,GACA,MAC0B;CAC1B,IAAI,CAAC,MACH,OAAO;CAET,IAAI,EAAE,IAAI,OAAO,eAAe,GAC9B,OAAO;CAET,OAAO,MAAM,KAAK,MAAM,EAAE,IAAI,GAAG;AACnC;;;;;;;;AASA,SAAgB,WAAW;CAEzB,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,EAAE,MAAM,aAAa,kBAAkB,SAAS;EACtD,MAAM,EAAE,SAAS,EAAE;EACnB,IACE,CAAC,iBACD,QACA,KAAK,WAAW,OAAO,KACvB,KAAK,WAAW,QAAQ,KACxB,KAAK,WAAW,UAAU,KAC1B,KAAK,WAAW,sBAAsB,KACtC,KAAK,WAAW,eAAe,KAC9B,KAAK,WAAW,YAAY,KAAK,KAAK,SAAS,YAAY,KAC5D,0BAA0B,KAAK,IAAI,GACnC;GACA,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,OAAO,YAAY,MAAM,QAAQ;GACzC,EAAE,OAAO,eAAe,aAAa;GACrC,OAAO,EAAE,KAAK,MAAM,GAAG;EACzB;EACA,OAAO,EAAE,SAAS,eAAe,GAAG;CACtC;AACF"}
|
package/dist/middleware/csrf.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { timingSafeEqualString } from "@storyshelf/core/utils";
|
|
5
5
|
import { SESSION_COOKIE } from "@storyshelf/core/types";
|
|
6
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
7
|
-
|
|
7
|
+
// src/middleware/csrf.ts
|
|
8
8
|
const DEFAULT_SESSION = "default";
|
|
9
9
|
const FALLBACK_SECRET = randomBytes(32).toString("hex");
|
|
10
10
|
/** Use the app secret when provided so tokens survive restarts and instances. */
|
|
@@ -69,7 +69,6 @@ function csrf(secret) {
|
|
|
69
69
|
function getCsrfToken(secret, sessionId = DEFAULT_SESSION) {
|
|
70
70
|
return generateToken(resolveSecret(secret), sessionId);
|
|
71
71
|
}
|
|
72
|
-
//#endregion
|
|
73
72
|
export { csrf, getCsrfToken, sessionIdFrom };
|
|
74
73
|
|
|
75
74
|
//# sourceMappingURL=csrf.mjs.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
// src/middleware/rate-limit.ts
|
|
5
5
|
/** Direct peer IP from server runtimes exposing the socket (unspoofable TCP source). */
|
|
6
6
|
function peerIp(c) {
|
|
7
7
|
const env = c.env ?? {};
|
|
@@ -25,44 +25,51 @@ function rateLimitKey(prefix) {
|
|
|
25
25
|
return `${prefix}:anonymous`;
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
const stores = /* @__PURE__ */ new Map();
|
|
29
28
|
/** Cap tracked buckets (spoofed keys would otherwise grow the map forever). */
|
|
30
29
|
const MAX_BUCKETS = 1e4;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
30
|
+
const SWEEP_INTERVAL_MS = 6e4;
|
|
31
|
+
/**
|
|
32
|
+
* Create a rate limiter with its own bucket store, so each app instance
|
|
33
|
+
* counts independently. Limiters built from the same `rateLimit` factory that
|
|
34
|
+
* resolve to the same key share one counter (e.g. `/api/v1/*` and
|
|
35
|
+
* `/api/v1/tokens/*` both bill the caller's `api:` bucket).
|
|
36
|
+
*/
|
|
37
|
+
function createRateLimiter() {
|
|
38
|
+
const stores = /* @__PURE__ */ new Map();
|
|
39
|
+
let lastSweep = Date.now();
|
|
40
|
+
const sweep = (now) => {
|
|
41
|
+
lastSweep = now;
|
|
42
|
+
for (const [key, entry] of stores) if (now > entry.resetAt) stores.delete(key);
|
|
43
|
+
};
|
|
44
|
+
return (options) => {
|
|
45
|
+
const { windowMs, max, keyGenerator } = options;
|
|
46
|
+
return async (c, next) => {
|
|
47
|
+
const key = keyGenerator ? keyGenerator(c) : rateLimitKey("api")(c);
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
if (now - lastSweep > SWEEP_INTERVAL_MS) sweep(now);
|
|
50
|
+
let entry = stores.get(key);
|
|
51
|
+
if (!entry || now > entry.resetAt) {
|
|
52
|
+
entry = {
|
|
53
|
+
count: 0,
|
|
54
|
+
resetAt: now + windowMs
|
|
55
|
+
};
|
|
56
|
+
if (stores.size >= MAX_BUCKETS) {
|
|
57
|
+
const oldest = stores.keys().next();
|
|
58
|
+
if (!oldest.done) stores.delete(oldest.value);
|
|
59
|
+
}
|
|
60
|
+
stores.set(key, entry);
|
|
61
|
+
}
|
|
62
|
+
entry.count += 1;
|
|
63
|
+
if (entry.count > max) {
|
|
64
|
+
c.header("Retry-After", String(Math.ceil((entry.resetAt - now) / 1e3)));
|
|
65
|
+
return c.json({ error: "Too many requests" }, 429);
|
|
51
66
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
|
|
57
|
-
c.header("Retry-After", String(retryAfter));
|
|
58
|
-
return c.json({ error: "Too many requests" }, 429);
|
|
59
|
-
}
|
|
60
|
-
c.header("X-RateLimit-Limit", String(max));
|
|
61
|
-
c.header("X-RateLimit-Remaining", String(Math.max(0, max - entry.count)));
|
|
62
|
-
await next();
|
|
67
|
+
c.header("X-RateLimit-Limit", String(max));
|
|
68
|
+
c.header("X-RateLimit-Remaining", String(Math.max(0, max - entry.count)));
|
|
69
|
+
return await next();
|
|
70
|
+
};
|
|
63
71
|
};
|
|
64
72
|
}
|
|
65
|
-
|
|
66
|
-
export { rateLimit, rateLimitKey };
|
|
73
|
+
export { createRateLimiter, rateLimitKey };
|
|
67
74
|
|
|
68
75
|
//# sourceMappingURL=rate-limit.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":["import type { Context,
|
|
1
|
+
{"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":["import type { Context, MiddlewareHandler } from \"hono\";\n\ninterface RateLimitEntry {\n count: number;\n resetAt: number;\n}\n\nexport interface RateLimitOptions {\n windowMs: number;\n max: number;\n keyGenerator?: (c: Context) => string;\n}\n\n/** Direct peer IP from server runtimes exposing the socket (unspoofable TCP source). */\nfunction peerIp(c: Pick<Context, \"env\">): string | undefined {\n const env = (c.env ?? {}) as {\n incoming?: { socket?: { remoteAddress?: unknown } };\n server?: { incoming?: { socket?: { remoteAddress?: unknown } } };\n };\n const address =\n env.incoming?.socket?.remoteAddress ?? env.server?.incoming?.socket?.remoteAddress;\n return typeof address === \"string\" && address ? address : undefined;\n}\n\n/**\n * Rate-limit identity for one bucket prefix: direct peer first, then the\n * first `X-Forwarded-For` entry (client-controlled, so only a fallback),\n * then `X-Real-IP`, then a shared bucket. Deployments behind proxies with\n * many users behind one peer IP share that peer's budget.\n */\nexport function rateLimitKey(prefix: string): (c: Context) => string {\n return (c) => {\n const peer = peerIp(c);\n if (peer) {\n return `${prefix}:${peer}`;\n }\n const forwarded = c.req.header(\"x-forwarded-for\")?.split(\",\")[0]?.trim();\n if (forwarded) {\n return `${prefix}:fwd:${forwarded}`;\n }\n const real = c.req.header(\"x-real-ip\")?.trim();\n if (real) {\n return `${prefix}:fwd:${real}`;\n }\n return `${prefix}:anonymous`;\n };\n}\n\n/** Cap tracked buckets (spoofed keys would otherwise grow the map forever). */\nconst MAX_BUCKETS = 10_000;\nconst SWEEP_INTERVAL_MS = 60_000;\n\n/**\n * Create a rate limiter with its own bucket store, so each app instance\n * counts independently. Limiters built from the same `rateLimit` factory that\n * resolve to the same key share one counter (e.g. `/api/v1/*` and\n * `/api/v1/tokens/*` both bill the caller's `api:` bucket).\n */\nexport function createRateLimiter(): (options: RateLimitOptions) => MiddlewareHandler {\n const stores = new Map<string, RateLimitEntry>();\n let lastSweep = Date.now();\n\n const sweep = (now: number): void => {\n lastSweep = now;\n for (const [key, entry] of stores) {\n if (now > entry.resetAt) {\n stores.delete(key);\n }\n }\n };\n\n return (options) => {\n const { windowMs, max, keyGenerator } = options;\n return async (c, next) => {\n const key = keyGenerator ? keyGenerator(c) : rateLimitKey(\"api\")(c);\n const now = Date.now();\n if (now - lastSweep > SWEEP_INTERVAL_MS) {\n sweep(now);\n }\n let entry = stores.get(key);\n if (!entry || now > entry.resetAt) {\n entry = { count: 0, resetAt: now + windowMs };\n if (stores.size >= MAX_BUCKETS) {\n const oldest = stores.keys().next();\n if (!oldest.done) {\n stores.delete(oldest.value);\n }\n }\n stores.set(key, entry);\n }\n entry.count += 1;\n if (entry.count > max) {\n c.header(\"Retry-After\", String(Math.ceil((entry.resetAt - now) / 1000)));\n return c.json({ error: \"Too many requests\" }, 429);\n }\n c.header(\"X-RateLimit-Limit\", String(max));\n c.header(\"X-RateLimit-Remaining\", String(Math.max(0, max - entry.count)));\n return await next();\n };\n };\n}\n"],"mappings":";;;;;AAcA,SAAS,OAAO,GAA6C;CAC3D,MAAM,MAAO,EAAE,OAAO,CAAC;CAIvB,MAAM,UACJ,IAAI,UAAU,QAAQ,iBAAiB,IAAI,QAAQ,UAAU,QAAQ;CACvE,OAAO,OAAO,YAAY,YAAY,UAAU,UAAU,KAAA;AAC5D;;;;;;;AAQA,SAAgB,aAAa,QAAwC;CACnE,QAAQ,MAAM;EACZ,MAAM,OAAO,OAAO,CAAC;EACrB,IAAI,MACF,OAAO,GAAG,OAAO,GAAG;EAEtB,MAAM,YAAY,EAAE,IAAI,OAAO,iBAAiB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK;EACvE,IAAI,WACF,OAAO,GAAG,OAAO,OAAO;EAE1B,MAAM,OAAO,EAAE,IAAI,OAAO,WAAW,CAAC,EAAE,KAAK;EAC7C,IAAI,MACF,OAAO,GAAG,OAAO,OAAO;EAE1B,OAAO,GAAG,OAAO;CACnB;AACF;;AAGA,MAAM,cAAc;AACpB,MAAM,oBAAoB;;;;;;;AAQ1B,SAAgB,oBAAsE;CACpF,MAAM,yBAAS,IAAI,IAA4B;CAC/C,IAAI,YAAY,KAAK,IAAI;CAEzB,MAAM,SAAS,QAAsB;EACnC,YAAY;EACZ,KAAK,MAAM,CAAC,KAAK,UAAU,QACzB,IAAI,MAAM,MAAM,SACd,OAAO,OAAO,GAAG;CAGvB;CAEA,QAAQ,YAAY;EAClB,MAAM,EAAE,UAAU,KAAK,iBAAiB;EACxC,OAAO,OAAO,GAAG,SAAS;GACxB,MAAM,MAAM,eAAe,aAAa,CAAC,IAAI,aAAa,KAAK,CAAC,CAAC,CAAC;GAClE,MAAM,MAAM,KAAK,IAAI;GACrB,IAAI,MAAM,YAAY,mBACpB,MAAM,GAAG;GAEX,IAAI,QAAQ,OAAO,IAAI,GAAG;GAC1B,IAAI,CAAC,SAAS,MAAM,MAAM,SAAS;IACjC,QAAQ;KAAE,OAAO;KAAG,SAAS,MAAM;IAAS;IAC5C,IAAI,OAAO,QAAQ,aAAa;KAC9B,MAAM,SAAS,OAAO,KAAK,CAAC,CAAC,KAAK;KAClC,IAAI,CAAC,OAAO,MACV,OAAO,OAAO,OAAO,KAAK;IAE9B;IACA,OAAO,IAAI,KAAK,KAAK;GACvB;GACA,MAAM,SAAS;GACf,IAAI,MAAM,QAAQ,KAAK;IACrB,EAAE,OAAO,eAAe,OAAO,KAAK,MAAM,MAAM,UAAU,OAAO,GAAI,CAAC,CAAC;IACvE,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;GACnD;GACA,EAAE,OAAO,qBAAqB,OAAO,GAAG,CAAC;GACzC,EAAE,OAAO,yBAAyB,OAAO,KAAK,IAAI,GAAG,MAAM,MAAM,KAAK,CAAC,CAAC;GACxE,OAAO,MAAM,KAAK;EACpB;CACF;AACF"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
import { redactSecrets } from "@storyshelf/core/utils";
|
|
5
|
+
// src/middleware/request-log.ts
|
|
5
6
|
/** Hono middleware logging request start/end with duration. */
|
|
6
7
|
function requestLogging(logger) {
|
|
7
8
|
return async (c, next) => {
|
|
@@ -10,21 +11,20 @@ function requestLogging(logger) {
|
|
|
10
11
|
logger.info({
|
|
11
12
|
reqId: id,
|
|
12
13
|
method: c.req.method,
|
|
13
|
-
url: c.req.path
|
|
14
|
+
url: redactSecrets(c.req.path)
|
|
14
15
|
}, "request start");
|
|
15
16
|
await next();
|
|
16
17
|
const userId = c.get("userId");
|
|
17
18
|
logger.info({
|
|
18
19
|
reqId: id,
|
|
19
20
|
method: c.req.method,
|
|
20
|
-
url: c.req.path,
|
|
21
|
+
url: redactSecrets(c.req.path),
|
|
21
22
|
status: c.res.status,
|
|
22
23
|
durationMs: Math.round(performance.now() - started),
|
|
23
24
|
...userId ? { userId } : {}
|
|
24
25
|
}, "request end");
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
|
-
//#endregion
|
|
28
28
|
export { requestLogging };
|
|
29
29
|
|
|
30
30
|
//# sourceMappingURL=request-log.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request-log.mjs","names":[],"sources":["../../src/middleware/request-log.ts"],"sourcesContent":["import type { Logger } from \"@storyshelf/core/logger\";\nimport type { Context, Next } from \"hono\";\n\n/** Hono middleware logging request start/end with duration. */\nexport function requestLogging(logger: Logger) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const started = performance.now();\n const id = c.get(\"requestId\");\n logger.info({ reqId: id, method: c.req.method, url: c.req.path }
|
|
1
|
+
{"version":3,"file":"request-log.mjs","names":[],"sources":["../../src/middleware/request-log.ts"],"sourcesContent":["import type { Logger } from \"@storyshelf/core/logger\";\nimport { redactSecrets } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\n\n/** Hono middleware logging request start/end with duration. */\nexport function requestLogging(logger: Logger) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const started = performance.now();\n const id = c.get(\"requestId\");\n logger.info(\n { reqId: id, method: c.req.method, url: redactSecrets(c.req.path) },\n \"request start\",\n );\n await next();\n const userId = c.get(\"userId\") as string | null | undefined;\n logger.info(\n {\n reqId: id,\n method: c.req.method,\n url: redactSecrets(c.req.path),\n status: c.res.status,\n durationMs: Math.round(performance.now() - started),\n ...(userId ? { userId } : {}),\n },\n \"request end\",\n );\n };\n}\n"],"mappings":";;;;;;AAKA,SAAgB,eAAe,QAAgB;CAE7C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,UAAU,YAAY,IAAI;EAChC,MAAM,KAAK,EAAE,IAAI,WAAW;EAC5B,OAAO,KACL;GAAE,OAAO;GAAI,QAAQ,EAAE,IAAI;GAAQ,KAAK,cAAc,EAAE,IAAI,IAAI;EAAE,GAClE,eACF;EACA,MAAM,KAAK;EACX,MAAM,SAAS,EAAE,IAAI,QAAQ;EAC7B,OAAO,KACL;GACE,OAAO;GACP,QAAQ,EAAE,IAAI;GACd,KAAK,cAAc,EAAE,IAAI,IAAI;GAC7B,QAAQ,EAAE,IAAI;GACd,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,OAAO;GAClD,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,GACA,aACF;CACF;AACF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { matchedRouteClass } from "./require-testing.mjs";
|
|
5
|
+
import { resolveAuthorizedProject } from "../routers/helpers.mjs";
|
|
6
|
+
// src/middleware/require-review.ts
|
|
7
|
+
/** Body of the 404 a `review` route answers for a project with review off. */
|
|
8
|
+
const REVIEW_DISABLED_MESSAGE = "Review is off for this project.";
|
|
9
|
+
/** Roles that may see a project's routes at all; the gate answers only to them. */
|
|
10
|
+
const VIEWER_ROLES = [
|
|
11
|
+
"viewer",
|
|
12
|
+
"developer",
|
|
13
|
+
"approver",
|
|
14
|
+
"admin"
|
|
15
|
+
];
|
|
16
|
+
/** The project slug of `/api/v1/projects/:slug/...` and `/projects/:slug/...` paths. */
|
|
17
|
+
function projectSlugOf(path) {
|
|
18
|
+
return /^(?:\/api\/v1)?\/projects\/(?<slug>[^/]+)\//u.exec(path)?.groups?.["slug"];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Gate routes classified `review` (ADR 0013, decision 10): they answer 404 for a project that has
|
|
22
|
+
* review off, as if they did not exist. The switch is read from the database on every request, so
|
|
23
|
+
* turning review off or on takes effect at once. Only a caller who may view the project is told
|
|
24
|
+
* (anyone else gets the route's own refusal), so the gate reveals nothing about a project the
|
|
25
|
+
* caller cannot see. It runs after the store scope and the auth gate, which it needs to resolve
|
|
26
|
+
* the caller; routes of other classes, and unknown projects, pass through.
|
|
27
|
+
*/
|
|
28
|
+
function requireReview() {
|
|
29
|
+
return async (c, next) => {
|
|
30
|
+
if (matchedRouteClass(c) !== "review" || !await reviewIsOff(c)) {
|
|
31
|
+
await next();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (c.req.path.startsWith("/api/")) return c.json({ error: REVIEW_DISABLED_MESSAGE }, 404);
|
|
35
|
+
return c.text(REVIEW_DISABLED_MESSAGE, 404);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Whether the request names a project the caller may view and whose review switch is off. */
|
|
39
|
+
async function reviewIsOff(c) {
|
|
40
|
+
const slug = projectSlugOf(c.req.path);
|
|
41
|
+
if (slug === void 0) return false;
|
|
42
|
+
try {
|
|
43
|
+
return (await resolveAuthorizedProject(c, slug, ...VIEWER_ROLES)).reviewEnabled === false;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export { REVIEW_DISABLED_MESSAGE, requireReview };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=require-review.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-review.mjs","names":[],"sources":["../../src/middleware/require-review.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\nimport { resolveAuthorizedProject } from \"../routers/helpers.ts\";\nimport { matchedRouteClass } from \"./require-testing.ts\";\n\n/** Body of the 404 a `review` route answers for a project with review off. */\nexport const REVIEW_DISABLED_MESSAGE = \"Review is off for this project.\";\n\n/** Roles that may see a project's routes at all; the gate answers only to them. */\nconst VIEWER_ROLES = [\"viewer\", \"developer\", \"approver\", \"admin\"] as const;\n\n/** The project slug of `/api/v1/projects/:slug/...` and `/projects/:slug/...` paths. */\nfunction projectSlugOf(path: string): string | undefined {\n return /^(?:\\/api\\/v1)?\\/projects\\/(?<slug>[^/]+)\\//u.exec(path)?.groups?.[\"slug\"];\n}\n\n/**\n * Gate routes classified `review` (ADR 0013, decision 10): they answer 404 for a project that has\n * review off, as if they did not exist. The switch is read from the database on every request, so\n * turning review off or on takes effect at once. Only a caller who may view the project is told\n * (anyone else gets the route's own refusal), so the gate reveals nothing about a project the\n * caller cannot see. It runs after the store scope and the auth gate, which it needs to resolve\n * the caller; routes of other classes, and unknown projects, pass through.\n */\nexport function requireReview() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (matchedRouteClass(c) !== \"review\" || !(await reviewIsOff(c))) {\n await next();\n return;\n }\n if (c.req.path.startsWith(\"/api/\")) {\n return c.json({ error: REVIEW_DISABLED_MESSAGE }, 404);\n }\n return c.text(REVIEW_DISABLED_MESSAGE, 404);\n };\n}\n\n/** Whether the request names a project the caller may view and whose review switch is off. */\nasync function reviewIsOff(c: Context): Promise<boolean> {\n const slug = projectSlugOf(c.req.path);\n if (slug === undefined) {\n return false;\n }\n try {\n const project = await resolveAuthorizedProject(c, slug, ...VIEWER_ROLES);\n return project.reviewEnabled === false;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;AAKA,MAAa,0BAA0B;;AAGvC,MAAM,eAAe;CAAC;CAAU;CAAa;CAAY;AAAO;;AAGhE,SAAS,cAAc,MAAkC;CACvD,OAAO,+CAA+C,KAAK,IAAI,CAAC,EAAE,SAAS;AAC7E;;;;;;;;;AAUA,SAAgB,gBAAgB;CAE9B,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,kBAAkB,CAAC,MAAM,YAAY,CAAE,MAAM,YAAY,CAAC,GAAI;GAChE,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,KAAK,WAAW,OAAO,GAC/B,OAAO,EAAE,KAAK,EAAE,OAAO,wBAAwB,GAAG,GAAG;EAEvD,OAAO,EAAE,KAAK,yBAAyB,GAAG;CAC5C;AACF;;AAGA,eAAe,YAAY,GAA8B;CACvD,MAAM,OAAO,cAAc,EAAE,IAAI,IAAI;CACrC,IAAI,SAAS,KAAA,GACX,OAAO;CAET,IAAI;EAEF,QAAO,MADe,yBAAyB,GAAG,MAAM,GAAG,YAAY,EAAA,CACxD,kBAAkB;CACnC,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { classOfRoute } from "../routes/classification.mjs";
|
|
5
|
+
import { matchedRoutes } from "hono/route";
|
|
6
|
+
// src/middleware/require-testing.ts
|
|
7
|
+
/** Body of the 404 a `testing` route answers on a hosting-only instance. */
|
|
8
|
+
const TESTING_DISABLED_MESSAGE = "Testing is not enabled on this instance.";
|
|
9
|
+
/**
|
|
10
|
+
* Gate routes classified `testing` (ADR 0006, decisions 1 and 8): without `options.testing` they
|
|
11
|
+
* answer 404, as if the feature did not exist. The class comes from the route table through the
|
|
12
|
+
* handler Hono matched, so a new route needs no guard of its own, only a table entry. It is mounted
|
|
13
|
+
* before the CSRF and rate-limit guards so a hidden route answers the same 404 for every request. `hosting`,
|
|
14
|
+
* `both` and `review` routes, and requests that match no classified route, pass through.
|
|
15
|
+
*/
|
|
16
|
+
function requireTesting(enabled) {
|
|
17
|
+
return async (c, next) => {
|
|
18
|
+
if (enabled || !isTestingRoute(c)) {
|
|
19
|
+
await next();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (c.req.path.startsWith("/api/")) return c.json({ error: TESTING_DISABLED_MESSAGE }, 404);
|
|
23
|
+
return c.text(TESTING_DISABLED_MESSAGE, 404);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The last matched handler that the table classifies (middleware matches come first). */
|
|
27
|
+
function matchedRouteClass(c) {
|
|
28
|
+
const routes = matchedRoutes(c);
|
|
29
|
+
for (let index = routes.length - 1; index >= 0; index -= 1) {
|
|
30
|
+
const route = routes[index];
|
|
31
|
+
const routeClass = route && classOfRoute(route.method, route.path);
|
|
32
|
+
if (routeClass !== void 0) return routeClass;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isTestingRoute(c) {
|
|
36
|
+
return matchedRouteClass(c) === "testing";
|
|
37
|
+
}
|
|
38
|
+
export { TESTING_DISABLED_MESSAGE, matchedRouteClass, requireTesting };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=require-testing.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-testing.mjs","names":[],"sources":["../../src/middleware/require-testing.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\nimport { matchedRoutes } from \"hono/route\";\nimport { classOfRoute, type RouteClass } from \"../routes/classification.ts\";\n\n/** Body of the 404 a `testing` route answers on a hosting-only instance. */\nexport const TESTING_DISABLED_MESSAGE = \"Testing is not enabled on this instance.\";\n\n/**\n * Gate routes classified `testing` (ADR 0006, decisions 1 and 8): without `options.testing` they\n * answer 404, as if the feature did not exist. The class comes from the route table through the\n * handler Hono matched, so a new route needs no guard of its own, only a table entry. It is mounted\n * before the CSRF and rate-limit guards so a hidden route answers the same 404 for every request. `hosting`,\n * `both` and `review` routes, and requests that match no classified route, pass through.\n */\nexport function requireTesting(enabled: boolean) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (enabled || !isTestingRoute(c)) {\n await next();\n return;\n }\n if (c.req.path.startsWith(\"/api/\")) {\n return c.json({ error: TESTING_DISABLED_MESSAGE }, 404);\n }\n return c.text(TESTING_DISABLED_MESSAGE, 404);\n };\n}\n\n/** The last matched handler that the table classifies (middleware matches come first). */\nexport function matchedRouteClass(c: Context): RouteClass | undefined {\n const routes = matchedRoutes(c);\n for (let index = routes.length - 1; index >= 0; index -= 1) {\n const route = routes[index];\n const routeClass = route && classOfRoute(route.method, route.path);\n if (routeClass !== undefined) {\n return routeClass;\n }\n }\n return undefined;\n}\n\nfunction isTestingRoute(c: Context): boolean {\n return matchedRouteClass(c) === \"testing\";\n}\n"],"mappings":";;;;;;;AAKA,MAAa,2BAA2B;;;;;;;;AASxC,SAAgB,eAAe,SAAkB;CAE/C,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,WAAW,CAAC,eAAe,CAAC,GAAG;GACjC,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,KAAK,WAAW,OAAO,GAC/B,OAAO,EAAE,KAAK,EAAE,OAAO,yBAAyB,GAAG,GAAG;EAExD,OAAO,EAAE,KAAK,0BAA0B,GAAG;CAC7C;AACF;;AAGA,SAAgB,kBAAkB,GAAoC;CACpE,MAAM,SAAS,cAAc,CAAC;CAC9B,KAAK,IAAI,QAAQ,OAAO,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;EAC1D,MAAM,QAAQ,OAAO;EACrB,MAAM,aAAa,SAAS,aAAa,MAAM,QAAQ,MAAM,IAAI;EACjE,IAAI,eAAe,KAAA,GACjB,OAAO;CAEX;AAEF;AAEA,SAAS,eAAe,GAAqB;CAC3C,OAAO,kBAAkB,CAAC,MAAM;AAClC"}
|
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { runWithTimings } from "@storyshelf/core/utils";
|
|
5
5
|
import { setMetric, timing } from "hono/timing";
|
|
6
|
-
|
|
6
|
+
// src/middleware/server-timing.ts
|
|
7
7
|
/**
|
|
8
8
|
* Opt-in `Server-Timing` response headers over Hono's timing middleware.
|
|
9
9
|
*
|
|
@@ -33,7 +33,6 @@ function serverTiming(enabled) {
|
|
|
33
33
|
if (headers.length > 0) c.res.headers.append("Server-Timing", headers.join(","));
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
//#endregion
|
|
37
36
|
export { serverTiming };
|
|
38
37
|
|
|
39
38
|
//# sourceMappingURL=server-timing.mjs.map
|
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { sanitizeErrorText } from "@storyshelf/core/utils";
|
|
5
|
-
|
|
5
|
+
// src/middleware/setup-gate.ts
|
|
6
6
|
/** Failures safe to expose on the setup gate's 503 (redacted, no secrets). */
|
|
7
7
|
function publicFailures(result) {
|
|
8
8
|
return result.failures.map((failure) => ({
|
|
@@ -31,7 +31,6 @@ function setupGate(getReady) {
|
|
|
31
31
|
await next();
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
//#endregion
|
|
35
34
|
export { setupGate };
|
|
36
35
|
|
|
37
36
|
//# sourceMappingURL=setup-gate.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup-gate.mjs","names":[],"sources":["../../src/middleware/setup-gate.ts"],"sourcesContent":["import type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport type {
|
|
1
|
+
{"version":3,"file":"setup-gate.mjs","names":[],"sources":["../../src/middleware/setup-gate.ts"],"sourcesContent":["import type { AdapterSetupResult } from \"@storyshelf/core/adapter/setup\";\nimport type { ShelfOptions } from \"@storyshelf/core/config\";\nimport { sanitizeErrorText } from \"@storyshelf/core/utils\";\nimport type { Context, Next } from \"hono\";\nimport type { JobsWiring } from \"../jobs-setup.ts\";\nimport type { ServerRuntime } from \"../runtime.ts\";\n\nexport interface MiddlewareWiring extends ServerRuntime, Omit<JobsWiring, \"consumer\"> {\n options: ShelfOptions;\n getReady: () => Promise<AdapterSetupResult>;\n}\n\n/** Failures safe to expose on the setup gate's 503 (redacted, no secrets). */\nfunction publicFailures(\n result: AdapterSetupResult,\n): { category: string; kind: string; error: string }[] {\n return result.failures.map((failure) => ({\n category: failure.category,\n kind: failure.kind,\n error: sanitizeErrorText(failure.error, 500),\n }));\n}\n\n/**\n * Block requests until adapter setup settles; answer 503 with the per-adapter\n * failures when setup failed. Health probes and the admin System page are\n * exempt (they report setup state themselves instead of being masked by\n * the gate — both are site-admin-gated where it matters).\n */\nexport function setupGate(getReady: () => Promise<AdapterSetupResult>) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n if (c.req.path === \"/api/v1/health\" || c.req.path === \"/admin\") {\n await next();\n return;\n }\n const result = await getReady();\n if (!result.ok) {\n return c.json({ error: \"Adapters failed setup\", failures: publicFailures(result) }, 503);\n }\n await next();\n };\n}\n"],"mappings":";;;;;;AAaA,SAAS,eACP,QACqD;CACrD,OAAO,OAAO,SAAS,KAAK,aAAa;EACvC,UAAU,QAAQ;EAClB,MAAM,QAAQ;EACd,OAAO,kBAAkB,QAAQ,OAAO,GAAG;CAC7C,EAAE;AACJ;;;;;;;AAQA,SAAgB,UAAU,UAA6C;CAErE,OAAO,OAAO,GAAY,SAAyC;EACjE,IAAI,EAAE,IAAI,SAAS,oBAAoB,EAAE,IAAI,SAAS,UAAU;GAC9D,MAAM,KAAK;GACX;EACF;EACA,MAAM,SAAS,MAAM,SAAS;EAC9B,IAAI,CAAC,OAAO,IACV,OAAO,EAAE,KAAK;GAAE,OAAO;GAAyB,UAAU,eAAe,MAAM;EAAE,GAAG,GAAG;EAEzF,MAAM,KAAK;CACb;AACF"}
|
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { runWithStore } from "../store.mjs";
|
|
5
5
|
import { sessionIdFrom } from "./csrf.mjs";
|
|
6
|
-
|
|
6
|
+
// src/middleware/store-scope.ts
|
|
7
7
|
/** Hono middleware running downstream handlers inside the request store. */
|
|
8
8
|
function storeScope(deps) {
|
|
9
9
|
return async (c, next) => {
|
|
@@ -17,17 +17,26 @@ function storeScope(deps) {
|
|
|
17
17
|
logger: deps.logger,
|
|
18
18
|
user,
|
|
19
19
|
authEnabled: deps.authEnabled,
|
|
20
|
+
testing: deps.testing,
|
|
20
21
|
sessionId: sessionIdFrom(c),
|
|
21
22
|
enqueueCapture: deps.enqueueCapture,
|
|
22
23
|
captureQueue: deps.captureQueue,
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
capturesHere: deps.capturesHere,
|
|
25
|
+
publish: deps.publish,
|
|
26
|
+
cancelBuildJobs: deps.cancelBuildJobs,
|
|
27
|
+
buildState: deps.buildState,
|
|
28
|
+
captureConcurrency: deps.captureConcurrency,
|
|
29
|
+
runner: deps.runner,
|
|
30
|
+
publicOrigin: deps.publicOrigin,
|
|
31
|
+
design: deps.design,
|
|
32
|
+
notifiers: deps.notifiers,
|
|
33
|
+
notificationsConfig: deps.notificationsConfig,
|
|
34
|
+
ai: deps.ai
|
|
25
35
|
}, async () => {
|
|
26
36
|
await next();
|
|
27
37
|
});
|
|
28
38
|
};
|
|
29
39
|
}
|
|
30
|
-
//#endregion
|
|
31
40
|
export { storeScope };
|
|
32
41
|
|
|
33
42
|
//# sourceMappingURL=store-scope.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-scope.mjs","names":[],"sources":["../../src/middleware/store-scope.ts"],"sourcesContent":["import type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type {
|
|
1
|
+
{"version":3,"file":"store-scope.mjs","names":[],"sources":["../../src/middleware/store-scope.ts"],"sourcesContent":["import type { CaptureQueue } from \"@storyshelf/core/adapter/capture-queue\";\nimport type { CaptureRunner } from \"@storyshelf/core/adapter/capture-runner\";\nimport type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { DesignProvider } from \"@storyshelf/core/adapter/design\";\nimport type { NotifierProvider } from \"@storyshelf/core/adapter/notifier\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { Ai } from \"@storyshelf/core/ai\";\nimport type { BuildStatePublisher } from \"@storyshelf/core/capture\";\nimport type { NotificationsConfig, ShelfConfig, UIConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { PublicOriginResolver } from \"@storyshelf/core/models\";\nimport type { AuthUser } from \"@storyshelf/core/types\";\nimport type { Context, Next } from \"hono\";\nimport type { PublishService } from \"../publish-setup.ts\";\nimport { runWithStore } from \"../store.ts\";\nimport { sessionIdFrom } from \"./csrf.ts\";\n\n/** Dependencies scoped into the request store for router handlers. */\nexport interface StoreScopeDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n authEnabled: boolean;\n testing: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: CaptureQueue | null;\n /** This instance runs the capture jobs itself (the retry and compute-jobs UI applies). */\n capturesHere?: boolean;\n publish?: PublishService;\n cancelBuildJobs?: (buildId: string) => Promise<boolean>;\n buildState?: BuildStatePublisher;\n captureConcurrency?: number;\n runner?: Pick<CaptureRunner, \"metadata\"> | undefined;\n publicOrigin?: PublicOriginResolver | undefined;\n design: DesignProvider[];\n notifiers: NotifierProvider[];\n notificationsConfig?: NotificationsConfig;\n ai?: Ai;\n resolveUser: (c: Context) => Promise<AuthUser | null>;\n}\n\n/** Hono middleware running downstream handlers inside the request store. */\nexport function storeScope(deps: StoreScopeDeps) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const user = await deps.resolveUser(c);\n c.set(\"userId\", user?.id ?? null);\n await runWithStore(\n {\n db: deps.db,\n storage: deps.storage,\n config: deps.config,\n ui: deps.ui,\n logger: deps.logger,\n user,\n authEnabled: deps.authEnabled,\n testing: deps.testing,\n sessionId: sessionIdFrom(c),\n enqueueCapture: deps.enqueueCapture,\n captureQueue: deps.captureQueue,\n capturesHere: deps.capturesHere,\n publish: deps.publish,\n cancelBuildJobs: deps.cancelBuildJobs,\n buildState: deps.buildState,\n captureConcurrency: deps.captureConcurrency,\n runner: deps.runner,\n publicOrigin: deps.publicOrigin,\n design: deps.design,\n notifiers: deps.notifiers,\n notificationsConfig: deps.notificationsConfig,\n ai: deps.ai,\n },\n async () => {\n await next();\n },\n );\n };\n}\n"],"mappings":";;;;;;;AA4CA,SAAgB,WAAW,MAAsB;CAE/C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,OAAO,MAAM,KAAK,YAAY,CAAC;EACrC,EAAE,IAAI,UAAU,MAAM,MAAM,IAAI;EAChC,MAAM,aACJ;GACE,IAAI,KAAK;GACT,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,IAAI,KAAK;GACT,QAAQ,KAAK;GACb;GACA,aAAa,KAAK;GAClB,SAAS,KAAK;GACd,WAAW,cAAc,CAAC;GAC1B,gBAAgB,KAAK;GACrB,cAAc,KAAK;GACnB,cAAc,KAAK;GACnB,SAAS,KAAK;GACd,iBAAiB,KAAK;GACtB,YAAY,KAAK;GACjB,oBAAoB,KAAK;GACzB,QAAQ,KAAK;GACb,cAAc,KAAK;GACnB,QAAQ,KAAK;GACb,WAAW,KAAK;GAChB,qBAAqB,KAAK;GAC1B,IAAI,KAAK;EACX,GACA,YAAY;GACV,MAAM,KAAK;EACb,CACF;CACF;AACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "./store.mjs";
|
|
5
|
+
// src/notifications-pause.ts
|
|
6
|
+
/** What the System page and the notification settings tell the viewer. */
|
|
7
|
+
const NOTIFICATIONS_PAUSED_TEXT = "Notifications are paused until a public URL is set; set PUBLIC_BASE_URL or confirm the detected origin.";
|
|
8
|
+
/** The origin notifications link to, or `undefined` while none is resolved. */
|
|
9
|
+
async function notificationOrigin(deps) {
|
|
10
|
+
const trimmed = (deps.publicOrigin ? await deps.publicOrigin.url() : deps.config.publicBaseUrl)?.trim().replace(/\/+$/u, "");
|
|
11
|
+
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
12
|
+
}
|
|
13
|
+
/** Whether the current instance sends no notifications for want of a public origin. */
|
|
14
|
+
async function notificationsPaused(store = getStore()) {
|
|
15
|
+
return await notificationOrigin(store) === void 0;
|
|
16
|
+
}
|
|
17
|
+
/** Loggers that already said so; cleared when an origin resolves, so a later pause logs again. */
|
|
18
|
+
const warned = /* @__PURE__ */ new WeakSet();
|
|
19
|
+
/** Log (once per pause, per logger) that a notification was skipped. */
|
|
20
|
+
function warnPaused(logger, event) {
|
|
21
|
+
if (!warned.has(logger)) {
|
|
22
|
+
warned.add(logger);
|
|
23
|
+
logger.warn({ event }, `${NOTIFICATIONS_PAUSED_TEXT} Skipping notifications.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Forget a logged pause: an origin resolved. */
|
|
27
|
+
function clearPaused(logger) {
|
|
28
|
+
warned.delete(logger);
|
|
29
|
+
}
|
|
30
|
+
export { NOTIFICATIONS_PAUSED_TEXT, clearPaused, notificationOrigin, notificationsPaused, warnPaused };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=notifications-pause.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications-pause.mjs","names":[],"sources":["../src/notifications-pause.ts"],"sourcesContent":["import type { ShelfConfig } from \"@storyshelf/core/config\";\n/**\n * Notifications need a public origin (ADR 0014, decision 11): every message carries a link and\n * every git status targets one, so while the resolver (ADR 0007, decision 6) finds no confirmed\n * origin (a detected one counts only once an admin confirmed it) nothing is sent. Pausing is not a failure: no channel is marked as errored, and the System page\n * and the notification settings say what to set.\n */\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport type { PublicOriginResolver } from \"@storyshelf/core/models\";\nimport { getStore, type Store } from \"./store.ts\";\n\n/** What the System page and the notification settings tell the viewer. */\nexport const NOTIFICATIONS_PAUSED_TEXT =\n \"Notifications are paused until a public URL is set; set PUBLIC_BASE_URL or confirm the detected origin.\";\n\n/** What a pause check reads. */\ninterface OriginDeps {\n config: Pick<ShelfConfig, \"publicBaseUrl\">;\n /** The resolver; absent (hand-built stores) means `config.publicBaseUrl` only. */\n publicOrigin?: Pick<PublicOriginResolver, \"url\"> | undefined;\n}\n\n/** The origin notifications link to, or `undefined` while none is resolved. */\nexport async function notificationOrigin(deps: OriginDeps): Promise<string | undefined> {\n const origin = deps.publicOrigin ? await deps.publicOrigin.url() : deps.config.publicBaseUrl;\n const trimmed = origin?.trim().replace(/\\/+$/u, \"\");\n return trimmed === undefined || trimmed === \"\" ? undefined : trimmed;\n}\n\n/** Whether the current instance sends no notifications for want of a public origin. */\nexport async function notificationsPaused(store: Store = getStore()): Promise<boolean> {\n return (await notificationOrigin(store)) === undefined;\n}\n\n/** Loggers that already said so; cleared when an origin resolves, so a later pause logs again. */\nconst warned = new WeakSet<object>();\n\n/** Log (once per pause, per logger) that a notification was skipped. */\nexport function warnPaused(logger: Logger, event: string): void {\n if (!warned.has(logger)) {\n warned.add(logger);\n logger.warn({ event }, `${NOTIFICATIONS_PAUSED_TEXT} Skipping notifications.`);\n }\n}\n\n/** Forget a logged pause: an origin resolved. */\nexport function clearPaused(logger: Logger): void {\n warned.delete(logger);\n}\n"],"mappings":";;;;;;AAYA,MAAa,4BACX;;AAUF,eAAsB,mBAAmB,MAA+C;CAEtF,MAAM,WADS,KAAK,eAAe,MAAM,KAAK,aAAa,IAAI,IAAI,KAAK,OAAO,cAAA,EACvD,KAAK,CAAC,CAAC,QAAQ,SAAS,EAAE;CAClD,OAAO,YAAY,KAAA,KAAa,YAAY,KAAK,KAAA,IAAY;AAC/D;;AAGA,eAAsB,oBAAoB,QAAe,SAAS,GAAqB;CACrF,OAAQ,MAAM,mBAAmB,KAAK,MAAO,KAAA;AAC/C;;AAGA,MAAM,yBAAS,IAAI,QAAgB;;AAGnC,SAAgB,WAAW,QAAgB,OAAqB;CAC9D,IAAI,CAAC,OAAO,IAAI,MAAM,GAAG;EACvB,OAAO,IAAI,MAAM;EACjB,OAAO,KAAK,EAAE,MAAM,GAAG,GAAG,0BAA0B,yBAAyB;CAC/E;AACF;;AAGA,SAAgB,YAAY,QAAsB;CAChD,OAAO,OAAO,MAAM;AACtB"}
|
package/dist/notify.d.mts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import __tsdown_shims_path from 'node:path';
|
|
2
2
|
import __tsdown_shims_url from 'node:url';
|
|
3
|
-
import { NotifierProvider } from "@storyshelf/core/adapter/notifier";
|
|
4
3
|
import { ShelfConfig, UIConfig } from "@storyshelf/core/config";
|
|
5
|
-
import
|
|
4
|
+
import "@storyshelf/core/models";
|
|
5
|
+
import { NotifierProvider } from "@storyshelf/core/adapter/notifier";
|
|
6
6
|
import { DatabaseAdapter } from "@storyshelf/core/adapter/database";
|
|
7
|
+
import { Logger } from "@storyshelf/core/logger";
|
|
7
8
|
import "@storyshelf/core/schema";
|
|
8
|
-
//#region src/notify.d.ts
|
|
9
9
|
/**
|
|
10
10
|
* Host hook for the auth engine (`onAuthSystemEvent`): fans `sys:*` auth
|
|
11
11
|
* events out to admin channels. Safe to call anywhere — outside a request
|
|
12
12
|
* scope (e.g. boot) it resolves to a no-op instead of throwing.
|
|
13
13
|
*/
|
|
14
14
|
declare function createAuthSystemHook(): (event: "sys:user-created" | "sys:invite-issued", data: Record<string, unknown>) => Promise<void>;
|
|
15
|
-
//#endregion
|
|
16
15
|
export { createAuthSystemHook };
|
|
17
16
|
//# sourceMappingURL=notify.d.mts.map
|