@storyshelf/app 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +13 -5
- package/dist/asset-manifest.mjs.map +1 -1
- package/dist/assets/{htmx.min.js → htmx.mjs} +10 -1
- package/dist/assets/htmx.mjs.map +1 -0
- package/dist/assets/mark.mjs +10 -0
- package/dist/assets/mark.mjs.map +1 -0
- package/dist/boot-tasks.mjs +80 -0
- package/dist/boot-tasks.mjs.map +1 -0
- package/dist/build-state-flags.mjs +18 -0
- package/dist/build-state-flags.mjs.map +1 -0
- package/dist/build-state-setup.mjs +75 -0
- package/dist/build-state-setup.mjs.map +1 -0
- package/dist/capture-alerts.mjs +46 -0
- package/dist/capture-alerts.mjs.map +1 -0
- package/dist/capture-process.mjs +59 -0
- package/dist/capture-process.mjs.map +1 -0
- package/dist/capture-setup.mjs +95 -117
- package/dist/capture-setup.mjs.map +1 -1
- package/dist/connections.mjs +34 -0
- package/dist/connections.mjs.map +1 -0
- package/dist/consumer-setup.mjs +58 -0
- package/dist/consumer-setup.mjs.map +1 -0
- package/dist/git-status.mjs +46 -0
- package/dist/git-status.mjs.map +1 -0
- package/dist/hidden-data.mjs +24 -0
- package/dist/hidden-data.mjs.map +1 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +203 -125
- package/dist/index.mjs.map +1 -1
- package/dist/insights/budget.mjs +49 -0
- package/dist/insights/budget.mjs.map +1 -0
- package/dist/insights/deps.mjs +21 -0
- package/dist/insights/deps.mjs.map +1 -0
- package/dist/insights/errors.mjs +22 -0
- package/dist/insights/errors.mjs.map +1 -0
- package/dist/insights/health-panel.mjs +40 -0
- package/dist/insights/health-panel.mjs.map +1 -0
- package/dist/insights/job.mjs +103 -0
- package/dist/insights/job.mjs.map +1 -0
- package/dist/insights/load-health.mjs +59 -0
- package/dist/insights/load-health.mjs.map +1 -0
- package/dist/insights/load-triage.mjs +98 -0
- package/dist/insights/load-triage.mjs.map +1 -0
- package/dist/insights/panel.mjs +28 -0
- package/dist/insights/panel.mjs.map +1 -0
- package/dist/insights/request.mjs +113 -0
- package/dist/insights/request.mjs.map +1 -0
- package/dist/insights/stale.mjs +21 -0
- package/dist/insights/stale.mjs.map +1 -0
- package/dist/insights/usage-report.mjs +60 -0
- package/dist/insights/usage-report.mjs.map +1 -0
- package/dist/insights/view.mjs +48 -0
- package/dist/insights/view.mjs.map +1 -0
- package/dist/job-canceller.mjs +14 -0
- package/dist/job-canceller.mjs.map +1 -0
- package/dist/job-queue.mjs +51 -0
- package/dist/job-queue.mjs.map +1 -0
- package/dist/jobs-setup.mjs +132 -0
- package/dist/jobs-setup.mjs.map +1 -0
- package/dist/lifecycle.mjs +91 -19
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +4 -2
- package/dist/middleware/auth-gate.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +41 -33
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +3 -2
- package/dist/middleware/request-log.mjs.map +1 -1
- package/dist/middleware/require-review.mjs +51 -0
- package/dist/middleware/require-review.mjs.map +1 -0
- package/dist/middleware/require-testing.mjs +41 -0
- package/dist/middleware/require-testing.mjs.map +1 -0
- package/dist/middleware/setup-gate.mjs.map +1 -1
- package/dist/middleware/store-scope.mjs +12 -2
- package/dist/middleware/store-scope.mjs.map +1 -1
- package/dist/notifications-pause.mjs +33 -0
- package/dist/notifications-pause.mjs.map +1 -0
- package/dist/notify.d.mts +2 -1
- package/dist/notify.mjs +78 -39
- package/dist/notify.mjs.map +1 -1
- package/dist/pages/a11y-violations.mjs +116 -0
- package/dist/pages/a11y-violations.mjs.map +1 -0
- package/dist/pages/admin-ai.mjs +78 -0
- package/dist/pages/admin-ai.mjs.map +1 -0
- package/dist/pages/admin-system-origin.mjs +123 -0
- package/dist/pages/admin-system-origin.mjs.map +1 -0
- package/dist/pages/admin-system-targets.mjs +53 -0
- package/dist/pages/admin-system-targets.mjs.map +1 -0
- package/dist/pages/admin-system.mjs +63 -13
- package/dist/pages/admin-system.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +104 -76
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-actions.mjs +87 -33
- package/dist/pages/build-diff-actions.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +16 -14
- package/dist/pages/build-diff-comments.mjs.map +1 -1
- package/dist/pages/build-diff-environment.mjs +82 -0
- package/dist/pages/build-diff-environment.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +22 -4
- package/dist/pages/build-diff-header.mjs.map +1 -1
- package/dist/pages/build-diff-insight.mjs +113 -0
- package/dist/pages/build-diff-insight.mjs.map +1 -0
- package/dist/pages/build-diff-model.mjs +55 -10
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +28 -12
- package/dist/pages/build-diff-nav.mjs.map +1 -1
- package/dist/pages/build-diff-shortcuts.mjs +5 -1
- package/dist/pages/build-diff-shortcuts.mjs.map +1 -1
- package/dist/pages/build-diff-usage.mjs +63 -0
- package/dist/pages/build-diff-usage.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +62 -5
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +34 -13
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +74 -0
- package/dist/pages/build-tests.mjs.map +1 -0
- package/dist/pages/component-detail.mjs +249 -0
- package/dist/pages/component-detail.mjs.map +1 -0
- package/dist/pages/component-stories.mjs +168 -0
- package/dist/pages/component-stories.mjs.map +1 -0
- package/dist/pages/compute-jobs.mjs +13 -26
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/design-links.mjs +182 -0
- package/dist/pages/design-links.mjs.map +1 -0
- package/dist/pages/environment-change.mjs +53 -0
- package/dist/pages/environment-change.mjs.map +1 -0
- package/dist/pages/health-panel.mjs +126 -0
- package/dist/pages/health-panel.mjs.map +1 -0
- package/dist/pages/library-grid.mjs +105 -0
- package/dist/pages/library-grid.mjs.map +1 -0
- package/dist/pages/library-model.mjs +47 -0
- package/dist/pages/library-model.mjs.map +1 -0
- package/dist/pages/library-snapshots.mjs +130 -0
- package/dist/pages/library-snapshots.mjs.map +1 -0
- package/dist/pages/library.mjs +112 -218
- package/dist/pages/library.mjs.map +1 -1
- package/dist/pages/notifications-paused.mjs +22 -0
- package/dist/pages/notifications-paused.mjs.map +1 -0
- package/dist/pages/profile.mjs +53 -48
- package/dist/pages/profile.mjs.map +1 -1
- package/dist/pages/project-builds-table.mjs +2 -2
- package/dist/pages/project-builds-table.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +51 -21
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +51 -12
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects-card.mjs +14 -1
- package/dist/pages/projects-card.mjs.map +1 -1
- package/dist/pages/projects.mjs +6 -2
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/review-changeset.mjs +116 -0
- package/dist/pages/review-changeset.mjs.map +1 -0
- package/dist/pages/review-comments.mjs +103 -0
- package/dist/pages/review-comments.mjs.map +1 -0
- package/dist/pages/review-verdict.mjs +102 -0
- package/dist/pages/review-verdict.mjs.map +1 -0
- package/dist/pages/settings-ai.mjs +69 -0
- package/dist/pages/settings-ai.mjs.map +1 -0
- package/dist/pages/settings-capture.mjs +124 -0
- package/dist/pages/settings-capture.mjs.map +1 -0
- package/dist/pages/settings-design-credential.mjs +99 -0
- package/dist/pages/settings-design-credential.mjs.map +1 -0
- package/dist/pages/settings-design-links.mjs +179 -0
- package/dist/pages/settings-design-links.mjs.map +1 -0
- package/dist/pages/settings-design.mjs +48 -0
- package/dist/pages/settings-design.mjs.map +1 -0
- package/dist/pages/settings-general.mjs +46 -21
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +43 -2
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-links.mjs +112 -0
- package/dist/pages/settings-links.mjs.map +1 -0
- package/dist/pages/settings-notifications-git.mjs +128 -0
- package/dist/pages/settings-notifications-git.mjs.map +1 -0
- package/dist/pages/settings-notifications.mjs +79 -42
- package/dist/pages/settings-notifications.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +33 -2
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/target-choices.mjs +39 -0
- package/dist/pages/target-choices.mjs.map +1 -0
- package/dist/publish-setup.mjs +89 -0
- package/dist/publish-setup.mjs.map +1 -0
- package/dist/retention-timer.mjs +2 -1
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +48 -0
- package/dist/review-events.mjs.map +1 -0
- package/dist/routers/admin-pages.mjs +45 -3
- package/dist/routers/admin-pages.mjs.map +1 -1
- package/dist/routers/admin-public-origin.mjs +85 -0
- package/dist/routers/admin-public-origin.mjs.map +1 -0
- package/dist/routers/assets.mjs +9 -2
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -1
- package/dist/routers/baseline-guard.mjs +2 -2
- package/dist/routers/baseline-guard.mjs.map +1 -1
- package/dist/routers/baselines.handlers.mjs +64 -0
- package/dist/routers/baselines.handlers.mjs.map +1 -0
- package/dist/routers/baselines.mjs +125 -0
- package/dist/routers/baselines.mjs.map +1 -0
- package/dist/routers/build-cancel.mjs +68 -0
- package/dist/routers/build-cancel.mjs.map +1 -0
- package/dist/routers/build-changeset.mjs +70 -0
- package/dist/routers/build-changeset.mjs.map +1 -0
- package/dist/routers/build-reviews.handlers.mjs +86 -0
- package/dist/routers/build-reviews.handlers.mjs.map +1 -0
- package/dist/routers/build-reviews.mjs +74 -0
- package/dist/routers/build-reviews.mjs.map +1 -0
- package/dist/routers/builds.handlers.mjs +46 -55
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +71 -17
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/capture-config.mjs +35 -0
- package/dist/routers/capture-config.mjs.map +1 -0
- package/dist/routers/comments.mjs +41 -15
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/component-catalog.handlers.mjs +71 -0
- package/dist/routers/component-catalog.handlers.mjs.map +1 -0
- package/dist/routers/component.handlers.mjs +121 -0
- package/dist/routers/component.handlers.mjs.map +1 -0
- package/dist/routers/connections.mjs +118 -0
- package/dist/routers/connections.mjs.map +1 -0
- package/dist/routers/design-connect.handlers.mjs +44 -0
- package/dist/routers/design-connect.handlers.mjs.map +1 -0
- package/dist/routers/design.handlers.mjs +114 -0
- package/dist/routers/design.handlers.mjs.map +1 -0
- package/dist/routers/health-report.mjs +2 -3
- package/dist/routers/health-report.mjs.map +1 -1
- package/dist/routers/health.mjs +4 -3
- package/dist/routers/health.mjs.map +1 -1
- package/dist/routers/helpers.mjs +19 -3
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/insights-ui.mjs +64 -0
- package/dist/routers/insights-ui.mjs.map +1 -0
- package/dist/routers/insights.handlers.mjs +45 -0
- package/dist/routers/insights.handlers.mjs.map +1 -0
- package/dist/routers/insights.mjs +189 -0
- package/dist/routers/insights.mjs.map +1 -0
- package/dist/routers/links.handlers.mjs +80 -0
- package/dist/routers/links.handlers.mjs.map +1 -0
- package/dist/routers/links.mjs +139 -0
- package/dist/routers/links.mjs.map +1 -0
- package/dist/routers/media.mjs +18 -4
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +2 -1
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/notification-channels.mjs +17 -23
- package/dist/routers/notification-channels.mjs.map +1 -1
- package/dist/routers/oauth-state.mjs +45 -0
- package/dist/routers/oauth-state.mjs.map +1 -0
- package/dist/routers/preview-base.mjs +38 -0
- package/dist/routers/preview-base.mjs.map +1 -0
- package/dist/routers/preview-token.mjs +51 -0
- package/dist/routers/preview-token.mjs.map +1 -0
- package/dist/routers/profile.mjs +3 -1
- package/dist/routers/profile.mjs.map +1 -1
- package/dist/routers/projects.mjs +17 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/publish-upload.mjs +78 -0
- package/dist/routers/publish-upload.mjs.map +1 -0
- package/dist/routers/published-access.mjs +52 -0
- package/dist/routers/published-access.mjs.map +1 -0
- package/dist/routers/published-files.mjs +52 -0
- package/dist/routers/published-files.mjs.map +1 -0
- package/dist/routers/review-forms.mjs +72 -0
- package/dist/routers/review-forms.mjs.map +1 -0
- package/dist/routers/review-section.handlers.mjs +85 -0
- package/dist/routers/review-section.handlers.mjs.map +1 -0
- package/dist/routers/review-switch-off.mjs +29 -0
- package/dist/routers/review-switch-off.mjs.map +1 -0
- package/dist/routers/schemas.mjs +140 -22
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/scoped-models.mjs +22 -0
- package/dist/routers/scoped-models.mjs.map +1 -0
- package/dist/routers/settings-ai.mjs +35 -0
- package/dist/routers/settings-ai.mjs.map +1 -0
- package/dist/routers/settings-capture.mjs +77 -0
- package/dist/routers/settings-capture.mjs.map +1 -0
- package/dist/routers/settings-design.mjs +103 -0
- package/dist/routers/settings-design.mjs.map +1 -0
- package/dist/routers/settings-general.mjs +55 -10
- package/dist/routers/settings-general.mjs.map +1 -1
- package/dist/routers/settings-git-channel.mjs +77 -0
- package/dist/routers/settings-git-channel.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +27 -1
- package/dist/routers/settings-labels.mjs.map +1 -1
- package/dist/routers/settings-links.mjs +63 -0
- package/dist/routers/settings-links.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +4 -7
- package/dist/routers/settings-members.mjs.map +1 -1
- package/dist/routers/settings-notifications.mjs +15 -10
- package/dist/routers/settings-notifications.mjs.map +1 -1
- package/dist/routers/settings-tokens.mjs +2 -5
- package/dist/routers/settings-tokens.mjs.map +1 -1
- package/dist/routers/settings-webhooks.mjs +7 -24
- package/dist/routers/settings-webhooks.mjs.map +1 -1
- package/dist/routers/settings.handlers.mjs +107 -26
- package/dist/routers/settings.handlers.mjs.map +1 -1
- package/dist/routers/settings.mjs +16 -4
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/signed-storybook.mjs +53 -0
- package/dist/routers/signed-storybook.mjs.map +1 -0
- package/dist/routers/snapshot-groups.mjs +63 -0
- package/dist/routers/snapshot-groups.mjs.map +1 -0
- package/dist/routers/snapshots.mjs +103 -5
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/storybook.mjs +37 -88
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -5
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +34 -5
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +5 -11
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/routers/well-known.mjs +11 -4
- package/dist/routers/well-known.mjs.map +1 -1
- package/dist/routes/api-routes.mjs +93 -0
- package/dist/routes/api-routes.mjs.map +1 -0
- package/dist/routes/classification.mjs +38 -0
- package/dist/routes/classification.mjs.map +1 -0
- package/dist/routes/openapi-classes.mjs +22 -0
- package/dist/routes/openapi-classes.mjs.map +1 -0
- package/dist/routes/page-routes.mjs +104 -0
- package/dist/routes/page-routes.mjs.map +1 -0
- package/dist/runtime.mjs +18 -13
- package/dist/runtime.mjs.map +1 -1
- package/dist/serverless.mjs +5 -0
- package/dist/store.mjs +19 -1
- package/dist/store.mjs.map +1 -1
- package/dist/targets.mjs +46 -0
- package/dist/targets.mjs.map +1 -0
- package/dist/ui/buttons.mjs +16 -3
- package/dist/ui/buttons.mjs.map +1 -1
- package/dist/ui/design-reference.mjs +97 -0
- package/dist/ui/design-reference.mjs.map +1 -0
- package/dist/ui/document.mjs +1 -2
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/forms.mjs +6 -5
- package/dist/ui/forms.mjs.map +1 -1
- package/dist/ui/icons/paths.mjs +1 -0
- package/dist/ui/icons/paths.mjs.map +1 -1
- package/dist/ui/preview-embed.mjs +82 -0
- package/dist/ui/preview-embed.mjs.map +1 -0
- package/dist/ui/shell/client-script.mjs +85 -0
- package/dist/ui/shell/client-script.mjs.map +1 -1
- package/dist/ui/shell/nav.mjs +12 -8
- package/dist/ui/shell/nav.mjs.map +1 -1
- package/dist/ui/shell/review-script.mjs +8 -0
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +2 -2
- package/dist/ui/shell/sidebar.mjs.map +1 -1
- package/package.json +11 -9
- package/dist/asset-paths.mjs +0 -23
- package/dist/asset-paths.mjs.map +0 -1
- package/dist/assets/mark.svg +0 -1
- package/dist/pages/settings-status.mjs +0 -137
- package/dist/pages/settings-status.mjs.map +0 -1
- package/dist/routers/settings-status.mjs +0 -77
- package/dist/routers/settings-status.mjs.map +0 -1
- package/dist/routers/status-configs.mjs +0 -108
- package/dist/routers/status-configs.mjs.map +0 -1
|
@@ -3,13 +3,15 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
5
|
import { notifyProject } from "../notify.mjs";
|
|
6
|
+
import { REVIEW_EVENTS, announceReview } from "../review-events.mjs";
|
|
7
|
+
import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
|
|
6
8
|
import { hxRefresh } from "./htmx.mjs";
|
|
7
9
|
import { flashHx } from "./flash.mjs";
|
|
8
|
-
import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
|
|
9
10
|
import { commentCreateSchema, commentSchema, notFound as notFound$1, unauthorized } from "./schemas.mjs";
|
|
10
11
|
import { DEVELOPER_ROLES, VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
|
|
11
12
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
12
13
|
import { CommentModel } from "@storyshelf/core/models";
|
|
14
|
+
import { HTTPException } from "hono/http-exception";
|
|
13
15
|
//#region src/routers/comments.ts
|
|
14
16
|
const listCommentsRoute = createRoute({
|
|
15
17
|
method: "get",
|
|
@@ -78,16 +80,7 @@ function registerComments(app) {
|
|
|
78
80
|
app.openapi(createCommentRoute, async (c) => {
|
|
79
81
|
const { slug, buildId } = c.req.valid("param");
|
|
80
82
|
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
81
|
-
const
|
|
82
|
-
const body = c.req.valid("json");
|
|
83
|
-
const userId = getStore().user?.id ?? null;
|
|
84
|
-
const comment = await new CommentModel(getStore().db).create(project.id, build.id, userId, body);
|
|
85
|
-
await notifyProject(project, "comment:created", {
|
|
86
|
-
buildId: build.id,
|
|
87
|
-
commentId: comment.id,
|
|
88
|
-
snapshotId: comment.snapshotId,
|
|
89
|
-
body: comment.body.slice(0, 500)
|
|
90
|
-
}, `/builds/${build.id}`);
|
|
83
|
+
const comment = await createComment(project, (await buildForProject(project.id, buildId)).id, c.req.valid("json"));
|
|
91
84
|
hxRefresh(c);
|
|
92
85
|
flashHx(c, "Comment added");
|
|
93
86
|
return c.json(comment, 201);
|
|
@@ -95,15 +88,48 @@ function registerComments(app) {
|
|
|
95
88
|
app.openapi(resolveCommentRoute, async (c) => {
|
|
96
89
|
const { slug, buildId, commentId } = c.req.valid("param");
|
|
97
90
|
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
98
|
-
const
|
|
99
|
-
const comment = await new CommentModel(getStore().db).resolve(commentId);
|
|
100
|
-
if (comment.buildId !== build.id) notFound("Comment not found");
|
|
91
|
+
const comment = await resolveBuildComment((await buildForProject(project.id, buildId)).id, commentId);
|
|
101
92
|
hxRefresh(c);
|
|
102
93
|
flashHx(c, "Comment resolved");
|
|
103
94
|
return c.json(comment);
|
|
104
95
|
});
|
|
105
96
|
}
|
|
97
|
+
/** Mark a comment of `buildId` resolved; 404 for a comment of another build. */
|
|
98
|
+
async function resolveBuildComment(buildId, commentId) {
|
|
99
|
+
const comments = new CommentModel(getStore().db);
|
|
100
|
+
if (!(await comments.listByBuild(buildId)).find((item) => item.id === commentId)) notFound("Comment not found");
|
|
101
|
+
return await comments.resolve(commentId);
|
|
102
|
+
}
|
|
103
|
+
/** A comment anchors to the build, a story or a snapshot (ADR 0013, decision 2), never two. */
|
|
104
|
+
function assertOneAnchor(body) {
|
|
105
|
+
if (body.snapshotId !== void 0 && body.storyId !== void 0) throw new HTTPException(400, { message: "A comment anchors to the build, a story or a snapshot, not several" });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Snapshot comments keep `comment:created`; build and story comments, the review ones, raise
|
|
109
|
+
* `review:comment` (webhook and notifications, ADR 0013, decision 10). One event per comment.
|
|
110
|
+
*/
|
|
111
|
+
async function announceComment(project, buildId, comment) {
|
|
112
|
+
const data = {
|
|
113
|
+
buildId,
|
|
114
|
+
commentId: comment.id,
|
|
115
|
+
snapshotId: comment.snapshotId,
|
|
116
|
+
storyId: comment.storyId,
|
|
117
|
+
body: comment.body.slice(0, 500)
|
|
118
|
+
};
|
|
119
|
+
if (comment.snapshotId === null) {
|
|
120
|
+
await announceReview(project, REVIEW_EVENTS.comment, data, buildId);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await notifyProject(project, "comment:created", data, `/builds/${buildId}`);
|
|
124
|
+
}
|
|
125
|
+
async function createComment(project, buildId, body) {
|
|
126
|
+
assertOneAnchor(body);
|
|
127
|
+
const userId = getStore().user?.id ?? null;
|
|
128
|
+
const comment = await new CommentModel(getStore().db).create(project.id, buildId, userId, body);
|
|
129
|
+
await announceComment(project, buildId, comment);
|
|
130
|
+
return comment;
|
|
131
|
+
}
|
|
106
132
|
//#endregion
|
|
107
|
-
export { registerComments };
|
|
133
|
+
export { createComment, registerComments, resolveBuildComment };
|
|
108
134
|
|
|
109
135
|
//# sourceMappingURL=comments.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comments.mjs","names":["notFound"],"sources":["../../src/routers/comments.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { CommentModel } from \"@storyshelf/core/models\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, buildForProject } from \"./builds.handlers.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject, notFound as throwNotFound } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport { commentSchema, commentCreateSchema, notFound, unauthorized } from \"./schemas.ts\";\nconst listCommentsRoute = createRoute({\n method: \"get\",\n tags: [\"Review\"],\n summary: \"List comments\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: commentSchema.array() } },\n description: \"List comments on a build\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createCommentRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Add a comment\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: commentCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: commentSchema } },\n description: \"Comment created\",\n },\n ...notFound,\n },\n});\n\nconst resolveCommentRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Resolve a comment\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments/{commentId}/resolve\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), commentId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: commentSchema } },\n description: \"Comment resolved\",\n },\n ...notFound,\n },\n});\n\n/** Register the build comment list, create, and resolve endpoints. */\nexport function registerComments(app: ShelfRouter): void {\n app.openapi(listCommentsRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(await new CommentModel(getStore().db).listByBuild(build.id));\n });\n\n app.openapi(createCommentRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const
|
|
1
|
+
{"version":3,"file":"comments.mjs","names":["notFound"],"sources":["../../src/routers/comments.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { CommentModel, type CommentCreateInput } from \"@storyshelf/core/models\";\nimport type { Comment, Project } from \"@storyshelf/core/schema\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { notifyProject } from \"../notify.ts\";\nimport { REVIEW_EVENTS, announceReview } from \"../review-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, buildForProject } from \"./builds.handlers.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject, notFound as throwNotFound } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport { commentSchema, commentCreateSchema, notFound, unauthorized } from \"./schemas.ts\";\nconst listCommentsRoute = createRoute({\n method: \"get\",\n tags: [\"Review\"],\n summary: \"List comments\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: commentSchema.array() } },\n description: \"List comments on a build\",\n },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createCommentRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Add a comment\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: commentCreateSchema } } },\n },\n responses: {\n 201: {\n content: { \"application/json\": { schema: commentSchema } },\n description: \"Comment created\",\n },\n ...notFound,\n },\n});\n\nconst resolveCommentRoute = createRoute({\n method: \"post\",\n tags: [\"Review\"],\n summary: \"Resolve a comment\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/comments/{commentId}/resolve\",\n request: { params: z.object({ slug: z.string(), buildId: z.string(), commentId: z.string() }) },\n responses: {\n 200: {\n content: { \"application/json\": { schema: commentSchema } },\n description: \"Comment resolved\",\n },\n ...notFound,\n },\n});\n\n/** Register the build comment list, create, and resolve endpoints. */\nexport function registerComments(app: ShelfRouter): void {\n app.openapi(listCommentsRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(await new CommentModel(getStore().db).listByBuild(build.id));\n });\n\n app.openapi(createCommentRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const comment = await createComment(project, build.id, c.req.valid(\"json\"));\n hxRefresh(c);\n flashHx(c, \"Comment added\");\n return c.json(comment, 201);\n });\n\n app.openapi(resolveCommentRoute, async (c) => {\n const { slug, buildId, commentId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const comment = await resolveBuildComment(build.id, commentId);\n hxRefresh(c);\n flashHx(c, \"Comment resolved\");\n return c.json(comment);\n });\n}\n\n/** Mark a comment of `buildId` resolved; 404 for a comment of another build. */\nexport async function resolveBuildComment(buildId: string, commentId: string): Promise<Comment> {\n const comments = new CommentModel(getStore().db);\n const found = (await comments.listByBuild(buildId)).find((item) => item.id === commentId);\n if (!found) {\n throwNotFound(\"Comment not found\");\n }\n return await comments.resolve(commentId);\n}\n\n/** A comment anchors to the build, a story or a snapshot (ADR 0013, decision 2), never two. */\nfunction assertOneAnchor(body: {\n snapshotId?: string | undefined;\n storyId?: string | undefined;\n}): void {\n if (body.snapshotId !== undefined && body.storyId !== undefined) {\n throw new HTTPException(400, {\n message: \"A comment anchors to the build, a story or a snapshot, not several\",\n });\n }\n}\n\n/**\n * Snapshot comments keep `comment:created`; build and story comments, the review ones, raise\n * `review:comment` (webhook and notifications, ADR 0013, decision 10). One event per comment.\n */\nasync function announceComment(project: Project, buildId: string, comment: Comment): Promise<void> {\n const data = {\n buildId,\n commentId: comment.id,\n snapshotId: comment.snapshotId,\n storyId: comment.storyId,\n body: comment.body.slice(0, 500),\n };\n if (comment.snapshotId === null) {\n await announceReview(project, REVIEW_EVENTS.comment, data, buildId);\n return;\n }\n await notifyProject(project, \"comment:created\", data, `/builds/${buildId}`);\n}\n\nexport async function createComment(\n project: Project,\n buildId: string,\n body: CommentCreateInput,\n): Promise<Comment> {\n assertOneAnchor(body);\n const userId = getStore().user?.id ?? null;\n const comment = await new CommentModel(getStore().db).create(project.id, buildId, userId, body);\n await announceComment(project, buildId, comment);\n return comment;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAaA,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,cAAc,MAAM,EAAE,EAAE;GACjE,aAAa;EACf;EACA,GAAGA;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,qBAAqB,YAAY;CACrC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,SAAS,EAAE,OAAO;EAAE,CAAC;EAC1D,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,oBAAoB,EAAE,EAAE;CAC3E;CACA,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,cAAc,EAAE;GACzD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,sBAAsB,YAAY;CACtC,QAAQ;CACR,MAAM,CAAC,QAAQ;CACf,SAAS;CACT,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,SAAS,EAAE,OAAO;EAAG,WAAW,EAAE,OAAO;CAAE,CAAC,EAAE;CAC9F,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,cAAc,EAAE;GACzD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,iBAAiB,KAAwB;CACvD,IAAI,QAAQ,mBAAmB,OAAO,MAAM;EAC1C,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,UAAU;EACrE,MAAM,QAAQ,MAAM,gBAAgB,QAAQ,IAAI,OAAO;EACvD,OAAO,EAAE,KAAK,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,MAAM,EAAE,CAAC;CAC3E,CAAC;CAED,IAAI,QAAQ,oBAAoB,OAAO,MAAM;EAC3C,MAAM,EAAE,MAAM,YAAY,EAAE,IAAI,MAAM,OAAO;EAC7C,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAE1E,MAAM,UAAU,MAAM,cAAc,UAAS,MADzB,gBAAgB,QAAQ,IAAI,OAAO,EAAA,CACJ,IAAI,EAAE,IAAI,MAAM,MAAM,CAAC;EAC1E,UAAU,CAAC;EACX,QAAQ,GAAG,eAAe;EAC1B,OAAO,EAAE,KAAK,SAAS,GAAG;CAC5B,CAAC;CAED,IAAI,QAAQ,qBAAqB,OAAO,MAAM;EAC5C,MAAM,EAAE,MAAM,SAAS,cAAc,EAAE,IAAI,MAAM,OAAO;EACxD,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,eAAe;EAE1E,MAAM,UAAU,MAAM,qBAAoB,MADtB,gBAAgB,QAAQ,IAAI,OAAO,EAAA,CACP,IAAI,SAAS;EAC7D,UAAU,CAAC;EACX,QAAQ,GAAG,kBAAkB;EAC7B,OAAO,EAAE,KAAK,OAAO;CACvB,CAAC;AACH;;AAGA,eAAsB,oBAAoB,SAAiB,WAAqC;CAC9F,MAAM,WAAW,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE;CAE/C,IAAI,EADW,MAAM,SAAS,YAAY,OAAO,EAAA,CAAG,MAAM,SAAS,KAAK,OAAO,SACtE,GACP,SAAc,mBAAmB;CAEnC,OAAO,MAAM,SAAS,QAAQ,SAAS;AACzC;;AAGA,SAAS,gBAAgB,MAGhB;CACP,IAAI,KAAK,eAAe,KAAA,KAAa,KAAK,YAAY,KAAA,GACpD,MAAM,IAAI,cAAc,KAAK,EAC3B,SAAS,qEACX,CAAC;AAEL;;;;;AAMA,eAAe,gBAAgB,SAAkB,SAAiB,SAAiC;CACjG,MAAM,OAAO;EACX;EACA,WAAW,QAAQ;EACnB,YAAY,QAAQ;EACpB,SAAS,QAAQ;EACjB,MAAM,QAAQ,KAAK,MAAM,GAAG,GAAG;CACjC;CACA,IAAI,QAAQ,eAAe,MAAM;EAC/B,MAAM,eAAe,SAAS,cAAc,SAAS,MAAM,OAAO;EAClE;CACF;CACA,MAAM,cAAc,SAAS,mBAAmB,MAAM,WAAW,SAAS;AAC5E;AAEA,eAAsB,cACpB,SACA,SACA,MACkB;CAClB,gBAAgB,IAAI;CACpB,MAAM,SAAS,SAAS,CAAC,CAAC,MAAM,MAAM;CACtC,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,QAAQ,IAAI,SAAS,QAAQ,IAAI;CAC9F,MAAM,gBAAgB,SAAS,SAAS,OAAO;CAC/C,OAAO;AACT"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { catalogOf, getStore } from "../store.mjs";
|
|
5
|
+
import { catalogPresence, componentBuildHistory, diffEntries } from "@storyshelf/core/capture";
|
|
6
|
+
import { BuildComponentModel, BuildModel } from "@storyshelf/core/models";
|
|
7
|
+
//#region src/routers/component-catalog.handlers.ts
|
|
8
|
+
/** Newest builds a component's history lists. */
|
|
9
|
+
const HISTORY_LIMIT = 50;
|
|
10
|
+
/** Changed rows whose added/removed/renamed counts are worked out (each loads two catalogs). */
|
|
11
|
+
const DETAIL_LIMIT = 10;
|
|
12
|
+
/** Newest build on the default branch, else the newest at all; the library's own choice. */
|
|
13
|
+
async function latestProjectBuild(project) {
|
|
14
|
+
const builds = new BuildModel(getStore().db);
|
|
15
|
+
return (await builds.list(project.id, {
|
|
16
|
+
branch: project.gitDefaultBranch,
|
|
17
|
+
limit: 1
|
|
18
|
+
}))[0] ?? (await builds.list(project.id, { limit: 1 }))[0];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The catalog view of a story file, or null when no published build holds it. The latest build is
|
|
22
|
+
* catalogued first (lazily, when it predates the catalog) so presence can be told.
|
|
23
|
+
*/
|
|
24
|
+
async function loadCatalogView(project, importPath) {
|
|
25
|
+
const latest = await latestProjectBuild(project);
|
|
26
|
+
const latestRows = latest ? await catalogueLatest(project, latest) : [];
|
|
27
|
+
const rows = await new BuildComponentModel(getStore().db).listByImportPath(project.id, importPath);
|
|
28
|
+
const history = componentBuildHistory(rows, await buildsOf(rows)).slice(0, HISTORY_LIMIT);
|
|
29
|
+
const shown = history.find((entry) => entry.build.gitBranch === project.gitDefaultBranch) ?? history[0];
|
|
30
|
+
if (!shown) return null;
|
|
31
|
+
return {
|
|
32
|
+
build: shown.build,
|
|
33
|
+
title: shown.component.title,
|
|
34
|
+
entries: await entriesOf(project.id, shown.build.id, importPath),
|
|
35
|
+
history: await withChanges(project.id, importPath, history),
|
|
36
|
+
presence: catalogPresence(history, importPath, latestRows.length > 0 ? latestRows : null)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Catalogue the latest build when it has no catalog yet; its component rows. */
|
|
40
|
+
async function catalogueLatest(project, latest) {
|
|
41
|
+
await catalogOf().load(project.id, latest.id).catch(() => null);
|
|
42
|
+
return await new BuildComponentModel(getStore().db).listByBuild(latest.id);
|
|
43
|
+
}
|
|
44
|
+
async function buildsOf(rows) {
|
|
45
|
+
const model = new BuildModel(getStore().db);
|
|
46
|
+
return (await Promise.all([...new Set(rows.map((row) => row.buildId))].map(async (id) => await model.get(id)))).filter((build) => build !== null);
|
|
47
|
+
}
|
|
48
|
+
/** A file's entries in one build's catalog; empty when the catalog cannot be read. */
|
|
49
|
+
async function entriesOf(projectId, buildId, importPath) {
|
|
50
|
+
return ((await catalogOf().load(projectId, buildId).catch(() => null))?.entries ?? []).filter((entry) => entry.importPath === importPath);
|
|
51
|
+
}
|
|
52
|
+
/** Add added/removed/renamed counts to the newest changed rows. */
|
|
53
|
+
async function withChanges(projectId, importPath, history) {
|
|
54
|
+
let budget = DETAIL_LIMIT;
|
|
55
|
+
return await Promise.all(history.map(async (row) => {
|
|
56
|
+
if (row.change !== "changed" || !row.previous || budget <= 0) return {
|
|
57
|
+
...row,
|
|
58
|
+
changes: null
|
|
59
|
+
};
|
|
60
|
+
budget -= 1;
|
|
61
|
+
const [before, after] = await Promise.all([entriesOf(projectId, row.previous.buildId, importPath), entriesOf(projectId, row.build.id, importPath)]);
|
|
62
|
+
return {
|
|
63
|
+
...row,
|
|
64
|
+
changes: diffEntries(before, after)
|
|
65
|
+
};
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { latestProjectBuild, loadCatalogView };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=component-catalog.handlers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-catalog.handlers.mjs","names":[],"sources":["../../src/routers/component-catalog.handlers.ts"],"sourcesContent":["import {\n catalogPresence,\n componentBuildHistory,\n diffEntries,\n type CatalogEntry,\n type ComponentBuildRow,\n type EntryChanges,\n} from \"@storyshelf/core/capture\";\nimport { BuildComponentModel, BuildModel } from \"@storyshelf/core/models\";\nimport type { ComponentPresence } from \"@storyshelf/core/models\";\nimport type { Build, BuildComponent, Project } from \"@storyshelf/core/schema\";\nimport { catalogOf, getStore } from \"../store.ts\";\n\n/** Newest builds a component's history lists. */\nconst HISTORY_LIMIT = 50;\n\n/** Changed rows whose added/removed/renamed counts are worked out (each loads two catalogs). */\nconst DETAIL_LIMIT = 10;\n\n/** A build of the component's history, with what changed in its stories when known. */\nexport interface CatalogHistoryRow extends ComponentBuildRow {\n changes: EntryChanges | null;\n}\n\n/** What the catalog knows of a component (ADR 0006, decision 9); the same with `testing` on or off. */\nexport interface ComponentCatalogView {\n /** The build the Stories tab shows: newest on the default branch holding the file, else newest. */\n build: Build;\n title: string;\n /** The file's stories and docs entries in that build. */\n entries: CatalogEntry[];\n history: CatalogHistoryRow[];\n presence: ComponentPresence;\n}\n\n/** Newest build on the default branch, else the newest at all; the library's own choice. */\nexport async function latestProjectBuild(project: Project): Promise<Build | undefined> {\n const builds = new BuildModel(getStore().db);\n return (\n (await builds.list(project.id, { branch: project.gitDefaultBranch, limit: 1 }))[0] ??\n (await builds.list(project.id, { limit: 1 }))[0]\n );\n}\n\n/**\n * The catalog view of a story file, or null when no published build holds it. The latest build is\n * catalogued first (lazily, when it predates the catalog) so presence can be told.\n */\nexport async function loadCatalogView(\n project: Project,\n importPath: string,\n): Promise<ComponentCatalogView | null> {\n const latest = await latestProjectBuild(project);\n const latestRows = latest ? await catalogueLatest(project, latest) : [];\n const rows = await new BuildComponentModel(getStore().db).listByImportPath(\n project.id,\n importPath,\n );\n const history = componentBuildHistory(rows, await buildsOf(rows)).slice(0, HISTORY_LIMIT);\n const shown =\n history.find((entry) => entry.build.gitBranch === project.gitDefaultBranch) ?? history[0];\n if (!shown) {\n return null;\n }\n return {\n build: shown.build,\n title: shown.component.title,\n entries: await entriesOf(project.id, shown.build.id, importPath),\n history: await withChanges(project.id, importPath, history),\n presence: catalogPresence(history, importPath, latestRows.length > 0 ? latestRows : null),\n };\n}\n\n/** Catalogue the latest build when it has no catalog yet; its component rows. */\nasync function catalogueLatest(project: Project, latest: Build): Promise<BuildComponent[]> {\n await catalogOf()\n .load(project.id, latest.id)\n .catch(() => null);\n return await new BuildComponentModel(getStore().db).listByBuild(latest.id);\n}\n\nasync function buildsOf(rows: readonly BuildComponent[]): Promise<Build[]> {\n const model = new BuildModel(getStore().db);\n const found = await Promise.all(\n [...new Set(rows.map((row) => row.buildId))].map(async (id) => await model.get(id)),\n );\n return found.filter((build): build is Build => build !== null);\n}\n\n/** A file's entries in one build's catalog; empty when the catalog cannot be read. */\nasync function entriesOf(\n projectId: string,\n buildId: string,\n importPath: string,\n): Promise<CatalogEntry[]> {\n const catalog = await catalogOf()\n .load(projectId, buildId)\n .catch(() => null);\n return (catalog?.entries ?? []).filter((entry) => entry.importPath === importPath);\n}\n\n/** Add added/removed/renamed counts to the newest changed rows. */\nasync function withChanges(\n projectId: string,\n importPath: string,\n history: readonly ComponentBuildRow[],\n): Promise<CatalogHistoryRow[]> {\n let budget = DETAIL_LIMIT;\n return await Promise.all(\n history.map(async (row): Promise<CatalogHistoryRow> => {\n if (row.change !== \"changed\" || !row.previous || budget <= 0) {\n return { ...row, changes: null };\n }\n budget -= 1;\n const [before, after] = await Promise.all([\n entriesOf(projectId, row.previous.buildId, importPath),\n entriesOf(projectId, row.build.id, importPath),\n ]);\n return { ...row, changes: diffEntries(before, after) };\n }),\n );\n}\n"],"mappings":";;;;;;;;AAcA,MAAM,gBAAgB;;AAGtB,MAAM,eAAe;;AAmBrB,eAAsB,mBAAmB,SAA8C;CACrF,MAAM,SAAS,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE;CAC3C,QACG,MAAM,OAAO,KAAK,QAAQ,IAAI;EAAE,QAAQ,QAAQ;EAAkB,OAAO;CAAE,CAAC,EAAA,CAAG,OAC/E,MAAM,OAAO,KAAK,QAAQ,IAAI,EAAE,OAAO,EAAE,CAAC,EAAA,CAAG;AAElD;;;;;AAMA,eAAsB,gBACpB,SACA,YACsC;CACtC,MAAM,SAAS,MAAM,mBAAmB,OAAO;CAC/C,MAAM,aAAa,SAAS,MAAM,gBAAgB,SAAS,MAAM,IAAI,CAAC;CACtE,MAAM,OAAO,MAAM,IAAI,oBAAoB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,iBACxD,QAAQ,IACR,UACF;CACA,MAAM,UAAU,sBAAsB,MAAM,MAAM,SAAS,IAAI,CAAC,CAAC,CAAC,MAAM,GAAG,aAAa;CACxF,MAAM,QACJ,QAAQ,MAAM,UAAU,MAAM,MAAM,cAAc,QAAQ,gBAAgB,KAAK,QAAQ;CACzF,IAAI,CAAC,OACH,OAAO;CAET,OAAO;EACL,OAAO,MAAM;EACb,OAAO,MAAM,UAAU;EACvB,SAAS,MAAM,UAAU,QAAQ,IAAI,MAAM,MAAM,IAAI,UAAU;EAC/D,SAAS,MAAM,YAAY,QAAQ,IAAI,YAAY,OAAO;EAC1D,UAAU,gBAAgB,SAAS,YAAY,WAAW,SAAS,IAAI,aAAa,IAAI;CAC1F;AACF;;AAGA,eAAe,gBAAgB,SAAkB,QAA0C;CACzF,MAAM,UAAU,CAAC,CACd,KAAK,QAAQ,IAAI,OAAO,EAAE,CAAC,CAC3B,YAAY,IAAI;CACnB,OAAO,MAAM,IAAI,oBAAoB,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,YAAY,OAAO,EAAE;AAC3E;AAEA,eAAe,SAAS,MAAmD;CACzE,MAAM,QAAQ,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE;CAI1C,QAAO,MAHa,QAAQ,IAC1B,CAAC,GAAG,IAAI,IAAI,KAAK,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,OAAO,MAAM,MAAM,IAAI,EAAE,CAAC,CACpF,EAAA,CACa,QAAQ,UAA0B,UAAU,IAAI;AAC/D;;AAGA,eAAe,UACb,WACA,SACA,YACyB;CAIzB,SAAQ,MAHc,UAAU,CAAC,CAC9B,KAAK,WAAW,OAAO,CAAC,CACxB,YAAY,IAAI,EAAA,EACF,WAAW,CAAC,EAAA,CAAG,QAAQ,UAAU,MAAM,eAAe,UAAU;AACnF;;AAGA,eAAe,YACb,WACA,YACA,SAC8B;CAC9B,IAAI,SAAS;CACb,OAAO,MAAM,QAAQ,IACnB,QAAQ,IAAI,OAAO,QAAoC;EACrD,IAAI,IAAI,WAAW,aAAa,CAAC,IAAI,YAAY,UAAU,GACzD,OAAO;GAAE,GAAG;GAAK,SAAS;EAAK;EAEjC,UAAU;EACV,MAAM,CAAC,QAAQ,SAAS,MAAM,QAAQ,IAAI,CACxC,UAAU,WAAW,IAAI,SAAS,SAAS,UAAU,GACrD,UAAU,WAAW,IAAI,MAAM,IAAI,UAAU,CAC/C,CAAC;EACD,OAAO;GAAE,GAAG;GAAK,SAAS,YAAY,QAAQ,KAAK;EAAE;CACvD,CAAC,CACH;AACF"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore, testingEnabled } from "../store.mjs";
|
|
5
|
+
import { linkedUsageFor } from "./links.handlers.mjs";
|
|
6
|
+
import { previewBaseOf } from "./preview-base.mjs";
|
|
7
|
+
import { designUrlsFor, loadDesignResolutions, loadReviewDesign, returnPathOf } from "./design.handlers.mjs";
|
|
8
|
+
import { loadComponentReview } from "./review-section.handlers.mjs";
|
|
9
|
+
import { groupSnapshots } from "../pages/build-diff-model.mjs";
|
|
10
|
+
import { componentTabs } from "../pages/component-detail.mjs";
|
|
11
|
+
import { entriesFromSnapshots } from "../pages/library-model.mjs";
|
|
12
|
+
import { latestProjectBuild, loadCatalogView } from "./component-catalog.handlers.mjs";
|
|
13
|
+
import { BuildModel, ProjectModel, SnapshotModel, componentHistory, componentPresence, normalizeImportPath } from "@storyshelf/core/models";
|
|
14
|
+
//#region src/routers/component.handlers.ts
|
|
15
|
+
async function buildsOf(snapshots) {
|
|
16
|
+
const model = new BuildModel(getStore().db);
|
|
17
|
+
return (await Promise.all([...new Set(snapshots.map((snap) => snap.buildId))].map(async (id) => await model.get(id)))).filter((build) => build !== null);
|
|
18
|
+
}
|
|
19
|
+
/** Design links of a component's first state, with cached metadata and connect offers. */
|
|
20
|
+
async function loadDesignTab(c, project, states) {
|
|
21
|
+
const [first] = states;
|
|
22
|
+
if (!first) return [];
|
|
23
|
+
const urls = designUrlsFor(await loadDesignResolutions(project, states), first);
|
|
24
|
+
return await loadReviewDesign(project, first, urls, returnPathOf(c)).catch(() => []);
|
|
25
|
+
}
|
|
26
|
+
/** Tab-specific data (design strip or linked packages), loaded only when its tab is open. */
|
|
27
|
+
async function loadTabData(c, data, tab, states) {
|
|
28
|
+
const [first] = states;
|
|
29
|
+
if (!first) return {
|
|
30
|
+
design: [],
|
|
31
|
+
usage: []
|
|
32
|
+
};
|
|
33
|
+
if (tab === "design") return {
|
|
34
|
+
design: await loadDesignTab(c, data.project, states),
|
|
35
|
+
usage: []
|
|
36
|
+
};
|
|
37
|
+
if (tab === "linked") return {
|
|
38
|
+
design: [],
|
|
39
|
+
usage: await linkedUsageFor(c, data.project, data.shownBuild.id, first.storyImportPath).catch(() => [])
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
design: [],
|
|
43
|
+
usage: []
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** History, states and tombstone presence of a story file from its snapshots. */
|
|
47
|
+
async function loadCaptured(project, importPath) {
|
|
48
|
+
const { db } = getStore();
|
|
49
|
+
const snapshots = await new SnapshotModel(db).listByImportPath(project.id, importPath);
|
|
50
|
+
const history = componentHistory(snapshots, await buildsOf(snapshots));
|
|
51
|
+
const shownBuild = history[0]?.build;
|
|
52
|
+
if (!shownBuild) return null;
|
|
53
|
+
const states = snapshots.filter((snap) => snap.buildId === shownBuild.id);
|
|
54
|
+
const latest = await latestProjectBuild(project);
|
|
55
|
+
const latestSnapshots = latest ? await new SnapshotModel(db).listByBuild(latest.id) : [];
|
|
56
|
+
const title = states[0]?.storyTitle ?? importPath;
|
|
57
|
+
return {
|
|
58
|
+
states,
|
|
59
|
+
title,
|
|
60
|
+
captured: {
|
|
61
|
+
shownBuild,
|
|
62
|
+
groups: groupSnapshots(states),
|
|
63
|
+
history,
|
|
64
|
+
presence: componentPresence(history, latest?.id ?? null, latestSnapshots, title)
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load a component page by story-file path, or null when the project or file is unknown. The
|
|
70
|
+
* catalog gives the page its skeleton in both modes; with `testing`, captured snapshots are layered
|
|
71
|
+
* on. Moved/deleted files still load, carrying a tombstone presence.
|
|
72
|
+
*/
|
|
73
|
+
async function loadComponentPage(c, slug, rawPath, requestedTab, story) {
|
|
74
|
+
const project = await new ProjectModel(getStore().db).getBySlug(slug);
|
|
75
|
+
const importPath = normalizeImportPath(rawPath);
|
|
76
|
+
if (!project || !importPath) return null;
|
|
77
|
+
const testing = testingEnabled();
|
|
78
|
+
const [loaded, catalog] = await Promise.all([testing ? loadCaptured(project, importPath) : Promise.resolve(null), loadCatalogView(project, importPath).catch(() => null)]);
|
|
79
|
+
const view = catalog ?? (loaded ? viewOfCaptured(loaded) : null);
|
|
80
|
+
if (!view) return null;
|
|
81
|
+
const tab = resolveTab(requestedTab, testing, loaded !== null);
|
|
82
|
+
const tabData = loaded ? await loadTabData(c, {
|
|
83
|
+
project,
|
|
84
|
+
shownBuild: loaded.captured.shownBuild
|
|
85
|
+
}, tab, loaded.states) : {
|
|
86
|
+
design: [],
|
|
87
|
+
usage: []
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
project,
|
|
91
|
+
importPath,
|
|
92
|
+
title: loaded?.title ?? view.title,
|
|
93
|
+
tab,
|
|
94
|
+
testing,
|
|
95
|
+
catalog: view,
|
|
96
|
+
captured: loaded?.captured ?? null,
|
|
97
|
+
story,
|
|
98
|
+
previewBase: await previewBaseOf(project, view.build),
|
|
99
|
+
publishedBaseDomain: getStore().config.publishedBaseDomain,
|
|
100
|
+
review: await loadComponentReview(project, view.build),
|
|
101
|
+
...tabData
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** A catalog view for a captured file whose published index is gone: its snapshots stand in. */
|
|
105
|
+
function viewOfCaptured(loaded) {
|
|
106
|
+
return {
|
|
107
|
+
build: loaded.captured.shownBuild,
|
|
108
|
+
title: loaded.title,
|
|
109
|
+
entries: entriesFromSnapshots(loaded.states),
|
|
110
|
+
history: [],
|
|
111
|
+
presence: loaded.captured.presence
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The requested tab when this page offers it; else States once captured, Stories otherwise. */
|
|
115
|
+
function resolveTab(requested, testing, captured) {
|
|
116
|
+
return componentTabs(testing).find((candidate) => candidate === requested) ?? (testing && captured ? "states" : "stories");
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { loadComponentPage };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=component.handlers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.handlers.mjs","names":[],"sources":["../../src/routers/component.handlers.ts"],"sourcesContent":["import {\n BuildModel,\n componentHistory,\n componentPresence,\n normalizeImportPath,\n ProjectModel,\n SnapshotModel,\n} from \"@storyshelf/core/models\";\nimport type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport type { Context } from \"hono\";\nimport { groupSnapshots } from \"../pages/build-diff-model.ts\";\nimport {\n componentTabs,\n type CapturedComponent,\n type ComponentPageData,\n type ComponentTab,\n} from \"../pages/component-detail.tsx\";\nimport type { DesignItem } from \"../pages/design-links.tsx\";\nimport { entriesFromSnapshots } from \"../pages/library-model.ts\";\nimport { getStore, testingEnabled } from \"../store.ts\";\nimport {\n latestProjectBuild,\n loadCatalogView,\n type ComponentCatalogView,\n} from \"./component-catalog.handlers.ts\";\nimport {\n designUrlsFor,\n loadDesignResolutions,\n loadReviewDesign,\n returnPathOf,\n} from \"./design.handlers.ts\";\nimport { linkedUsageFor } from \"./links.handlers.ts\";\nimport { previewBaseOf } from \"./preview-base.ts\";\nimport { loadComponentReview } from \"./review-section.handlers.ts\";\n\nasync function buildsOf(snapshots: readonly Snapshot[]): Promise<Build[]> {\n const model = new BuildModel(getStore().db);\n const found = await Promise.all(\n [...new Set(snapshots.map((snap) => snap.buildId))].map(async (id) => await model.get(id)),\n );\n return found.filter((build): build is Build => build !== null);\n}\n\n/** Design links of a component's first state, with cached metadata and connect offers. */\nasync function loadDesignTab(\n c: Context,\n project: ComponentPageData[\"project\"],\n states: Snapshot[],\n): Promise<DesignItem[]> {\n const [first] = states;\n if (!first) return [];\n const urls = designUrlsFor(await loadDesignResolutions(project, states), first);\n return await loadReviewDesign(project, first, urls, returnPathOf(c)).catch(() => []);\n}\n\n/** Tab-specific data (design strip or linked packages), loaded only when its tab is open. */\nasync function loadTabData(\n c: Context,\n data: { project: Project; shownBuild: Build },\n tab: ComponentTab,\n states: Snapshot[],\n): Promise<Pick<ComponentPageData, \"design\" | \"usage\">> {\n const [first] = states;\n if (!first) return { design: [], usage: [] };\n if (tab === \"design\") return { design: await loadDesignTab(c, data.project, states), usage: [] };\n if (tab === \"linked\") {\n const usage = await linkedUsageFor(\n c,\n data.project,\n data.shownBuild.id,\n first.storyImportPath,\n ).catch(() => []);\n return { design: [], usage };\n }\n return { design: [], usage: [] };\n}\n\n/** What a story file's captured snapshots give the page; null when it was never captured. */\ninterface CapturedLoad {\n captured: CapturedComponent;\n /** The shown build's snapshots of the file (they drive the Design and Linked tabs). */\n states: Snapshot[];\n /** Title of the newest snapshot (retitles update in place). */\n title: string;\n}\n\n/** History, states and tombstone presence of a story file from its snapshots. */\nasync function loadCaptured(project: Project, importPath: string): Promise<CapturedLoad | null> {\n const { db } = getStore();\n const snapshots = await new SnapshotModel(db).listByImportPath(project.id, importPath);\n const history = componentHistory(snapshots, await buildsOf(snapshots));\n const shownBuild = history[0]?.build;\n if (!shownBuild) return null;\n const states = snapshots.filter((snap) => snap.buildId === shownBuild.id);\n const latest = await latestProjectBuild(project);\n const latestSnapshots = latest ? await new SnapshotModel(db).listByBuild(latest.id) : [];\n const title = states[0]?.storyTitle ?? importPath;\n return {\n states,\n title,\n captured: {\n shownBuild,\n groups: groupSnapshots(states),\n history,\n presence: componentPresence(history, latest?.id ?? null, latestSnapshots, title),\n },\n };\n}\n\n/**\n * Load a component page by story-file path, or null when the project or file is unknown. The\n * catalog gives the page its skeleton in both modes; with `testing`, captured snapshots are layered\n * on. Moved/deleted files still load, carrying a tombstone presence.\n */\nexport async function loadComponentPage(\n c: Context,\n slug: string,\n rawPath: string,\n requestedTab: string | undefined,\n story?: string,\n): Promise<ComponentPageData | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n const importPath = normalizeImportPath(rawPath);\n if (!project || !importPath) return null;\n const testing = testingEnabled();\n const [loaded, catalog] = await Promise.all([\n testing ? loadCaptured(project, importPath) : Promise.resolve(null),\n loadCatalogView(project, importPath).catch(() => null),\n ]);\n const view = catalog ?? (loaded ? viewOfCaptured(loaded) : null);\n if (!view) return null;\n const tab = resolveTab(requestedTab, testing, loaded !== null);\n const tabData = loaded\n ? await loadTabData(c, { project, shownBuild: loaded.captured.shownBuild }, tab, loaded.states)\n : { design: [], usage: [] };\n return {\n project,\n importPath,\n title: loaded?.title ?? view.title,\n tab,\n testing,\n catalog: view,\n captured: loaded?.captured ?? null,\n story,\n previewBase: await previewBaseOf(project, view.build),\n publishedBaseDomain: getStore().config.publishedBaseDomain,\n review: await loadComponentReview(project, view.build),\n ...tabData,\n };\n}\n\n/** A catalog view for a captured file whose published index is gone: its snapshots stand in. */\nfunction viewOfCaptured(loaded: CapturedLoad): ComponentCatalogView {\n return {\n build: loaded.captured.shownBuild,\n title: loaded.title,\n entries: entriesFromSnapshots(loaded.states),\n history: [],\n presence: loaded.captured.presence,\n };\n}\n\n/** The requested tab when this page offers it; else States once captured, Stories otherwise. */\nfunction resolveTab(\n requested: string | undefined,\n testing: boolean,\n captured: boolean,\n): ComponentTab {\n const tabs = componentTabs(testing);\n const found = tabs.find((candidate) => candidate === requested);\n return found ?? (testing && captured ? \"states\" : \"stories\");\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { forbidden, notFound, unauthorized } from "./helpers.mjs";
|
|
6
|
+
import { flash } from "./flash.mjs";
|
|
7
|
+
import { connectionModel, findConnectable } from "../connections.mjs";
|
|
8
|
+
import { safeReturnPath, signState, verifyState } from "./oauth-state.mjs";
|
|
9
|
+
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
10
|
+
//#region src/routers/connections.ts
|
|
11
|
+
const COOKIE = "storyshelf_oauth_state";
|
|
12
|
+
const COOKIE_PATH = "/connections";
|
|
13
|
+
/**
|
|
14
|
+
* "Connect your account" for any service exposing OAuth (see `connectableServices`):
|
|
15
|
+
* start → consent at the service → callback stores the user's own connection.
|
|
16
|
+
* Services stay unaware of routes, state signing and storage.
|
|
17
|
+
*/
|
|
18
|
+
function registerConnections(app) {
|
|
19
|
+
app.get("/connections/:service/start", handleStart);
|
|
20
|
+
app.get("/connections/:service/callback", handleCallback);
|
|
21
|
+
app.post("/connections/:service/disconnect", handleDisconnect);
|
|
22
|
+
}
|
|
23
|
+
function serviceOf(c) {
|
|
24
|
+
const service = findConnectable(c.req.param("service") ?? "");
|
|
25
|
+
if (!service) notFound("Service does not support connecting accounts");
|
|
26
|
+
return service;
|
|
27
|
+
}
|
|
28
|
+
function sessionUser() {
|
|
29
|
+
const { user, authEnabled } = getStore();
|
|
30
|
+
if (!authEnabled) notFound("Connecting accounts requires authentication");
|
|
31
|
+
if (!user) unauthorized();
|
|
32
|
+
return user;
|
|
33
|
+
}
|
|
34
|
+
function signingSecret() {
|
|
35
|
+
const { secret } = getStore().config;
|
|
36
|
+
if (!secret) notFound("Connecting accounts requires a configured server secret");
|
|
37
|
+
return secret;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Callback URL of an interactive, request-bound flow (ADR 0007): the confirmed public origin wins,
|
|
41
|
+
* the request's own address is the fallback authentication is allowed.
|
|
42
|
+
*/
|
|
43
|
+
async function redirectUri(c, kind) {
|
|
44
|
+
const { publicOrigin, config } = getStore();
|
|
45
|
+
const requestOrigin = new URL(c.req.url).origin;
|
|
46
|
+
return `${(publicOrigin ? await publicOrigin.authOrigin(requestOrigin) : config.publicBaseUrl ?? requestOrigin).replace(/\/+$/u, "")}/connections/${kind}/callback`;
|
|
47
|
+
}
|
|
48
|
+
async function handleStart(c) {
|
|
49
|
+
const service = serviceOf(c);
|
|
50
|
+
const user = sessionUser();
|
|
51
|
+
const kind = service.metadata.kind;
|
|
52
|
+
const { state, nonce } = signState(signingSecret(), {
|
|
53
|
+
userId: user.id,
|
|
54
|
+
service: kind,
|
|
55
|
+
returnTo: safeReturnPath(c.req.query("return"), "/")
|
|
56
|
+
});
|
|
57
|
+
setCookie(c, COOKIE, nonce, {
|
|
58
|
+
path: COOKIE_PATH,
|
|
59
|
+
httpOnly: true,
|
|
60
|
+
sameSite: "Lax",
|
|
61
|
+
maxAge: 600,
|
|
62
|
+
secure: new URL(c.req.url).protocol === "https:"
|
|
63
|
+
});
|
|
64
|
+
return c.redirect(service.oauth.authorizeUrl({
|
|
65
|
+
state,
|
|
66
|
+
redirectUri: await redirectUri(c, kind)
|
|
67
|
+
}), 302);
|
|
68
|
+
}
|
|
69
|
+
async function handleCallback(c) {
|
|
70
|
+
const service = serviceOf(c);
|
|
71
|
+
const user = sessionUser();
|
|
72
|
+
const verified = verifiedState(c, service.metadata.kind, user.id);
|
|
73
|
+
const code = c.req.query("code");
|
|
74
|
+
if (!code || c.req.query("error")) flash(c, `${service.metadata.name} connection was cancelled`, "warning");
|
|
75
|
+
else await completeConnect(c, service, user.id, code);
|
|
76
|
+
return c.redirect(verified.returnTo, 302);
|
|
77
|
+
}
|
|
78
|
+
/** Check the callback's signed state against the service, the signed-in user and this browser's cookie. */
|
|
79
|
+
function verifiedState(c, kind, userId) {
|
|
80
|
+
const verified = verifyState(signingSecret(), c.req.query("state") ?? "");
|
|
81
|
+
const nonce = getCookie(c, COOKIE);
|
|
82
|
+
deleteCookie(c, COOKIE, { path: COOKIE_PATH });
|
|
83
|
+
if (!verified || verified.service !== kind || verified.userId !== userId) forbidden();
|
|
84
|
+
if (verified.nonce !== nonce) forbidden();
|
|
85
|
+
return verified;
|
|
86
|
+
}
|
|
87
|
+
async function completeConnect(c, service, userId, code) {
|
|
88
|
+
const kind = service.metadata.kind;
|
|
89
|
+
try {
|
|
90
|
+
const tokens = await service.oauth.exchangeCode({
|
|
91
|
+
code,
|
|
92
|
+
redirectUri: await redirectUri(c, kind)
|
|
93
|
+
});
|
|
94
|
+
await connectionModel().connect(userId, kind, tokens);
|
|
95
|
+
flash(c, `${service.metadata.name} connected`);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
getStore().logger.warn({
|
|
98
|
+
err: error,
|
|
99
|
+
service: kind
|
|
100
|
+
}, "oauth connect failed");
|
|
101
|
+
flash(c, `Could not connect ${service.metadata.name}`, "danger");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function handleDisconnect(c) {
|
|
105
|
+
const service = serviceOf(c);
|
|
106
|
+
const user = sessionUser();
|
|
107
|
+
const form = await c.req.formData();
|
|
108
|
+
await connectionModel().remove(user.id, service.metadata.kind);
|
|
109
|
+
flash(c, `${service.metadata.name} disconnected`);
|
|
110
|
+
return c.redirect(safeReturnPath(formString(form.get("return")), "/"), 303);
|
|
111
|
+
}
|
|
112
|
+
function formString(value) {
|
|
113
|
+
return typeof value === "string" ? value : "";
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
export { registerConnections };
|
|
117
|
+
|
|
118
|
+
//# sourceMappingURL=connections.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connections.mjs","names":[],"sources":["../../src/routers/connections.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport { deleteCookie, getCookie, setCookie } from \"hono/cookie\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { connectionModel, findConnectable, type Connectable } from \"../connections.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { forbidden, notFound, unauthorized } from \"./helpers.ts\";\nimport { safeReturnPath, signState, verifyState, type OAuthState } from \"./oauth-state.ts\";\n\nconst COOKIE = \"storyshelf_oauth_state\";\nconst COOKIE_PATH = \"/connections\";\n\n/**\n * \"Connect your account\" for any service exposing OAuth (see `connectableServices`):\n * start → consent at the service → callback stores the user's own connection.\n * Services stay unaware of routes, state signing and storage.\n */\nexport function registerConnections(app: ShelfRouter): void {\n app.get(\"/connections/:service/start\", handleStart);\n app.get(\"/connections/:service/callback\", handleCallback);\n app.post(\"/connections/:service/disconnect\", handleDisconnect);\n}\n\nfunction serviceOf(c: Context): Connectable {\n const service = findConnectable(c.req.param(\"service\") ?? \"\");\n if (!service) notFound(\"Service does not support connecting accounts\");\n return service;\n}\n\nfunction sessionUser(): { id: string } {\n const { user, authEnabled } = getStore();\n if (!authEnabled) notFound(\"Connecting accounts requires authentication\");\n if (!user) unauthorized();\n return user;\n}\n\nfunction signingSecret(): string {\n const { secret } = getStore().config;\n if (!secret) notFound(\"Connecting accounts requires a configured server secret\");\n return secret;\n}\n\n/**\n * Callback URL of an interactive, request-bound flow (ADR 0007): the confirmed public origin wins,\n * the request's own address is the fallback authentication is allowed.\n */\nasync function redirectUri(c: Context, kind: string): Promise<string> {\n const { publicOrigin, config } = getStore();\n const requestOrigin = new URL(c.req.url).origin;\n const base = publicOrigin\n ? await publicOrigin.authOrigin(requestOrigin)\n : (config.publicBaseUrl ?? requestOrigin);\n return `${base.replace(/\\/+$/u, \"\")}/connections/${kind}/callback`;\n}\n\nasync function handleStart(c: Context): Promise<Response> {\n const service = serviceOf(c);\n const user = sessionUser();\n const kind = service.metadata.kind;\n const { state, nonce } = signState(signingSecret(), {\n userId: user.id,\n service: kind,\n returnTo: safeReturnPath(c.req.query(\"return\"), \"/\"),\n });\n setCookie(c, COOKIE, nonce, {\n path: COOKIE_PATH,\n httpOnly: true,\n sameSite: \"Lax\",\n maxAge: 600,\n secure: new URL(c.req.url).protocol === \"https:\",\n });\n return c.redirect(\n service.oauth.authorizeUrl({ state, redirectUri: await redirectUri(c, kind) }),\n 302,\n );\n}\n\nasync function handleCallback(c: Context): Promise<Response> {\n const service = serviceOf(c);\n const user = sessionUser();\n const verified = verifiedState(c, service.metadata.kind, user.id);\n const code = c.req.query(\"code\");\n if (!code || c.req.query(\"error\")) {\n flash(c, `${service.metadata.name} connection was cancelled`, \"warning\");\n } else {\n await completeConnect(c, service, user.id, code);\n }\n return c.redirect(verified.returnTo, 302);\n}\n\n/** Check the callback's signed state against the service, the signed-in user and this browser's cookie. */\nfunction verifiedState(c: Context, kind: string, userId: string): OAuthState {\n const verified = verifyState(signingSecret(), c.req.query(\"state\") ?? \"\");\n const nonce = getCookie(c, COOKIE);\n deleteCookie(c, COOKIE, { path: COOKIE_PATH });\n if (!verified || verified.service !== kind || verified.userId !== userId) forbidden();\n if (verified.nonce !== nonce) forbidden();\n return verified;\n}\n\nasync function completeConnect(\n c: Context,\n service: Connectable,\n userId: string,\n code: string,\n): Promise<void> {\n const kind = service.metadata.kind;\n try {\n const tokens = await service.oauth.exchangeCode({\n code,\n redirectUri: await redirectUri(c, kind),\n });\n await connectionModel().connect(userId, kind, tokens);\n flash(c, `${service.metadata.name} connected`);\n } catch (error) {\n getStore().logger.warn({ err: error, service: kind }, \"oauth connect failed\");\n flash(c, `Could not connect ${service.metadata.name}`, \"danger\");\n }\n}\n\nasync function handleDisconnect(c: Context): Promise<Response> {\n const service = serviceOf(c);\n const user = sessionUser();\n const form = await c.req.formData();\n await connectionModel().remove(user.id, service.metadata.kind);\n flash(c, `${service.metadata.name} disconnected`);\n return c.redirect(safeReturnPath(formString(form.get(\"return\")), \"/\"), 303);\n}\n\nfunction formString(value: FormDataEntryValue | null): string {\n return typeof value === \"string\" ? value : \"\";\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
|
+
import { getStore } from "../store.mjs";
|
|
6
|
+
import { canConnect, connectionModel } from "../connections.mjs";
|
|
7
|
+
import { DesignCredentialModel } from "@storyshelf/core/models";
|
|
8
|
+
//#region src/routers/design-connect.handlers.ts
|
|
9
|
+
/** The signed-in user whose own connection may serve previews (none when auth is off). */
|
|
10
|
+
function viewerIdOf() {
|
|
11
|
+
const { user, authEnabled } = getStore();
|
|
12
|
+
return authEnabled ? user?.id : void 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Per provider kind, whether to offer "Connect your account": only when the operator
|
|
16
|
+
* enabled OAuth, the viewer is signed in, and the project has no shared credential.
|
|
17
|
+
*/
|
|
18
|
+
async function loadConnectStates(projectId, kinds, returnTo) {
|
|
19
|
+
const viewerId = viewerIdOf();
|
|
20
|
+
if (!viewerId || !canConnect()) return /* @__PURE__ */ new Map();
|
|
21
|
+
const { db, config, design } = getStore();
|
|
22
|
+
const credentials = new DesignCredentialModel(db, credentialKeys(config));
|
|
23
|
+
const candidates = design.filter((p) => p.oauth && kinds.includes(p.metadata.kind));
|
|
24
|
+
const states = await Promise.all(candidates.map(async (provider) => {
|
|
25
|
+
const kind = provider.metadata.kind;
|
|
26
|
+
if (await credentials.info(projectId, kind)) return null;
|
|
27
|
+
return [kind, connectOffer(kind, await connectionModel().info(viewerId, kind), returnTo)];
|
|
28
|
+
}));
|
|
29
|
+
return new Map(states.filter((entry) => entry !== null));
|
|
30
|
+
}
|
|
31
|
+
function connectOffer(kind, row, returnTo) {
|
|
32
|
+
const base = `/connections/${kind}`;
|
|
33
|
+
const expired = row?.status === "expired";
|
|
34
|
+
return {
|
|
35
|
+
state: row ? expired ? "reconnect" : "connected" : "connect",
|
|
36
|
+
startUrl: `${base}/start?return=${encodeURIComponent(returnTo)}`,
|
|
37
|
+
disconnectUrl: `${base}/disconnect`,
|
|
38
|
+
returnTo
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { loadConnectStates, viewerIdOf };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=design-connect.handlers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"design-connect.handlers.mjs","names":[],"sources":["../../src/routers/design-connect.handlers.ts"],"sourcesContent":["import { DesignCredentialModel } from \"@storyshelf/core/models\";\nimport type { UserConnection } from \"@storyshelf/core/schema\";\nimport { canConnect, connectionModel } from \"../connections.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport type { DesignConnect } from \"../pages/design-links.tsx\";\nimport { getStore } from \"../store.ts\";\n\n/** The signed-in user whose own connection may serve previews (none when auth is off). */\nexport function viewerIdOf(): string | undefined {\n const { user, authEnabled } = getStore();\n return authEnabled ? user?.id : undefined;\n}\n\n/**\n * Per provider kind, whether to offer \"Connect your account\": only when the operator\n * enabled OAuth, the viewer is signed in, and the project has no shared credential.\n */\nexport async function loadConnectStates(\n projectId: string,\n kinds: readonly string[],\n returnTo: string,\n): Promise<Map<string, DesignConnect>> {\n const viewerId = viewerIdOf();\n if (!viewerId || !canConnect()) return new Map();\n const { db, config, design } = getStore();\n const credentials = new DesignCredentialModel(db, credentialKeys(config));\n const candidates = design.filter((p) => p.oauth && kinds.includes(p.metadata.kind));\n const states = await Promise.all(\n candidates.map(async (provider) => {\n const kind = provider.metadata.kind;\n const shared = await credentials.info(projectId, kind);\n if (shared) return null;\n return [\n kind,\n connectOffer(kind, await connectionModel().info(viewerId, kind), returnTo),\n ] as const;\n }),\n );\n return new Map(states.filter((entry) => entry !== null));\n}\n\nfunction connectOffer(kind: string, row: UserConnection | null, returnTo: string): DesignConnect {\n const base = `/connections/${kind}`;\n const expired = row?.status === \"expired\";\n return {\n state: row ? (expired ? \"reconnect\" : \"connected\") : \"connect\",\n startUrl: `${base}/start?return=${encodeURIComponent(returnTo)}`,\n disconnectUrl: `${base}/disconnect`,\n returnTo,\n };\n}\n"],"mappings":";;;;;;;;;AAQA,SAAgB,aAAiC;CAC/C,MAAM,EAAE,MAAM,gBAAgB,SAAS;CACvC,OAAO,cAAc,MAAM,KAAK,KAAA;AAClC;;;;;AAMA,eAAsB,kBACpB,WACA,OACA,UACqC;CACrC,MAAM,WAAW,WAAW;CAC5B,IAAI,CAAC,YAAY,CAAC,WAAW,GAAG,uBAAO,IAAI,IAAI;CAC/C,MAAM,EAAE,IAAI,QAAQ,WAAW,SAAS;CACxC,MAAM,cAAc,IAAI,sBAAsB,IAAI,eAAe,MAAM,CAAC;CACxE,MAAM,aAAa,OAAO,QAAQ,MAAM,EAAE,SAAS,MAAM,SAAS,EAAE,SAAS,IAAI,CAAC;CAClF,MAAM,SAAS,MAAM,QAAQ,IAC3B,WAAW,IAAI,OAAO,aAAa;EACjC,MAAM,OAAO,SAAS,SAAS;EAE/B,IAAI,MADiB,YAAY,KAAK,WAAW,IAAI,GACzC,OAAO;EACnB,OAAO,CACL,MACA,aAAa,MAAM,MAAM,gBAAgB,CAAC,CAAC,KAAK,UAAU,IAAI,GAAG,QAAQ,CAC3E;CACF,CAAC,CACH;CACA,OAAO,IAAI,IAAI,OAAO,QAAQ,UAAU,UAAU,IAAI,CAAC;AACzD;AAEA,SAAS,aAAa,MAAc,KAA4B,UAAiC;CAC/F,MAAM,OAAO,gBAAgB;CAC7B,MAAM,UAAU,KAAK,WAAW;CAChC,OAAO;EACL,OAAO,MAAO,UAAU,cAAc,cAAe;EACrD,UAAU,GAAG,KAAK,gBAAgB,mBAAmB,QAAQ;EAC7D,eAAe,GAAG,KAAK;EACvB;CACF;AACF"}
|