@storyshelf/app 0.7.0 → 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/dist/app-types.d.mts +1 -3
- package/dist/asset-manifest.mjs +1 -2
- package/dist/assets/htmx.mjs +1 -2
- package/dist/assets/mark.mjs +1 -2
- package/dist/boot-tasks.mjs +1 -2
- package/dist/build-state-flags.mjs +1 -2
- package/dist/build-state-setup.mjs +1 -2
- package/dist/capture-alerts.mjs +1 -2
- package/dist/capture-process.mjs +1 -2
- package/dist/capture-setup.mjs +1 -2
- package/dist/connections.mjs +1 -2
- package/dist/consumer-setup.mjs +1 -2
- package/dist/credential-keys.mjs +1 -2
- package/dist/credentials.mjs +1 -2
- package/dist/git-status.mjs +1 -2
- package/dist/hidden-data.mjs +1 -2
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +2 -3
- package/dist/insights/budget.mjs +1 -2
- package/dist/insights/deps.mjs +1 -2
- package/dist/insights/errors.mjs +1 -2
- package/dist/insights/health-panel.mjs +0 -1
- package/dist/insights/job.mjs +1 -2
- package/dist/insights/load-health.mjs +1 -2
- package/dist/insights/load-triage.mjs +1 -2
- package/dist/insights/panel.mjs +1 -2
- package/dist/insights/request.mjs +1 -2
- package/dist/insights/stale.mjs +1 -2
- package/dist/insights/usage-report.mjs +1 -2
- package/dist/insights/view.mjs +1 -2
- package/dist/job-canceller.mjs +1 -2
- package/dist/job-queue.mjs +1 -2
- package/dist/jobs-setup.mjs +1 -2
- package/dist/lifecycle.mjs +2 -3
- package/dist/lifecycle.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +1 -2
- package/dist/middleware/csrf.mjs +1 -2
- package/dist/middleware/rate-limit.mjs +1 -2
- package/dist/middleware/request-log.mjs +1 -2
- package/dist/middleware/require-review.mjs +1 -2
- package/dist/middleware/require-testing.mjs +1 -2
- package/dist/middleware/server-timing.mjs +1 -2
- package/dist/middleware/setup-gate.mjs +1 -2
- package/dist/middleware/store-scope.mjs +1 -2
- package/dist/notifications-pause.mjs +1 -2
- package/dist/notify.d.mts +1 -3
- package/dist/notify.mjs +1 -2
- package/dist/pages/a11y-violations.mjs +1 -2
- package/dist/pages/admin-ai.mjs +1 -2
- package/dist/pages/admin-system-origin.mjs +19 -10
- package/dist/pages/admin-system-origin.mjs.map +1 -1
- package/dist/pages/admin-system-targets.mjs +1 -2
- package/dist/pages/admin-system.mjs +1 -2
- package/dist/pages/build-detail.mjs +1 -2
- package/dist/pages/build-diff-actions.mjs +1 -2
- package/dist/pages/build-diff-comments.mjs +1 -2
- 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 +1 -2
- package/dist/pages/build-diff-header.mjs +1 -2
- package/dist/pages/build-diff-insight.mjs +1 -2
- package/dist/pages/build-diff-model.mjs +20 -3
- package/dist/pages/build-diff-model.mjs.map +1 -1
- package/dist/pages/build-diff-nav.mjs +1 -2
- 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 +1 -2
- package/dist/pages/build-diff-stale.mjs +1 -2
- package/dist/pages/build-diff-usage.mjs +1 -2
- package/dist/pages/build-diff-viewer.mjs +38 -12
- package/dist/pages/build-diff-viewer.mjs.map +1 -1
- package/dist/pages/build-diff.mjs +6 -12
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/build-tests.mjs +1 -2
- package/dist/pages/component-detail.mjs +1 -2
- package/dist/pages/component-stories.mjs +1 -2
- package/dist/pages/compute-jobs.mjs +1 -2
- package/dist/pages/design-links.mjs +1 -2
- package/dist/pages/environment-change.mjs +1 -2
- package/dist/pages/health-panel.mjs +1 -2
- package/dist/pages/invite.mjs +1 -2
- package/dist/pages/label-detail.mjs +1 -2
- package/dist/pages/library-grid.mjs +1 -2
- package/dist/pages/library-model.mjs +1 -2
- package/dist/pages/library-snapshots.mjs +1 -2
- package/dist/pages/library.mjs +1 -2
- package/dist/pages/login.mjs +1 -2
- package/dist/pages/notifications-paused.mjs +1 -2
- package/dist/pages/passkey-ceremony.mjs +1 -2
- package/dist/pages/profile.mjs +1 -2
- package/dist/pages/project-builds-table.mjs +1 -2
- package/dist/pages/project-builds.mjs +1 -2
- package/dist/pages/project-create.mjs +1 -2
- package/dist/pages/project-settings.mjs +1 -2
- package/dist/pages/projects-card.mjs +1 -2
- package/dist/pages/projects.mjs +1 -2
- package/dist/pages/review-changeset.mjs +1 -2
- package/dist/pages/review-comments.mjs +1 -2
- package/dist/pages/review-verdict.mjs +1 -2
- package/dist/pages/root.mjs +1 -2
- package/dist/pages/settings-ai.mjs +1 -2
- package/dist/pages/settings-capture.mjs +1 -2
- package/dist/pages/settings-design-credential.mjs +1 -2
- package/dist/pages/settings-design-links.mjs +1 -2
- package/dist/pages/settings-design.mjs +1 -2
- package/dist/pages/settings-general.mjs +1 -2
- package/dist/pages/settings-labels.mjs +1 -2
- package/dist/pages/settings-links.mjs +1 -2
- package/dist/pages/settings-members.mjs +1 -2
- package/dist/pages/settings-notifications-git.mjs +1 -2
- package/dist/pages/settings-notifications.mjs +1 -2
- package/dist/pages/settings-tests.mjs +1 -2
- 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 +1 -2
- package/dist/publish-setup.mjs +1 -2
- package/dist/retention-timer.mjs +10 -11
- package/dist/retention-timer.mjs.map +1 -1
- package/dist/review-events.mjs +1 -2
- package/dist/routers/admin-pages.mjs +1 -2
- package/dist/routers/admin-public-origin.mjs +10 -3
- package/dist/routers/admin-public-origin.mjs.map +1 -1
- package/dist/routers/admin.mjs +8 -4
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +1 -2
- package/dist/routers/attempts.mjs +1 -2
- package/dist/routers/auth-sync.mjs +1 -2
- package/dist/routers/auth.mjs +1 -2
- package/dist/routers/baseline-guard.mjs +2 -3
- 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 +1 -2
- package/dist/routers/baselines.mjs +1 -2
- package/dist/routers/build-cancel.mjs +1 -2
- package/dist/routers/build-changeset.mjs +1 -2
- package/dist/routers/build-reviews.handlers.mjs +1 -2
- package/dist/routers/build-reviews.mjs +1 -2
- package/dist/routers/builds.handlers.mjs +23 -9
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +7 -3
- 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 +1 -2
- package/dist/routers/comments.mjs +1 -2
- package/dist/routers/comparison-image.mjs +29 -0
- package/dist/routers/comparison-image.mjs.map +1 -0
- package/dist/routers/component-catalog.handlers.mjs +1 -2
- package/dist/routers/component.handlers.mjs +1 -2
- package/dist/routers/connections.mjs +1 -2
- package/dist/routers/design-connect.handlers.mjs +1 -2
- package/dist/routers/design.handlers.mjs +1 -2
- package/dist/routers/flash.mjs +1 -2
- package/dist/routers/health-report.mjs +1 -2
- package/dist/routers/health.mjs +1 -2
- package/dist/routers/helpers.mjs +1 -2
- package/dist/routers/htmx.mjs +1 -2
- package/dist/routers/insights-ui.mjs +1 -2
- package/dist/routers/insights.handlers.mjs +1 -2
- package/dist/routers/insights.mjs +1 -2
- package/dist/routers/labels.mjs +1 -2
- package/dist/routers/links.handlers.mjs +1 -2
- package/dist/routers/links.mjs +1 -2
- package/dist/routers/media.mjs +14 -5
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +1 -2
- package/dist/routers/notification-channels.mjs +1 -2
- package/dist/routers/notification-subscriptions.mjs +1 -2
- package/dist/routers/oauth-state.mjs +1 -2
- package/dist/routers/preview-base.mjs +1 -2
- package/dist/routers/preview-token.mjs +1 -2
- package/dist/routers/profile.mjs +1 -2
- package/dist/routers/projects.mjs +1 -2
- package/dist/routers/publish-upload.mjs +19 -12
- package/dist/routers/publish-upload.mjs.map +1 -1
- package/dist/routers/published-access.mjs +1 -2
- package/dist/routers/published-compression.mjs +109 -0
- package/dist/routers/published-compression.mjs.map +1 -0
- package/dist/routers/published-files.mjs +26 -9
- package/dist/routers/published-files.mjs.map +1 -1
- package/dist/routers/review-forms.mjs +1 -2
- package/dist/routers/review-next.mjs +1 -2
- package/dist/routers/review-section.handlers.mjs +1 -2
- package/dist/routers/review-switch-off.mjs +1 -2
- package/dist/routers/schemas.mjs +1 -2
- package/dist/routers/scoped-models.mjs +1 -2
- package/dist/routers/settings-ai.mjs +1 -2
- package/dist/routers/settings-capture.mjs +1 -2
- package/dist/routers/settings-design.mjs +1 -2
- package/dist/routers/settings-general.mjs +1 -2
- package/dist/routers/settings-git-channel.mjs +1 -2
- package/dist/routers/settings-labels.mjs +1 -2
- package/dist/routers/settings-links.mjs +1 -2
- package/dist/routers/settings-members.mjs +1 -2
- package/dist/routers/settings-notifications.mjs +1 -2
- package/dist/routers/settings-tokens.mjs +1 -2
- package/dist/routers/settings-webhooks.mjs +1 -2
- package/dist/routers/settings.handlers.mjs +1 -2
- package/dist/routers/settings.mjs +1 -2
- package/dist/routers/signed-storybook.mjs +5 -3
- package/dist/routers/signed-storybook.mjs.map +1 -1
- package/dist/routers/snapshot-groups.mjs +1 -2
- package/dist/routers/snapshots.mjs +1 -2
- package/dist/routers/storybook.mjs +21 -17
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +1 -2
- package/dist/routers/ui.mjs +4 -2
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +1 -2
- package/dist/routers/well-known.mjs +1 -2
- package/dist/routes/api-routes.mjs +1 -2
- package/dist/routes/classification.mjs +1 -2
- package/dist/routes/openapi-classes.mjs +1 -2
- package/dist/routes/page-routes.mjs +2 -3
- package/dist/routes/page-routes.mjs.map +1 -1
- package/dist/runtime.mjs +1 -2
- package/dist/serverless.mjs +1 -2
- package/dist/store.mjs +1 -2
- package/dist/targets.mjs +1 -2
- package/dist/ui/avatar.mjs +1 -2
- package/dist/ui/buttons.mjs +1 -2
- 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 +1 -2
- package/dist/ui/document.mjs +1 -2
- 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 +1 -2
- package/dist/ui/icons/icon.mjs +1 -2
- package/dist/ui/icons/paths.mjs +1 -2
- 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 +1 -2
- 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 +1 -2
- package/dist/ui/shell/nav.mjs +1 -2
- package/dist/ui/shell/review-script.mjs +17 -3
- package/dist/ui/shell/review-script.mjs.map +1 -1
- package/dist/ui/shell/sidebar.mjs +1 -2
- 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 +9 -9
package/dist/routers/helpers.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { getStore } from "../store.mjs";
|
|
|
5
5
|
import { sha256, timingSafeEqualString } from "@storyshelf/core/utils";
|
|
6
6
|
import { MemberModel, ProjectModel, TokenModel, UserModel } from "@storyshelf/core/models";
|
|
7
7
|
import { HTTPException } from "hono/http-exception";
|
|
8
|
-
|
|
8
|
+
// src/routers/helpers.ts
|
|
9
9
|
/** Throw a 401 Unauthorized error for unauthenticated requests. */
|
|
10
10
|
function unauthorized() {
|
|
11
11
|
throw new HTTPException(401, { message: "Unauthorized" });
|
|
@@ -120,7 +120,6 @@ function requireSiteAdmin(c) {
|
|
|
120
120
|
if (user?.role === "admin" || requestHasAdminToken(c)) return;
|
|
121
121
|
forbidden();
|
|
122
122
|
}
|
|
123
|
-
//#endregion
|
|
124
123
|
export { assertRole, currentProjectRole, findProjectBySlug, forbidden, notFound, requestHasAdminToken, requireSiteAdmin, resolveAuthorizedProject, resolveReviewer, unauthorized };
|
|
125
124
|
|
|
126
125
|
//# sourceMappingURL=helpers.mjs.map
|
package/dist/routers/htmx.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
|
|
4
|
+
// src/routers/htmx.ts
|
|
5
5
|
/** Check whether the request came from HTMX (HX-Request header). */
|
|
6
6
|
function isHxRequest(c) {
|
|
7
7
|
return c.req.header("HX-Request") === "true";
|
|
@@ -18,7 +18,6 @@ function hxRedirect(c, url) {
|
|
|
18
18
|
function hxRefresh(c) {
|
|
19
19
|
if (isHxRequest(c)) c.header("HX-Refresh", "true");
|
|
20
20
|
}
|
|
21
|
-
//#endregion
|
|
22
21
|
export { hxRedirect, hxRefresh, isHxRequest };
|
|
23
22
|
|
|
24
23
|
//# sourceMappingURL=htmx.mjs.map
|
|
@@ -10,7 +10,7 @@ import { InsightPanel } from "../pages/build-diff-insight.mjs";
|
|
|
10
10
|
import { HealthPanel } from "../pages/health-panel.mjs";
|
|
11
11
|
import { projectForRegenerate, projectForView } from "./insights.handlers.mjs";
|
|
12
12
|
import { jsx } from "hono/jsx/jsx-runtime";
|
|
13
|
-
|
|
13
|
+
// src/routers/insights-ui.tsx
|
|
14
14
|
/** Register the HTMX fragments for the project health panel. */
|
|
15
15
|
function registerHealthFragments(app) {
|
|
16
16
|
app.get("/projects/:slug/insights/health/panel", async (c) => {
|
|
@@ -58,7 +58,6 @@ function registerInsightFragments(app) {
|
|
|
58
58
|
})) : notFound();
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
-
//#endregion
|
|
62
61
|
export { registerInsightFragments };
|
|
63
62
|
|
|
64
63
|
//# sourceMappingURL=insights-ui.mjs.map
|
|
@@ -6,7 +6,7 @@ import { failWith } from "../insights/errors.mjs";
|
|
|
6
6
|
import { assertRole, requestHasAdminToken, resolveAuthorizedProject } from "./helpers.mjs";
|
|
7
7
|
import { insightDepsFromStore } from "../insights/deps.mjs";
|
|
8
8
|
import { APPROVER_ROLES, VIEW_ROLES } from "./builds.handlers.mjs";
|
|
9
|
-
|
|
9
|
+
// src/routers/insights.handlers.ts
|
|
10
10
|
/** AI deps or `501 ai-disabled` when the site has no AI configured. */
|
|
11
11
|
function requireAi() {
|
|
12
12
|
const deps = insightDepsFromStore();
|
|
@@ -39,7 +39,6 @@ async function projectForRegenerate(c, slug) {
|
|
|
39
39
|
project
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
-
//#endregion
|
|
43
42
|
export { isSiteAdmin, projectForRegenerate, projectForView, requireAi, requireProjectAi };
|
|
44
43
|
|
|
45
44
|
//# sourceMappingURL=insights.handlers.mjs.map
|
|
@@ -11,7 +11,7 @@ import { requestHealth, requestTriage } from "../insights/request.mjs";
|
|
|
11
11
|
import { isSiteAdmin, projectForRegenerate, projectForView } from "./insights.handlers.mjs";
|
|
12
12
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
13
13
|
import { InsightModel } from "@storyshelf/core/models";
|
|
14
|
-
|
|
14
|
+
// src/routers/insights.ts
|
|
15
15
|
const BUILD_PARAMS = z.object({
|
|
16
16
|
slug: z.string(),
|
|
17
17
|
buildId: z.string()
|
|
@@ -183,7 +183,6 @@ function registerInsights(app) {
|
|
|
183
183
|
registerBuildInsights(app);
|
|
184
184
|
registerHealthInsights(app);
|
|
185
185
|
}
|
|
186
|
-
//#endregion
|
|
187
186
|
export { registerInsights };
|
|
188
187
|
|
|
189
188
|
//# sourceMappingURL=insights.mjs.map
|
package/dist/routers/labels.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { labelTypeCreateSchema, labelTypeSchema, labelTypeUpdateSchema, notFound
|
|
|
7
7
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
8
|
import { LabelModel } from "@storyshelf/core/models";
|
|
9
9
|
import { HTTPException } from "hono/http-exception";
|
|
10
|
-
|
|
10
|
+
// src/routers/labels.ts
|
|
11
11
|
const VIEW_ROLES = [
|
|
12
12
|
"viewer",
|
|
13
13
|
"developer",
|
|
@@ -112,7 +112,6 @@ function registerLabels(app) {
|
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
-
//#endregion
|
|
116
115
|
export { registerLabels };
|
|
117
116
|
|
|
118
117
|
//# sourceMappingURL=labels.mjs.map
|
|
@@ -4,7 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { catalogOf, getStore } from "../store.mjs";
|
|
5
5
|
import { resolveAuthorizedProject } from "./helpers.mjs";
|
|
6
6
|
import { BuildModel, PackageUsageModel, ProjectModel, SnapshotModel, matchUpstreamStory } from "@storyshelf/core/models";
|
|
7
|
-
|
|
7
|
+
// src/routers/links.handlers.ts
|
|
8
8
|
const VIEW_ROLES = [
|
|
9
9
|
"viewer",
|
|
10
10
|
"developer",
|
|
@@ -74,7 +74,6 @@ async function linkedUsageFor(c, project, buildId, storyImportPath) {
|
|
|
74
74
|
if (!storyImportPath) return [];
|
|
75
75
|
return (await linkedUsageByStory(c, project, buildId, storyImportPath)).get(storyImportPath) ?? [];
|
|
76
76
|
}
|
|
77
|
-
//#endregion
|
|
78
77
|
export { linkedUsageByStory, linkedUsageFor };
|
|
79
78
|
|
|
80
79
|
//# sourceMappingURL=links.handlers.mjs.map
|
package/dist/routers/links.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { linkedUsageFor } from "./links.handlers.mjs";
|
|
|
9
9
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
10
10
|
import { PackageUsageModel, ProjectModel } from "@storyshelf/core/models";
|
|
11
11
|
import { HTTPException } from "hono/http-exception";
|
|
12
|
-
|
|
12
|
+
// src/routers/links.ts
|
|
13
13
|
const VIEW_ROLES = [
|
|
14
14
|
"viewer",
|
|
15
15
|
"developer",
|
|
@@ -133,7 +133,6 @@ function registerLinks(app) {
|
|
|
133
133
|
return c.json(await linkedUsageFor(c, project, build.id, snapshot.storyImportPath));
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
-
//#endregion
|
|
137
136
|
export { registerLinks };
|
|
138
137
|
|
|
139
138
|
//# sourceMappingURL=links.mjs.map
|
package/dist/routers/media.mjs
CHANGED
|
@@ -4,8 +4,9 @@ import.meta.url;
|
|
|
4
4
|
import { credentialKeys } from "../credential-keys.mjs";
|
|
5
5
|
import { getStore } from "../store.mjs";
|
|
6
6
|
import { notFound, resolveAuthorizedProject } from "./helpers.mjs";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import { readComparisonImage, validFrozenReference } from "./comparison-image.mjs";
|
|
8
|
+
import { BaselineModel, BuildModel, DesignCredentialModel, DesignNodeModel, SnapshotModel, baselineStatus } from "@storyshelf/core/models";
|
|
9
|
+
// src/routers/media.ts
|
|
9
10
|
const VIEW_ROLES = [
|
|
10
11
|
"viewer",
|
|
11
12
|
"developer",
|
|
@@ -50,10 +51,19 @@ function registerMedia(app) {
|
|
|
50
51
|
const snapshot = await findSnapshot(c.req.param("buildId"), c.req.param("snapshotId"), project.id);
|
|
51
52
|
const build = await new BuildModel(getStore().db).get(snapshot.buildId);
|
|
52
53
|
if (!build) notFound("Build not found");
|
|
54
|
+
if (c.req.query("reference") !== "default" && (snapshot.comparisonImagePath || snapshot.comparisonMetadata)) {
|
|
55
|
+
const frozen = validFrozenReference(snapshot, project.id, build.id);
|
|
56
|
+
if (!frozen) notFound("Original baseline is unavailable");
|
|
57
|
+
const image = await readComparisonImage(frozen.path, frozen.reference.imageSha256);
|
|
58
|
+
if (!image) notFound("Original baseline image is unavailable");
|
|
59
|
+
return imageResponse(image, "private, no-store");
|
|
60
|
+
}
|
|
53
61
|
const baselines = new BaselineModel(getStore().db, void 0, getStore().storage);
|
|
54
|
-
const baseline = await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser);
|
|
62
|
+
const baseline = c.req.query("reference") === "default" ? await baselines.getFor(project.id, snapshot.storyId, snapshot.viewportName, project.gitDefaultBranch, snapshot.browser) : await baselines.resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser);
|
|
63
|
+
if (c.req.query("reference") === "default" && (baseline?.id !== c.req.query("id") || baseline?.updatedAt !== c.req.query("version"))) notFound("Default baseline changed; reload the comparison");
|
|
55
64
|
if (!baseline) notFound("No baseline for this snapshot");
|
|
56
|
-
|
|
65
|
+
if (c.req.query("reference") !== "default" && snapshot.baselineVersion !== null && baselineStatus(snapshot, baseline) !== "current") notFound("Original baseline is no longer available");
|
|
66
|
+
return imageResponse(await baselines.read(baseline), "private, no-store");
|
|
57
67
|
});
|
|
58
68
|
}
|
|
59
69
|
async function findSnapshot(buildId, snapshotId, projectId) {
|
|
@@ -61,7 +71,6 @@ async function findSnapshot(buildId, snapshotId, projectId) {
|
|
|
61
71
|
if (!snapshot || snapshot.buildId !== buildId || snapshot.projectId !== projectId) notFound("Snapshot not found");
|
|
62
72
|
return snapshot;
|
|
63
73
|
}
|
|
64
|
-
//#endregion
|
|
65
74
|
export { registerMedia };
|
|
66
75
|
|
|
67
76
|
//# sourceMappingURL=media.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"media.mjs","names":[],"sources":["../../src/routers/media.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"media.mjs","names":[],"sources":["../../src/routers/media.ts"],"sourcesContent":["import {\n BaselineModel,\n DesignCredentialModel,\n DesignNodeModel,\n baselineStatus,\n} from \"@storyshelf/core/models\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport { SnapshotModel } from \"@storyshelf/core/models\";\nimport type { Snapshot } from \"@storyshelf/core/schema\";\nimport type { ProjectRole } from \"@storyshelf/core/types\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { credentialKeys } from \"../credential-keys.ts\";\nimport { getStore } from \"../store.ts\";\nimport { readComparisonImage, validFrozenReference } from \"./comparison-image.ts\";\nimport { notFound, resolveAuthorizedProject } from \"./helpers.ts\";\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\n\nconst PNG = \"image/png\";\n\nfunction imageResponse(buffer: Buffer, cacheControl = \"private, max-age=3600\"): Response {\n return new Response(new Uint8Array(buffer), {\n headers: { \"content-type\": PNG, \"cache-control\": cacheControl },\n });\n}\n\n/** Design renders are addressed by file version (`?v=`): immutable once the version matches the row. */\nconst RENDER_IMMUTABLE = \"private, max-age=31536000, immutable\";\n\n/** Register the PNG image, diff, and baseline media endpoints for snapshots. */\nexport function registerMedia(app: ShelfRouter): void {\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/image\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n return imageResponse(await getStore().storage.read(snapshot.screenshotPath));\n });\n\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/diff\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n if (!snapshot.diffPath) {\n notFound(\"No diff for this snapshot\");\n }\n return imageResponse(await getStore().storage.read(snapshot.diffPath));\n });\n\n // `:renderId` is the internal design_nodes row id (a ULID), not a provider node id.\n app.get(\"/api/v1/projects/:slug/design/renders/:renderId\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const { db, config, storage, design } = getStore();\n const credentials = new DesignCredentialModel(db, credentialKeys(config));\n const nodes = new DesignNodeModel(db, credentials, design, { storage });\n const found = await nodes.get(project.id, c.req.param(\"renderId\"));\n // A render fetched with a member's own connection is theirs alone (it may show files others cannot open).\n const node =\n found && (found.viewerId === \"\" || found.viewerId === getStore().user?.id) ? found : null;\n const png = node ? await nodes.readRender(node) : null;\n if (!png) {\n notFound(\"No design render\");\n }\n const versioned = node?.renderVersion && c.req.query(\"v\") === node.renderVersion;\n return imageResponse(png, versioned ? RENDER_IMMUTABLE : undefined);\n });\n\n app.get(\"/api/v1/projects/:slug/builds/:buildId/snapshots/:snapshotId/baseline\", async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.param(\"slug\"), ...VIEW_ROLES);\n const snapshot = await findSnapshot(\n c.req.param(\"buildId\"),\n c.req.param(\"snapshotId\"),\n project.id,\n );\n const build = await new BuildModel(getStore().db).get(snapshot.buildId);\n if (!build) {\n notFound(\"Build not found\");\n }\n if (\n c.req.query(\"reference\") !== \"default\" &&\n (snapshot.comparisonImagePath || snapshot.comparisonMetadata)\n ) {\n const frozen = validFrozenReference(snapshot, project.id, build.id);\n if (!frozen) {\n notFound(\"Original baseline is unavailable\");\n }\n const image = await readComparisonImage(frozen.path, frozen.reference.imageSha256);\n if (!image) {\n notFound(\"Original baseline image is unavailable\");\n }\n return imageResponse(image, \"private, no-store\");\n }\n const baselines = new BaselineModel(getStore().db, undefined, getStore().storage);\n const baseline =\n c.req.query(\"reference\") === \"default\"\n ? await baselines.getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n project.gitDefaultBranch,\n snapshot.browser,\n )\n : await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n if (\n c.req.query(\"reference\") === \"default\" &&\n (baseline?.id !== c.req.query(\"id\") || baseline?.updatedAt !== c.req.query(\"version\"))\n ) {\n notFound(\"Default baseline changed; reload the comparison\");\n }\n if (!baseline) {\n notFound(\"No baseline for this snapshot\");\n }\n if (\n c.req.query(\"reference\") !== \"default\" &&\n snapshot.baselineVersion !== null &&\n baselineStatus(snapshot, baseline) !== \"current\"\n ) {\n notFound(\"Original baseline is no longer available\");\n }\n return imageResponse(await baselines.read(baseline), \"private, no-store\");\n });\n}\n\nasync function findSnapshot(\n buildId: string,\n snapshotId: string,\n projectId: string,\n): Promise<Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== buildId || snapshot.projectId !== projectId) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n"],"mappings":";;;;;;;;;AAeA,MAAM,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;AAEtF,MAAM,MAAM;AAEZ,SAAS,cAAc,QAAgB,eAAe,yBAAmC;CACvF,OAAO,IAAI,SAAS,IAAI,WAAW,MAAM,GAAG,EAC1C,SAAS;EAAE,gBAAgB;EAAK,iBAAiB;CAAa,EAChE,CAAC;AACH;;AAGA,MAAM,mBAAmB;;AAGzB,SAAgB,cAAc,KAAwB;CACpD,IAAI,IAAI,sEAAsE,OAAO,MAAM;EACzF,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,MAAM,GAAG,GAAG,UAAU;EACpF,MAAM,WAAW,MAAM,aACrB,EAAE,IAAI,MAAM,SAAS,GACrB,EAAE,IAAI,MAAM,YAAY,GACxB,QAAQ,EACV;EACA,OAAO,cAAc,MAAM,SAAS,CAAC,CAAC,QAAQ,KAAK,SAAS,cAAc,CAAC;CAC7E,CAAC;CAED,IAAI,IAAI,qEAAqE,OAAO,MAAM;EACxF,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,MAAM,GAAG,GAAG,UAAU;EACpF,MAAM,WAAW,MAAM,aACrB,EAAE,IAAI,MAAM,SAAS,GACrB,EAAE,IAAI,MAAM,YAAY,GACxB,QAAQ,EACV;EACA,IAAI,CAAC,SAAS,UACZ,SAAS,2BAA2B;EAEtC,OAAO,cAAc,MAAM,SAAS,CAAC,CAAC,QAAQ,KAAK,SAAS,QAAQ,CAAC;CACvE,CAAC;CAGD,IAAI,IAAI,mDAAmD,OAAO,MAAM;EACtE,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,MAAM,GAAG,GAAG,UAAU;EACpF,MAAM,EAAE,IAAI,QAAQ,SAAS,WAAW,SAAS;EACjD,MAAM,cAAc,IAAI,sBAAsB,IAAI,eAAe,MAAM,CAAC;EACxE,MAAM,QAAQ,IAAI,gBAAgB,IAAI,aAAa,QAAQ,EAAE,QAAQ,CAAC;EACtE,MAAM,QAAQ,MAAM,MAAM,IAAI,QAAQ,IAAI,EAAE,IAAI,MAAM,UAAU,CAAC;EAEjE,MAAM,OACJ,UAAU,MAAM,aAAa,MAAM,MAAM,aAAa,SAAS,CAAC,CAAC,MAAM,MAAM,QAAQ;EACvF,MAAM,MAAM,OAAO,MAAM,MAAM,WAAW,IAAI,IAAI;EAClD,IAAI,CAAC,KACH,SAAS,kBAAkB;EAG7B,OAAO,cAAc,KADH,MAAM,iBAAiB,EAAE,IAAI,MAAM,GAAG,MAAM,KAAK,gBAC7B,mBAAmB,KAAA,CAAS;CACpE,CAAC;CAED,IAAI,IAAI,yEAAyE,OAAO,MAAM;EAC5F,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,MAAM,GAAG,GAAG,UAAU;EACpF,MAAM,WAAW,MAAM,aACrB,EAAE,IAAI,MAAM,SAAS,GACrB,EAAE,IAAI,MAAM,YAAY,GACxB,QAAQ,EACV;EACA,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,SAAS,OAAO;EACtE,IAAI,CAAC,OACH,SAAS,iBAAiB;EAE5B,IACE,EAAE,IAAI,MAAM,WAAW,MAAM,cAC5B,SAAS,uBAAuB,SAAS,qBAC1C;GACA,MAAM,SAAS,qBAAqB,UAAU,QAAQ,IAAI,MAAM,EAAE;GAClE,IAAI,CAAC,QACH,SAAS,kCAAkC;GAE7C,MAAM,QAAQ,MAAM,oBAAoB,OAAO,MAAM,OAAO,UAAU,WAAW;GACjF,IAAI,CAAC,OACH,SAAS,wCAAwC;GAEnD,OAAO,cAAc,OAAO,mBAAmB;EACjD;EACA,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,IAAI,KAAA,GAAW,SAAS,CAAC,CAAC,OAAO;EAChF,MAAM,WACJ,EAAE,IAAI,MAAM,WAAW,MAAM,YACzB,MAAM,UAAU,OACd,QAAQ,IACR,SAAS,SACT,SAAS,cACT,QAAQ,kBACR,SAAS,OACX,IACA,MAAM,UAAU,QACd,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;EACN,IACE,EAAE,IAAI,MAAM,WAAW,MAAM,cAC5B,UAAU,OAAO,EAAE,IAAI,MAAM,IAAI,KAAK,UAAU,cAAc,EAAE,IAAI,MAAM,SAAS,IAEpF,SAAS,iDAAiD;EAE5D,IAAI,CAAC,UACH,SAAS,+BAA+B;EAE1C,IACE,EAAE,IAAI,MAAM,WAAW,MAAM,aAC7B,SAAS,oBAAoB,QAC7B,eAAe,UAAU,QAAQ,MAAM,WAEvC,SAAS,0CAA0C;EAErD,OAAO,cAAc,MAAM,UAAU,KAAK,QAAQ,GAAG,mBAAmB;CAC1E,CAAC;AACH;AAEA,eAAe,aACb,SACA,YACA,WACmB;CACnB,MAAM,WAAW,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,UAAU;CACtE,IAAI,CAAC,YAAY,SAAS,YAAY,WAAW,SAAS,cAAc,WACtE,SAAS,oBAAoB;CAE/B,OAAO;AACT"}
|
package/dist/routers/members.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { groupMappingCreateSchema, groupMappingSchema, memberRoleSchema, memberS
|
|
|
7
7
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
8
|
import { MemberModel, ProjectGroupMappingModel } from "@storyshelf/core/models";
|
|
9
9
|
import { HTTPException } from "hono/http-exception";
|
|
10
|
-
|
|
10
|
+
// src/routers/members.ts
|
|
11
11
|
const VIEW_ROLES = [
|
|
12
12
|
"viewer",
|
|
13
13
|
"developer",
|
|
@@ -172,7 +172,6 @@ function registerGroupMappingRoutes(app) {
|
|
|
172
172
|
return c.body(null, 204);
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
|
-
//#endregion
|
|
176
175
|
export { registerMembers };
|
|
177
176
|
|
|
178
177
|
//# sourceMappingURL=members.mjs.map
|
|
@@ -7,7 +7,7 @@ import { notFound as notFound$1, notificationChannelCreateSchema, notificationCh
|
|
|
7
7
|
import { notificationChannelModel } from "./scoped-models.mjs";
|
|
8
8
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
9
|
import { NotificationChannelModel } from "@storyshelf/core/models";
|
|
10
|
-
|
|
10
|
+
// src/routers/notification-channels.ts
|
|
11
11
|
const ADMIN_ROLES = ["admin"];
|
|
12
12
|
/** Public channel view (secret redacted, config/events decoded). */
|
|
13
13
|
function toPublic(row) {
|
|
@@ -178,7 +178,6 @@ function registerSystemChannels(app) {
|
|
|
178
178
|
return c.body(null, 204);
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
|
-
//#endregion
|
|
182
181
|
export { registerNotificationChannels, registerSystemChannels };
|
|
183
182
|
|
|
184
183
|
//# sourceMappingURL=notification-channels.mjs.map
|
|
@@ -6,7 +6,7 @@ import { resolveAuthorizedProject, unauthorized } from "./helpers.mjs";
|
|
|
6
6
|
import { notFound, notificationSubscriptionPutSchema, notificationSubscriptionSchema, unauthorized as unauthorized$1 } from "./schemas.mjs";
|
|
7
7
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
8
|
import { NotificationSubscriptionModel } from "@storyshelf/core/models";
|
|
9
|
-
|
|
9
|
+
// src/routers/notification-subscriptions.ts
|
|
10
10
|
const VIEW_ROLES = [
|
|
11
11
|
"viewer",
|
|
12
12
|
"developer",
|
|
@@ -114,7 +114,6 @@ function registerNotificationSubscriptions(app) {
|
|
|
114
114
|
return c.json(rows.map((row) => toPublic(row)));
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
-
//#endregion
|
|
118
117
|
export { registerNotificationSubscriptions };
|
|
119
118
|
|
|
120
119
|
//# sourceMappingURL=notification-subscriptions.mjs.map
|
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { timingSafeEqualString } from "@storyshelf/core/utils";
|
|
5
5
|
import { createHmac, randomBytes } from "node:crypto";
|
|
6
|
-
|
|
6
|
+
// src/routers/oauth-state.ts
|
|
7
7
|
/** Signed, expiring OAuth `state` for "connect your account" flows (binds user, service and browser). */
|
|
8
8
|
/** State lifetime: long enough to sign in at the service, short enough to limit replay. */
|
|
9
9
|
const STATE_TTL_MS = 6e5;
|
|
@@ -39,7 +39,6 @@ function safeReturnPath(value, fallback) {
|
|
|
39
39
|
function mac(secret, payload) {
|
|
40
40
|
return createHmac("sha256", secret).update(`oauth-connect:${payload}`).digest("base64url");
|
|
41
41
|
}
|
|
42
|
-
//#endregion
|
|
43
42
|
export { safeReturnPath, signState, verifyState };
|
|
44
43
|
|
|
45
44
|
//# sourceMappingURL=oauth-state.mjs.map
|
|
@@ -6,7 +6,7 @@ import { getStore, testingEnabled } from "../store.mjs";
|
|
|
6
6
|
import { isAnonymouslyViewable, publishedAccess } from "./published-access.mjs";
|
|
7
7
|
import { STORYBOOK_TOKEN_PREFIX, signStorybookToken } from "@storyshelf/core/utils";
|
|
8
8
|
import { createUrlBuilder } from "@storyshelf/core/urls";
|
|
9
|
-
|
|
9
|
+
// src/routers/preview-base.ts
|
|
10
10
|
/** Lifetime of the token in a page: long enough to open the frame, short enough to be harmless. */
|
|
11
11
|
const PREVIEW_TOKEN_TTL = 3600;
|
|
12
12
|
/**
|
|
@@ -32,7 +32,6 @@ async function previewBaseOf(project, build) {
|
|
|
32
32
|
});
|
|
33
33
|
return `${STORYBOOK_TOKEN_PREFIX}${token}`;
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { PREVIEW_TOKEN_REFRESH, PREVIEW_TOKEN_TTL, isSignedPreviewBase, previewBaseOf };
|
|
37
36
|
|
|
38
37
|
//# sourceMappingURL=preview-base.mjs.map
|
|
@@ -6,7 +6,7 @@ import { isSignedPreviewBase, previewBaseOf } from "./preview-base.mjs";
|
|
|
6
6
|
import { SIGNED_URL_HEADERS } from "./published-files.mjs";
|
|
7
7
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
8
8
|
import { BuildModel, ProjectModel } from "@storyshelf/core/models";
|
|
9
|
-
|
|
9
|
+
// src/routers/preview-token.ts
|
|
10
10
|
/**
|
|
11
11
|
* `GET /api/v1/projects/:slug/builds/:buildId/preview-token`: a fresh root for a component page's preview frame
|
|
12
12
|
* (ADR 0006, decision 9). A frame holding a signed `/_s/<token>` URL (ADR 0007, decision 5) outlives
|
|
@@ -45,7 +45,6 @@ function registerPreviewToken(app) {
|
|
|
45
45
|
return c.json({ base }, 200, SIGNED_URL_HEADERS);
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
|
-
//#endregion
|
|
49
48
|
export { registerPreviewToken };
|
|
50
49
|
|
|
51
50
|
//# sourceMappingURL=preview-token.mjs.map
|
package/dist/routers/profile.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { NotificationSubscriptionModel, ProjectModel, UserModel } from "@storysh
|
|
|
11
11
|
import { SESSION_COOKIE } from "@storyshelf/core/types";
|
|
12
12
|
import { MIN_PASSWORD_LENGTH, unsignedToken } from "@storyshelf/core/auth";
|
|
13
13
|
import { eq, getTableColumns } from "@storyshelf/core/orm";
|
|
14
|
-
|
|
14
|
+
// src/routers/profile.ts
|
|
15
15
|
function field(form, name) {
|
|
16
16
|
const raw = form.get(name);
|
|
17
17
|
return typeof raw === "string" ? raw : "";
|
|
@@ -272,7 +272,6 @@ function readPasswordChange(form) {
|
|
|
272
272
|
next
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
|
-
//#endregion
|
|
276
275
|
export { registerProfile };
|
|
277
276
|
|
|
278
277
|
//# sourceMappingURL=profile.mjs.map
|
|
@@ -11,7 +11,7 @@ import { createRoute, z } from "@hono/zod-openapi";
|
|
|
11
11
|
import { resolveBrowsers } from "@storyshelf/core/capture";
|
|
12
12
|
import { LabelModel, MemberModel, ProjectModel } from "@storyshelf/core/models";
|
|
13
13
|
import { HTTPException } from "hono/http-exception";
|
|
14
|
-
|
|
14
|
+
// src/routers/projects.ts
|
|
15
15
|
const VIEW_ROLES = [
|
|
16
16
|
"viewer",
|
|
17
17
|
"developer",
|
|
@@ -143,7 +143,6 @@ function registerProjects(app) {
|
|
|
143
143
|
return c.body(null, 204);
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
|
-
//#endregion
|
|
147
146
|
export { registerProjects };
|
|
148
147
|
|
|
149
148
|
//# sourceMappingURL=projects.mjs.map
|
|
@@ -4,10 +4,10 @@ import.meta.url;
|
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
5
|
import { DEFAULT_SCRATCH_DIR } from "@storyshelf/core/config";
|
|
6
6
|
import { storybookZipPath } from "@storyshelf/core/utils";
|
|
7
|
-
import { FILE_BY_FILE_HINT, extractionLimitsOf, publishRoute, publishStoredZip, resolvePublishLimits } from "@storyshelf/core/capture";
|
|
7
|
+
import { FILE_BY_FILE_HINT, ZipInvalidError, ZipLimitError, extractionLimitsOf, publishRoute, publishStoredZip, resolvePublishLimits } from "@storyshelf/core/capture";
|
|
8
8
|
import { BuildModel } from "@storyshelf/core/models";
|
|
9
9
|
import { HTTPException } from "hono/http-exception";
|
|
10
|
-
|
|
10
|
+
// src/routers/publish-upload.ts
|
|
11
11
|
/**
|
|
12
12
|
* What the zip upload route does once the zip is stored (ADR 0006, decision 10): extract it
|
|
13
13
|
* inline when it is small, hand it to a `publish` job when it is larger, or refuse it with a
|
|
@@ -61,18 +61,25 @@ async function publishInline(build) {
|
|
|
61
61
|
logger.info({ buildId: build.id }, "upload statics persisted inline");
|
|
62
62
|
await publish?.completer.complete(build.id, { ok: true });
|
|
63
63
|
} catch (error) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
await publish?.completer.complete(build.id, {
|
|
70
|
-
ok: false,
|
|
71
|
-
error: message
|
|
72
|
-
});
|
|
64
|
+
if (error instanceof ZipInvalidError || error instanceof ZipLimitError) {
|
|
65
|
+
await discardZip(build.projectId, build.id);
|
|
66
|
+
throw new HTTPException(422, { message: error.message });
|
|
67
|
+
}
|
|
68
|
+
await failInlinePublish(build, error);
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
|
-
|
|
71
|
+
async function failInlinePublish(build, error) {
|
|
72
|
+
const { logger, publish } = getStore();
|
|
73
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
74
|
+
logger.error({
|
|
75
|
+
err: error,
|
|
76
|
+
buildId: build.id
|
|
77
|
+
}, "inline publish failed");
|
|
78
|
+
await publish?.completer.complete(build.id, {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: message
|
|
81
|
+
});
|
|
82
|
+
}
|
|
76
83
|
export { discardZip, planUpload, publishUpload };
|
|
77
84
|
|
|
78
85
|
//# sourceMappingURL=publish-upload.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-upload.mjs","names":[],"sources":["../../src/routers/publish-upload.ts"],"sourcesContent":["/**\n * What the zip upload route does once the zip is stored (ADR 0006, decision 10): extract it\n * inline when it is small, hand it to a `publish` job when it is larger, or refuse it with a\n * pointer to the file-by-file route on a host that runs no consumer.\n */\nimport {\n extractionLimitsOf,\n FILE_BY_FILE_HINT,\n publishRoute,\n publishStoredZip,\n resolvePublishLimits,\n} from \"@storyshelf/core/capture\";\nimport { DEFAULT_SCRATCH_DIR } from \"@storyshelf/core/config\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport type { Build } from \"@storyshelf/core/schema\";\nimport { storybookZipPath } from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { getStore } from \"../store.ts\";\n\n/** How an upload of `zipBytes` will be published, or a 413 when this host cannot publish it. */\nexport function planUpload(zipBytes: number): \"inline\" | \"job\" {\n const { config, publish } = getStore();\n const route = publishRoute(zipBytes, resolvePublishLimits(config));\n if (route === \"job\" && !publish?.enqueuePublish) {\n throw new HTTPException(413, { message: FILE_BY_FILE_HINT });\n }\n return route;\n}\n\n/** Remove a stored zip that was refused (best effort), so no orphan is left behind. */\nexport async function discardZip(projectId: string, buildId: string): Promise<void> {\n try {\n await getStore().storage.delete(storybookZipPath(projectId, buildId));\n } catch (error) {\n getStore().logger.warn({ err: error, buildId }, \"could not remove a refused zip\");\n }\n}\n\n/**\n * Publish the stored zip of `build` per `route`. Inline: extract now and complete the build (a\n * failure fails it, or, with a capture to come, is left for capture to backfill). Job: queue it.\n * Resolves to the build as it stands afterwards.\n */\nexport async function publishUpload(\n route: \"inline\" | \"job\",\n build: Build,\n reqId: string | undefined,\n): Promise<Build> {\n const { publish } = getStore();\n if (route === \"job\") {\n await publish?.enqueuePublish?.(build.id, reqId);\n return build;\n }\n await publishInline(build);\n return (await new BuildModel(getStore().db).get(build.id)) ?? build;\n}\n\nasync function publishInline(build: Build): Promise<void> {\n const { storage, config, logger, publish, db } = getStore();\n try {\n await publishStoredZip(\n {\n storage,\n scratchDir: config.scratchDir ?? DEFAULT_SCRATCH_DIR,\n limits: extractionLimitsOf(resolvePublishLimits(config)),\n db,\n logger,\n },\n { projectId: build.projectId, buildId: build.id },\n );\n logger.info({ buildId: build.id }, \"upload statics persisted inline\");\n await publish?.completer.complete(build.id, { ok: true });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n
|
|
1
|
+
{"version":3,"file":"publish-upload.mjs","names":[],"sources":["../../src/routers/publish-upload.ts"],"sourcesContent":["/**\n * What the zip upload route does once the zip is stored (ADR 0006, decision 10): extract it\n * inline when it is small, hand it to a `publish` job when it is larger, or refuse it with a\n * pointer to the file-by-file route on a host that runs no consumer.\n */\nimport {\n extractionLimitsOf,\n FILE_BY_FILE_HINT,\n publishRoute,\n publishStoredZip,\n resolvePublishLimits,\n ZipInvalidError,\n ZipLimitError,\n} from \"@storyshelf/core/capture\";\nimport { DEFAULT_SCRATCH_DIR } from \"@storyshelf/core/config\";\nimport { BuildModel } from \"@storyshelf/core/models\";\nimport type { Build } from \"@storyshelf/core/schema\";\nimport { storybookZipPath } from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { getStore } from \"../store.ts\";\n\n/** How an upload of `zipBytes` will be published, or a 413 when this host cannot publish it. */\nexport function planUpload(zipBytes: number): \"inline\" | \"job\" {\n const { config, publish } = getStore();\n const route = publishRoute(zipBytes, resolvePublishLimits(config));\n if (route === \"job\" && !publish?.enqueuePublish) {\n throw new HTTPException(413, { message: FILE_BY_FILE_HINT });\n }\n return route;\n}\n\n/** Remove a stored zip that was refused (best effort), so no orphan is left behind. */\nexport async function discardZip(projectId: string, buildId: string): Promise<void> {\n try {\n await getStore().storage.delete(storybookZipPath(projectId, buildId));\n } catch (error) {\n getStore().logger.warn({ err: error, buildId }, \"could not remove a refused zip\");\n }\n}\n\n/**\n * Publish the stored zip of `build` per `route`. Inline: extract now and complete the build (a\n * failure fails it, or, with a capture to come, is left for capture to backfill). Job: queue it.\n * Resolves to the build as it stands afterwards.\n */\nexport async function publishUpload(\n route: \"inline\" | \"job\",\n build: Build,\n reqId: string | undefined,\n): Promise<Build> {\n const { publish } = getStore();\n if (route === \"job\") {\n await publish?.enqueuePublish?.(build.id, reqId);\n return build;\n }\n await publishInline(build);\n return (await new BuildModel(getStore().db).get(build.id)) ?? build;\n}\n\nasync function publishInline(build: Build): Promise<void> {\n const { storage, config, logger, publish, db } = getStore();\n try {\n await publishStoredZip(\n {\n storage,\n scratchDir: config.scratchDir ?? DEFAULT_SCRATCH_DIR,\n limits: extractionLimitsOf(resolvePublishLimits(config)),\n db,\n logger,\n },\n { projectId: build.projectId, buildId: build.id },\n );\n logger.info({ buildId: build.id }, \"upload statics persisted inline\");\n await publish?.completer.complete(build.id, { ok: true });\n } catch (error) {\n if (error instanceof ZipInvalidError || error instanceof ZipLimitError) {\n await discardZip(build.projectId, build.id);\n throw new HTTPException(422, { message: error.message });\n }\n await failInlinePublish(build, error);\n }\n}\n\nasync function failInlinePublish(build: Build, error: unknown): Promise<void> {\n const { logger, publish } = getStore();\n const message = error instanceof Error ? error.message : String(error);\n logger.error({ err: error, buildId: build.id }, \"inline publish failed\");\n await publish?.completer.complete(build.id, { ok: false, error: message });\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAsBA,SAAgB,WAAW,UAAoC;CAC7D,MAAM,EAAE,QAAQ,YAAY,SAAS;CACrC,MAAM,QAAQ,aAAa,UAAU,qBAAqB,MAAM,CAAC;CACjE,IAAI,UAAU,SAAS,CAAC,SAAS,gBAC/B,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,kBAAkB,CAAC;CAE7D,OAAO;AACT;;AAGA,eAAsB,WAAW,WAAmB,SAAgC;CAClF,IAAI;EACF,MAAM,SAAS,CAAC,CAAC,QAAQ,OAAO,iBAAiB,WAAW,OAAO,CAAC;CACtE,SAAS,OAAO;EACd,SAAS,CAAC,CAAC,OAAO,KAAK;GAAE,KAAK;GAAO;EAAQ,GAAG,gCAAgC;CAClF;AACF;;;;;;AAOA,eAAsB,cACpB,OACA,OACA,OACgB;CAChB,MAAM,EAAE,YAAY,SAAS;CAC7B,IAAI,UAAU,OAAO;EACnB,MAAM,SAAS,iBAAiB,MAAM,IAAI,KAAK;EAC/C,OAAO;CACT;CACA,MAAM,cAAc,KAAK;CACzB,OAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,EAAE,KAAM;AAChE;AAEA,eAAe,cAAc,OAA6B;CACxD,MAAM,EAAE,SAAS,QAAQ,QAAQ,SAAS,OAAO,SAAS;CAC1D,IAAI;EACF,MAAM,iBACJ;GACE;GACA,YAAY,OAAO,cAAc;GACjC,QAAQ,mBAAmB,qBAAqB,MAAM,CAAC;GACvD;GACA;EACF,GACA;GAAE,WAAW,MAAM;GAAW,SAAS,MAAM;EAAG,CAClD;EACA,OAAO,KAAK,EAAE,SAAS,MAAM,GAAG,GAAG,iCAAiC;EACpE,MAAM,SAAS,UAAU,SAAS,MAAM,IAAI,EAAE,IAAI,KAAK,CAAC;CAC1D,SAAS,OAAO;EACd,IAAI,iBAAiB,mBAAmB,iBAAiB,eAAe;GACtE,MAAM,WAAW,MAAM,WAAW,MAAM,EAAE;GAC1C,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,MAAM,QAAQ,CAAC;EACzD;EACA,MAAM,kBAAkB,OAAO,KAAK;CACtC;AACF;AAEA,eAAe,kBAAkB,OAAc,OAA+B;CAC5E,MAAM,EAAE,QAAQ,YAAY,SAAS;CACrC,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CACrE,OAAO,MAAM;EAAE,KAAK;EAAO,SAAS,MAAM;CAAG,GAAG,uBAAuB;CACvE,MAAM,SAAS,UAAU,SAAS,MAAM,IAAI;EAAE,IAAI;EAAO,OAAO;CAAQ,CAAC;AAC3E"}
|
|
@@ -4,7 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
5
|
import { currentProjectRole } from "./helpers.mjs";
|
|
6
6
|
import { isPublicBuild } from "@storyshelf/core/models";
|
|
7
|
-
|
|
7
|
+
// src/routers/published-access.ts
|
|
8
8
|
/**
|
|
9
9
|
* The one policy for reading a build's published files (ADR 0007, decision 5; ADR 0011): a signed
|
|
10
10
|
* token naming the build, or a build anyone may open (public, or an instance without sign-in), or
|
|
@@ -46,7 +46,6 @@ function isAnonymouslyViewable(build, project) {
|
|
|
46
46
|
function publishedCorsHeaders(access) {
|
|
47
47
|
return access === "session" ? {} : { "access-control-allow-origin": "*" };
|
|
48
48
|
}
|
|
49
|
-
//#endregion
|
|
50
49
|
export { isAnonymouslyViewable, publishedAccess, publishedCorsHeaders };
|
|
51
50
|
|
|
52
51
|
//# sourceMappingURL=published-access.mjs.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { publishedGzipPath } from "@storyshelf/core/utils";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { gunzip, gzip } from "node:zlib";
|
|
8
|
+
// src/routers/published-compression.ts
|
|
9
|
+
const compress = promisify(gzip);
|
|
10
|
+
const decompress = promisify(gunzip);
|
|
11
|
+
const MAX_FILE_BYTES = 8388608;
|
|
12
|
+
const MAX_JOBS = 2;
|
|
13
|
+
const MAX_READS = 4;
|
|
14
|
+
const READ_TIMEOUT_MS = 250;
|
|
15
|
+
const states = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
/** Gzip is chosen only if explicitly accepted, or by a nonzero wildcard. */
|
|
17
|
+
function acceptsGzip(header) {
|
|
18
|
+
if (!header) return false;
|
|
19
|
+
const entries = header.split(",").map((part) => part.trim().split(";"));
|
|
20
|
+
const entry = entries.find(([name]) => name?.trim().toLowerCase() === "gzip") ?? entries.find(([name]) => name?.trim() === "*");
|
|
21
|
+
if (!entry) return false;
|
|
22
|
+
const quality = entry.slice(1).find((part) => /^\s*q\s*=/iu.test(part));
|
|
23
|
+
if (quality === void 0) return true;
|
|
24
|
+
const value = Number(quality.split("=")[1]);
|
|
25
|
+
return Number.isFinite(value) && value > 0 && value <= 1;
|
|
26
|
+
}
|
|
27
|
+
/** Return a verified existing variant, or the original while a bounded background job runs. */
|
|
28
|
+
async function publishedGzip(storage, hash, original, mime) {
|
|
29
|
+
if (typeof hash !== "string" || !eligible(hash, original, mime)) return void 0;
|
|
30
|
+
const state = stateOf(storage);
|
|
31
|
+
if (state.reads >= MAX_READS) return void 0;
|
|
32
|
+
state.reads += 1;
|
|
33
|
+
const found = await withDeadline(findVariant(storage, hash, original).finally(() => {
|
|
34
|
+
state.reads -= 1;
|
|
35
|
+
}));
|
|
36
|
+
if (found === "timeout") return void 0;
|
|
37
|
+
if (found) return found;
|
|
38
|
+
schedule(storage, state, hash, original);
|
|
39
|
+
}
|
|
40
|
+
function eligible(hash, original, mime) {
|
|
41
|
+
return typeof hash === "string" && /^[a-f0-9]{64}$/u.test(hash) && original.length > 0 && original.length <= MAX_FILE_BYTES && /^(?:text\/|application\/(?:javascript|json|xml|wasm)|image\/svg\+xml)/iu.test(mime);
|
|
42
|
+
}
|
|
43
|
+
function stateOf(storage) {
|
|
44
|
+
let state = states.get(storage);
|
|
45
|
+
if (!state) {
|
|
46
|
+
state = {
|
|
47
|
+
jobs: /* @__PURE__ */ new Map(),
|
|
48
|
+
reads: 0
|
|
49
|
+
};
|
|
50
|
+
states.set(storage, state);
|
|
51
|
+
}
|
|
52
|
+
return state;
|
|
53
|
+
}
|
|
54
|
+
async function findVariant(storage, hash, original) {
|
|
55
|
+
try {
|
|
56
|
+
const path = publishedGzipPath(hash);
|
|
57
|
+
if (!await storage.exists(path)) return void 0;
|
|
58
|
+
const bytes = await readBounded(storage, path, original.length);
|
|
59
|
+
if (!bytes || bytes.length === 0) return void 0;
|
|
60
|
+
return (await decompress(bytes, { maxOutputLength: original.length })).equals(original) ? bytes : void 0;
|
|
61
|
+
} catch {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Do not allocate an unbounded buffer when a damaged storage object is oversized. */
|
|
66
|
+
async function readBounded(storage, path, limit) {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
let size = 0;
|
|
69
|
+
for await (const chunk of await storage.readStream(path)) {
|
|
70
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
71
|
+
size += bytes.length;
|
|
72
|
+
if (size >= limit) return;
|
|
73
|
+
chunks.push(bytes);
|
|
74
|
+
}
|
|
75
|
+
return Buffer.concat(chunks, size);
|
|
76
|
+
}
|
|
77
|
+
/** No queue: at capacity a future request may try again. */
|
|
78
|
+
function schedule(storage, state, hash, original) {
|
|
79
|
+
if (state.jobs.has(hash) || state.jobs.size >= MAX_JOBS) return;
|
|
80
|
+
const job = generate(storage, hash, original).catch(() => {}).finally(() => {
|
|
81
|
+
state.jobs.delete(hash);
|
|
82
|
+
});
|
|
83
|
+
state.jobs.set(hash, job);
|
|
84
|
+
}
|
|
85
|
+
async function generate(storage, hash, original) {
|
|
86
|
+
if (createHash("sha256").update(original).digest("hex") !== hash) return;
|
|
87
|
+
const source = `content/${hash}`;
|
|
88
|
+
if (!await storage.exists(source)) return;
|
|
89
|
+
const compressed = await compress(original);
|
|
90
|
+
if (compressed.length >= original.length || !await storage.exists(source)) return;
|
|
91
|
+
const path = publishedGzipPath(hash);
|
|
92
|
+
await storage.write(path, compressed);
|
|
93
|
+
if (!await storage.exists(source)) await storage.delete(path);
|
|
94
|
+
}
|
|
95
|
+
async function withDeadline(promise) {
|
|
96
|
+
let timer;
|
|
97
|
+
try {
|
|
98
|
+
return await Promise.race([promise, new Promise((resolve) => {
|
|
99
|
+
timer = setTimeout(() => {
|
|
100
|
+
resolve("timeout");
|
|
101
|
+
}, READ_TIMEOUT_MS);
|
|
102
|
+
})]);
|
|
103
|
+
} finally {
|
|
104
|
+
if (timer) clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export { acceptsGzip, publishedGzip };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=published-compression.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"published-compression.mjs","names":[],"sources":["../../src/routers/published-compression.ts"],"sourcesContent":["/** Disposable, content-addressed gzip variants of authorized published files. */\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { publishedGzipPath } from \"@storyshelf/core/utils\";\nimport { createHash } from \"node:crypto\";\nimport { promisify } from \"node:util\";\nimport { gunzip, gzip } from \"node:zlib\";\n\nconst compress = promisify(gzip);\nconst decompress = promisify(gunzip);\nconst MAX_FILE_BYTES = 8 * 1024 * 1024;\nconst MAX_JOBS = 2;\nconst MAX_READS = 4;\nconst READ_TIMEOUT_MS = 250;\nconst states = new WeakMap<StorageAdapter, State>();\n\ninterface State {\n jobs: Map<string, Promise<void>>;\n reads: number;\n}\n\n/** Gzip is chosen only if explicitly accepted, or by a nonzero wildcard. */\nexport function acceptsGzip(header?: string): boolean {\n if (!header) return false;\n const entries = header.split(\",\").map((part) => part.trim().split(\";\"));\n const entry =\n entries.find(([name]) => name?.trim().toLowerCase() === \"gzip\") ??\n entries.find(([name]) => name?.trim() === \"*\");\n if (!entry) return false;\n const quality = entry.slice(1).find((part) => /^\\s*q\\s*=/iu.test(part));\n if (quality === undefined) return true;\n const value = Number(quality.split(\"=\")[1]);\n return Number.isFinite(value) && value > 0 && value <= 1;\n}\n\n/** Return a verified existing variant, or the original while a bounded background job runs. */\nexport async function publishedGzip(\n storage: StorageAdapter,\n hash: string | undefined,\n original: Buffer,\n mime: string,\n): Promise<Buffer | undefined> {\n if (typeof hash !== \"string\" || !eligible(hash, original, mime)) return undefined;\n const state = stateOf(storage);\n if (state.reads >= MAX_READS) return undefined;\n state.reads += 1;\n const read = findVariant(storage, hash, original).finally(() => {\n state.reads -= 1;\n });\n const found = await withDeadline(read);\n if (found === \"timeout\") return undefined;\n if (found) return found;\n schedule(storage, state, hash, original);\n return undefined;\n}\n\nfunction eligible(hash: string | undefined, original: Buffer, mime: string): boolean {\n return (\n typeof hash === \"string\" &&\n /^[a-f0-9]{64}$/u.test(hash) &&\n original.length > 0 &&\n original.length <= MAX_FILE_BYTES &&\n /^(?:text\\/|application\\/(?:javascript|json|xml|wasm)|image\\/svg\\+xml)/iu.test(mime)\n );\n}\n\nfunction stateOf(storage: StorageAdapter): State {\n let state = states.get(storage);\n if (!state) {\n state = { jobs: new Map(), reads: 0 };\n states.set(storage, state);\n }\n return state;\n}\n\nasync function findVariant(\n storage: StorageAdapter,\n hash: string,\n original: Buffer,\n): Promise<Buffer | undefined> {\n try {\n const path = publishedGzipPath(hash);\n if (!(await storage.exists(path))) return undefined;\n const bytes = await readBounded(storage, path, original.length);\n if (!bytes || bytes.length === 0) return undefined;\n const decoded = await decompress(bytes, { maxOutputLength: original.length });\n return decoded.equals(original) ? bytes : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Do not allocate an unbounded buffer when a damaged storage object is oversized. */\nasync function readBounded(\n storage: StorageAdapter,\n path: string,\n limit: number,\n): Promise<Buffer | undefined> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of await storage.readStream(path)) {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);\n size += bytes.length;\n if (size >= limit) {\n return undefined;\n }\n chunks.push(bytes);\n }\n return Buffer.concat(chunks, size);\n}\n\n/** No queue: at capacity a future request may try again. */\nfunction schedule(storage: StorageAdapter, state: State, hash: string, original: Buffer): void {\n if (state.jobs.has(hash) || state.jobs.size >= MAX_JOBS) return;\n const job = generate(storage, hash, original)\n .catch(() => {})\n .finally(() => {\n state.jobs.delete(hash);\n });\n state.jobs.set(hash, job);\n}\n\nasync function generate(storage: StorageAdapter, hash: string, original: Buffer): Promise<void> {\n if (createHash(\"sha256\").update(original).digest(\"hex\") !== hash) return;\n const source = `content/${hash}`;\n if (!(await storage.exists(source))) return;\n const compressed = await compress(original);\n if (compressed.length >= original.length || !(await storage.exists(source))) return;\n const path = publishedGzipPath(hash);\n await storage.write(path, compressed);\n // A purge can remove the original while compression is in flight.\n if (!(await storage.exists(source))) await storage.delete(path);\n}\n\nasync function withDeadline<T>(promise: Promise<T>): Promise<T | \"timeout\"> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => {\n resolve(\"timeout\");\n }, READ_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n"],"mappings":"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"}
|