@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/publish-setup.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { DEFAULT_SCRATCH_DIR } from "@storyshelf/core/config";
|
|
5
5
|
import { createCatalogHook, createCatalogLoader, createPublishCompleter, createPublishHandler, extractionLimitsOf, resolvePublishLimits } from "@storyshelf/core/capture";
|
|
6
|
-
|
|
6
|
+
// src/publish-setup.ts
|
|
7
7
|
/**
|
|
8
8
|
* Publishing wiring (ADR 0006, decision 10): the completer every upload route ends in, and the
|
|
9
9
|
* `publish` job kind on the shared database queue. Its handler runs on every instance with a
|
|
@@ -83,7 +83,6 @@ function publishHooks(catalog, captureWillRun, delivery) {
|
|
|
83
83
|
}
|
|
84
84
|
];
|
|
85
85
|
}
|
|
86
|
-
//#endregion
|
|
87
86
|
export { setupPublishing };
|
|
88
87
|
|
|
89
88
|
//# sourceMappingURL=publish-setup.mjs.map
|
package/dist/retention-timer.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import "node:url";
|
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { ProjectModel } from "@storyshelf/core/models";
|
|
5
5
|
import { Retention, purgeAiData } from "@storyshelf/core/retention";
|
|
6
|
-
|
|
6
|
+
// src/retention-timer.ts
|
|
7
7
|
const DEFAULT_BRANCH_TTL_DAYS = 30;
|
|
8
8
|
const DEFAULT_BRANCH_GC_INTERVAL_MS = 864e5;
|
|
9
9
|
/**
|
|
@@ -17,16 +17,20 @@ const DEFAULT_BRANCH_GC_INTERVAL_MS = 864e5;
|
|
|
17
17
|
* @param logger - Structured logger for sweep diagnostics
|
|
18
18
|
* @returns Timer handle with `stop()`, or `null` if GC is disabled
|
|
19
19
|
*/
|
|
20
|
-
function startBranchGcTimer(db, storage, config, logger) {
|
|
20
|
+
function startBranchGcTimer(db, storage, config, logger, testing = false) {
|
|
21
21
|
const ttlDays = config.branchTtlDays ?? DEFAULT_BRANCH_TTL_DAYS;
|
|
22
22
|
if (ttlDays === null) return null;
|
|
23
23
|
const intervalMs = config.branchGcIntervalMs ?? DEFAULT_BRANCH_GC_INTERVAL_MS;
|
|
24
24
|
if (intervalMs <= 0) return null;
|
|
25
|
-
return scheduleBranchGc(createBranchGcRunner(db, storage, logger, ttlDays), intervalMs);
|
|
25
|
+
return scheduleBranchGc(createBranchGcRunner(db, storage, logger, ttlDays, testing), intervalMs);
|
|
26
26
|
}
|
|
27
|
-
function createBranchGcRunner(db, storage, logger, ttlDays) {
|
|
27
|
+
function createBranchGcRunner(db, storage, logger, ttlDays, testing) {
|
|
28
28
|
return async () => {
|
|
29
29
|
try {
|
|
30
|
+
if (testing) {
|
|
31
|
+
await purgeAiData(db, /* @__PURE__ */ new Date(), logger);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
30
34
|
const projects = await new ProjectModel(db).list();
|
|
31
35
|
const retention = new Retention(db, storage, {
|
|
32
36
|
builds: db.tables.builds,
|
|
@@ -47,20 +51,15 @@ function createBranchGcRunner(db, storage, logger, ttlDays) {
|
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
53
|
function scheduleBranchGc(run, intervalMs) {
|
|
50
|
-
const timeout = setTimeout(() =>
|
|
51
|
-
run();
|
|
52
|
-
}, Math.min(36e5, intervalMs));
|
|
54
|
+
const timeout = setTimeout(() => run(), Math.min(36e5, intervalMs));
|
|
53
55
|
timeout.unref?.();
|
|
54
|
-
const interval = setInterval(() =>
|
|
55
|
-
run();
|
|
56
|
-
}, intervalMs);
|
|
56
|
+
const interval = setInterval(() => run(), intervalMs);
|
|
57
57
|
interval.unref?.();
|
|
58
58
|
return { stop: () => {
|
|
59
59
|
clearTimeout(timeout);
|
|
60
60
|
clearInterval(interval);
|
|
61
61
|
} };
|
|
62
62
|
}
|
|
63
|
-
//#endregion
|
|
64
63
|
export { startBranchGcTimer };
|
|
65
64
|
|
|
66
65
|
//# sourceMappingURL=retention-timer.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retention-timer.mjs","names":[],"sources":["../src/retention-timer.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { ShelfConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\n/* oxlint-disable typescript/promise-function-async -- interval helpers return promise factories */\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { Retention, purgeAiData } from \"@storyshelf/core/retention\";\n\nconst DEFAULT_BRANCH_TTL_DAYS = 30;\nconst DEFAULT_BRANCH_GC_INTERVAL_MS = 86_400_000;\n\n/** Handle for the background branch-GC interval; call `stop()` on server shutdown. */\nexport interface BranchGcTimer {\n stop(): void;\n}\n\n/**\n * Start a daily sweep that removes baselines for branches that have not\n * been updated within `branchTtlDays`. Returns a timer handle or `null`\n * when GC is disabled (`branchTtlDays: null` or non-positive interval).\n *\n * @param db - Database adapter to list projects\n * @param storage - Storage adapter for baseline cleanup\n * @param config - Shelf config supplying `branchTtlDays` and `branchGcIntervalMs`\n * @param logger - Structured logger for sweep diagnostics\n * @returns Timer handle with `stop()`, or `null` if GC is disabled\n */\nexport function startBranchGcTimer(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n config: ShelfConfig,\n logger: Logger,\n): BranchGcTimer | null {\n const ttlDays = config.branchTtlDays ?? DEFAULT_BRANCH_TTL_DAYS;\n if (ttlDays === null) {\n return null;\n }\n const intervalMs = config.branchGcIntervalMs ?? DEFAULT_BRANCH_GC_INTERVAL_MS;\n if (intervalMs <= 0) {\n return null;\n }\n const run = createBranchGcRunner(db, storage, logger, ttlDays);\n return scheduleBranchGc(run, intervalMs);\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nfunction createBranchGcRunner(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n logger: Logger,\n ttlDays: number,\n): () => Promise<void> {\n return async (): Promise<void> => {\n try {\n const projects = await new ProjectModel(db).list();\n const retention = new Retention(\n db,\n storage,\n {\n builds: db.tables.builds,\n buildLabels: db.tables.buildLabels,\n baselines: db.tables.baselines,\n },\n logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purgeStaleBranches(project, ttlDays)),\n );\n const totalBranches = results.reduce(\n (sum: number, r: { removedBranches: number }) => sum + r.removedBranches,\n 0,\n );\n const totalBaselines = results.reduce(\n (sum: number, r: { removedBaselines: number }) => sum + r.removedBaselines,\n 0,\n );\n await purgeAiData(db, new Date(), logger);\n if (totalBranches > 0 || totalBaselines > 0) {\n logger.info({ totalBranches, totalBaselines }, \"branch GC daily sweep complete\");\n }\n } catch (error) {\n logger.error({ err: error }, \"branch GC sweep failed\");\n }\n };\n}\n\nfunction scheduleBranchGc(run: () => Promise<void>, intervalMs: number): BranchGcTimer {\n const firstDelay = Math.min(3_600_000, intervalMs);\n
|
|
1
|
+
{"version":3,"file":"retention-timer.mjs","names":[],"sources":["../src/retention-timer.ts"],"sourcesContent":["import type { DatabaseAdapter } from \"@storyshelf/core/adapter/database\";\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport type { ShelfConfig } from \"@storyshelf/core/config\";\nimport type { Logger } from \"@storyshelf/core/logger\";\n/* oxlint-disable typescript/promise-function-async -- interval helpers return promise factories */\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { Retention, purgeAiData } from \"@storyshelf/core/retention\";\n\nconst DEFAULT_BRANCH_TTL_DAYS = 30;\nconst DEFAULT_BRANCH_GC_INTERVAL_MS = 86_400_000;\n\n/** Handle for the background branch-GC interval; call `stop()` on server shutdown. */\nexport interface BranchGcTimer {\n stop(): void;\n}\n\n/**\n * Start a daily sweep that removes baselines for branches that have not\n * been updated within `branchTtlDays`. Returns a timer handle or `null`\n * when GC is disabled (`branchTtlDays: null` or non-positive interval).\n *\n * @param db - Database adapter to list projects\n * @param storage - Storage adapter for baseline cleanup\n * @param config - Shelf config supplying `branchTtlDays` and `branchGcIntervalMs`\n * @param logger - Structured logger for sweep diagnostics\n * @returns Timer handle with `stop()`, or `null` if GC is disabled\n */\nexport function startBranchGcTimer(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n config: ShelfConfig,\n logger: Logger,\n testing = false,\n): BranchGcTimer | null {\n const ttlDays = config.branchTtlDays ?? DEFAULT_BRANCH_TTL_DAYS;\n if (ttlDays === null) {\n return null;\n }\n const intervalMs = config.branchGcIntervalMs ?? DEFAULT_BRANCH_GC_INTERVAL_MS;\n if (intervalMs <= 0) {\n return null;\n }\n const run = createBranchGcRunner(db, storage, logger, ttlDays, testing);\n return scheduleBranchGc(run, intervalMs);\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nfunction createBranchGcRunner(\n db: DatabaseAdapter,\n storage: StorageAdapter,\n logger: Logger,\n ttlDays: number,\n testing: boolean,\n): () => Promise<void> {\n // oxlint-disable-next-line eslint/max-statements -- the sweep and its error boundary are kept together\n return async (): Promise<void> => {\n try {\n // Baseline removal must be coordinated with references before testing deployments can GC.\n // Keep independent AI-data cleanup running even while branch GC is disabled.\n if (testing) {\n await purgeAiData(db, new Date(), logger);\n return;\n }\n const projects = await new ProjectModel(db).list();\n const retention = new Retention(\n db,\n storage,\n {\n builds: db.tables.builds,\n buildLabels: db.tables.buildLabels,\n baselines: db.tables.baselines,\n },\n logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purgeStaleBranches(project, ttlDays)),\n );\n const totalBranches = results.reduce(\n (sum: number, r: { removedBranches: number }) => sum + r.removedBranches,\n 0,\n );\n const totalBaselines = results.reduce(\n (sum: number, r: { removedBaselines: number }) => sum + r.removedBaselines,\n 0,\n );\n await purgeAiData(db, new Date(), logger);\n if (totalBranches > 0 || totalBaselines > 0) {\n logger.info({ totalBranches, totalBaselines }, \"branch GC daily sweep complete\");\n }\n } catch (error) {\n logger.error({ err: error }, \"branch GC sweep failed\");\n }\n };\n}\n\nfunction scheduleBranchGc(run: () => Promise<void>, intervalMs: number): BranchGcTimer {\n const firstDelay = Math.min(3_600_000, intervalMs);\n // oxlint-disable-next-line typescript/no-misused-promises, typescript/strict-void-return -- run catches failures; returning it allows deterministic completion in tests\n const timeout = setTimeout(() => run(), firstDelay);\n timeout.unref?.();\n\n // oxlint-disable-next-line typescript/no-misused-promises, typescript/strict-void-return -- run catches failures\n const interval = setInterval(() => run(), intervalMs);\n interval.unref?.();\n\n return {\n stop: () => {\n clearTimeout(timeout);\n clearInterval(interval);\n },\n };\n}\n"],"mappings":";;;;;;AAQA,MAAM,0BAA0B;AAChC,MAAM,gCAAgC;;;;;;;;;;;;AAkBtC,SAAgB,mBACd,IACA,SACA,QACA,QACA,UAAU,OACY;CACtB,MAAM,UAAU,OAAO,iBAAiB;CACxC,IAAI,YAAY,MACd,OAAO;CAET,MAAM,aAAa,OAAO,sBAAsB;CAChD,IAAI,cAAc,GAChB,OAAO;CAGT,OAAO,iBADK,qBAAqB,IAAI,SAAS,QAAQ,SAAS,OACrC,GAAG,UAAU;AACzC;AAGA,SAAS,qBACP,IACA,SACA,QACA,SACA,SACqB;CAErB,OAAO,YAA2B;EAChC,IAAI;GAGF,IAAI,SAAS;IACX,MAAM,YAAY,oBAAI,IAAI,KAAK,GAAG,MAAM;IACxC;GACF;GACA,MAAM,WAAW,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,KAAK;GACjD,MAAM,YAAY,IAAI,UACpB,IACA,SACA;IACE,QAAQ,GAAG,OAAO;IAClB,aAAa,GAAG,OAAO;IACvB,WAAW,GAAG,OAAO;GACvB,GACA,MACF;GACA,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,KAAK,YAAY,UAAU,mBAAmB,SAAS,OAAO,CAAC,CAC1E;GACA,MAAM,gBAAgB,QAAQ,QAC3B,KAAa,MAAmC,MAAM,EAAE,iBACzD,CACF;GACA,MAAM,iBAAiB,QAAQ,QAC5B,KAAa,MAAoC,MAAM,EAAE,kBAC1D,CACF;GACA,MAAM,YAAY,oBAAI,IAAI,KAAK,GAAG,MAAM;GACxC,IAAI,gBAAgB,KAAK,iBAAiB,GACxC,OAAO,KAAK;IAAE;IAAe;GAAe,GAAG,gCAAgC;EAEnF,SAAS,OAAO;GACd,OAAO,MAAM,EAAE,KAAK,MAAM,GAAG,wBAAwB;EACvD;CACF;AACF;AAEA,SAAS,iBAAiB,KAA0B,YAAmC;CAGrF,MAAM,UAAU,iBAAiB,IAAI,GAFlB,KAAK,IAAI,MAAW,UAEU,CAAC;CAClD,QAAQ,QAAQ;CAGhB,MAAM,WAAW,kBAAkB,IAAI,GAAG,UAAU;CACpD,SAAS,QAAQ;CAEjB,OAAO,EACL,YAAY;EACV,aAAa,OAAO;EACpB,cAAc,QAAQ;CACxB,EACF;AACF"}
|
package/dist/review-events.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { credentialKeys } from "./credential-keys.mjs";
|
|
|
5
5
|
import { getStore } from "./store.mjs";
|
|
6
6
|
import { notifyProjectWith } from "./notify.mjs";
|
|
7
7
|
import { emitWebhookEvent } from "@storyshelf/core/adapter/webhook-events";
|
|
8
|
-
|
|
8
|
+
// src/review-events.ts
|
|
9
9
|
/**
|
|
10
10
|
* Review events (ADR 0013, decisions 4 and 10; ADR 0014, decision 10). They ride the existing
|
|
11
11
|
* paths: signed webhooks to the project's subscribers and the notification fan-out (stored
|
|
@@ -42,7 +42,6 @@ function reviewRequestedData(build) {
|
|
|
42
42
|
message: build.message
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
//#endregion
|
|
46
45
|
export { REVIEW_EVENTS, announceReview, announceReviewWith, reviewRequestedData };
|
|
47
46
|
|
|
48
47
|
//# sourceMappingURL=review-events.mjs.map
|
|
@@ -15,7 +15,7 @@ import { hxRedirect } from "./htmx.mjs";
|
|
|
15
15
|
import { flash } from "./flash.mjs";
|
|
16
16
|
import { collectHealthReport } from "./health-report.mjs";
|
|
17
17
|
import { describeCaptureHost } from "@storyshelf/core/utils";
|
|
18
|
-
|
|
18
|
+
// src/routers/admin-pages.ts
|
|
19
19
|
/** Register the site-admin System page (adapter inventory + in-depth health). */
|
|
20
20
|
function registerAdminPages(app, deps, auth) {
|
|
21
21
|
app.get("/admin", async (c) => {
|
|
@@ -84,7 +84,6 @@ async function publicOriginView() {
|
|
|
84
84
|
learns: authEnabled
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
//#endregion
|
|
88
87
|
export { registerAdminPages };
|
|
89
88
|
|
|
90
89
|
//# sourceMappingURL=admin-pages.mjs.map
|
|
@@ -8,7 +8,7 @@ import { flash } from "./flash.mjs";
|
|
|
8
8
|
import { requestOpenBuildStatesNow } from "../git-status.mjs";
|
|
9
9
|
import { normalizeOrigin } from "@storyshelf/core/utils";
|
|
10
10
|
import { HTTPException } from "hono/http-exception";
|
|
11
|
-
|
|
11
|
+
// src/routers/admin-public-origin.ts
|
|
12
12
|
/**
|
|
13
13
|
* The public origin, learnt from an admin's browser (ADR 0007, decision 6): the System page reports
|
|
14
14
|
* `location.origin`, an admin confirms or replaces it, and background jobs read it from the database.
|
|
@@ -20,6 +20,13 @@ function registerPublicOriginRoutes(app) {
|
|
|
20
20
|
app.post("/admin/public-origin", handleSet);
|
|
21
21
|
app.post("/admin/public-origin/clear", handleClear);
|
|
22
22
|
}
|
|
23
|
+
/** What an admin is told when the origin is not theirs to set. */
|
|
24
|
+
const FIXED_ORIGIN_MESSAGE = "The public origin comes from PUBLIC_BASE_URL or the hosting platform, so there is nothing to confirm";
|
|
25
|
+
/** Whether the configuration or the platform answers: an origin an admin cannot confirm or replace. */
|
|
26
|
+
async function isFixed() {
|
|
27
|
+
const source = (await getStore().publicOrigin?.resolve())?.source;
|
|
28
|
+
return source === "config" || source === "platform";
|
|
29
|
+
}
|
|
23
30
|
/** The origin a report names, once it matches what the browser says it used; throws 400 otherwise. */
|
|
24
31
|
async function reportedOrigin(c) {
|
|
25
32
|
const body = await c.req.json().catch(() => null);
|
|
@@ -31,7 +38,7 @@ async function reportedOrigin(c) {
|
|
|
31
38
|
async function handleReport(c) {
|
|
32
39
|
requireSiteAdmin(c);
|
|
33
40
|
const { authEnabled, publicOrigin, user } = getStore();
|
|
34
|
-
if (!authEnabled || publicOrigin === void 0) return c.body(null, 204);
|
|
41
|
+
if (!authEnabled || publicOrigin === void 0 || await isFixed()) return c.body(null, 204);
|
|
35
42
|
const reported = await reportedOrigin(c);
|
|
36
43
|
const before = await publicOrigin.stored();
|
|
37
44
|
const wasResolved = await publicOrigin.url() !== void 0;
|
|
@@ -48,6 +55,7 @@ async function resumeIfResolved(wasResolved) {
|
|
|
48
55
|
}
|
|
49
56
|
/** Store what an admin typed; the message says why not when it is no origin. */
|
|
50
57
|
async function storeTyped(c) {
|
|
58
|
+
if (await isFixed()) return FIXED_ORIGIN_MESSAGE;
|
|
51
59
|
const { publicOrigin, user } = getStore();
|
|
52
60
|
const origin = (await c.req.formData()).get("origin");
|
|
53
61
|
try {
|
|
@@ -79,7 +87,6 @@ async function handleClear(c) {
|
|
|
79
87
|
flash(c, "Stored public origin removed");
|
|
80
88
|
return hxRedirect(c, "/admin");
|
|
81
89
|
}
|
|
82
|
-
//#endregion
|
|
83
90
|
export { registerPublicOriginRoutes };
|
|
84
91
|
|
|
85
92
|
//# sourceMappingURL=admin-public-origin.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin-public-origin.mjs","names":[],"sources":["../../src/routers/admin-public-origin.ts"],"sourcesContent":["/**\n * The public origin, learnt from an admin's browser (ADR 0007, decision 6): the System page reports\n * `location.origin`, an admin confirms or replaces it, and background jobs read it from the database.\n * Site-admin and CSRF protected; with authentication off there is no admin, so nothing is learnt.\n */\nimport { normalizeOrigin } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { requestOpenBuildStatesNow } from \"../git-status.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** Register the public-origin routes of the System page. */\nexport function registerPublicOriginRoutes(app: ShelfRouter): void {\n app.post(\"/admin/public-origin/report\", handleReport);\n app.post(\"/admin/public-origin\", handleSet);\n app.post(\"/admin/public-origin/clear\", handleClear);\n}\n\n/** The origin a report names, once it matches what the browser says it used; throws 400 otherwise. */\nasync function reportedOrigin(c: Context): Promise<string> {\n const body = (await c.req.json().catch(() => null)) as { origin?: unknown } | null;\n const reported = typeof body?.origin === \"string\" ? normalizeOrigin(body.origin) : undefined;\n // The browser states the origin it used itself; a report that disagrees is not one.\n if (reported === undefined || c.req.header(\"origin\") !== reported) {\n throw new HTTPException(400, { message: \"Origin does not match the request\" });\n }\n return reported;\n}\n\n/** The page reports the origin it was loaded from; the first report is kept. */\nasync function handleReport(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { authEnabled, publicOrigin, user } = getStore();\n if (!authEnabled || publicOrigin === undefined) {\n return c.body(null, 204);\n }\n const reported = await reportedOrigin(c);\n const before = await publicOrigin.stored();\n const wasResolved = (await publicOrigin.url()) !== undefined;\n const stored = await publicOrigin.report(reported, user?.id);\n await resumeIfResolved(wasResolved);\n return c.json(stored, before === null ? 201 : 200);\n}\n\n/**\n * Notifications were paused and an origin now resolves (an admin confirmed one; a mere report\n * resolves nothing): re-request the state of open builds.\n */\nasync function resumeIfResolved(wasResolved: boolean): Promise<void> {\n if (!wasResolved && (await getStore().publicOrigin?.url()) !== undefined) {\n requestOpenBuildStatesNow().catch(() => 0);\n }\n}\n\n/** Store what an admin typed; the message says why not when it is no origin. */\nasync function storeTyped(c: Context): Promise<string> {\n const { publicOrigin, user } = getStore();\n const origin = (await c.req.formData()).get(\"origin\");\n try {\n const wasResolved = (await publicOrigin?.url()) !== undefined;\n await publicOrigin?.set(typeof origin === \"string\" ? origin : \"\", user?.id);\n await resumeIfResolved(wasResolved);\n return \"Public origin saved\";\n } catch (error) {\n return error instanceof Error ? error.message : \"Invalid origin\";\n }\n}\n\n/** An admin confirmed the detected origin or entered another. */\nasync function handleSet(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { authEnabled, publicOrigin } = getStore();\n if (!authEnabled || publicOrigin === undefined) {\n flash(\n c,\n \"The public origin is learnt from an admin's browser, which needs authentication\",\n \"warning\",\n );\n return hxRedirect(c, \"/admin\");\n }\n const message = await storeTyped(c);\n flash(c, message, message === \"Public origin saved\" ? \"success\" : \"warning\");\n return hxRedirect(c, \"/admin\");\n}\n\n/** An admin dropped the stored origin; the next admin visit reports a fresh one. */\nasync function handleClear(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { publicOrigin } = getStore();\n await publicOrigin?.clear();\n flash(c, \"Stored public origin removed\");\n return hxRedirect(c, \"/admin\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,SAAgB,2BAA2B,KAAwB;CACjE,IAAI,KAAK,+BAA+B,YAAY;CACpD,IAAI,KAAK,wBAAwB,SAAS;CAC1C,IAAI,KAAK,8BAA8B,WAAW;AACpD;;AAGA,eAAe,eAAe,GAA6B;CACzD,MAAM,OAAQ,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,YAAY,IAAI;CACjD,MAAM,WAAW,OAAO,MAAM,WAAW,WAAW,gBAAgB,KAAK,MAAM,IAAI,KAAA;CAEnF,IAAI,aAAa,KAAA,KAAa,EAAE,IAAI,OAAO,QAAQ,MAAM,UACvD,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,oCAAoC,CAAC;CAE/E,OAAO;AACT;;AAGA,eAAe,aAAa,GAA+B;CACzD,iBAAiB,CAAC;CAClB,MAAM,EAAE,aAAa,cAAc,SAAS,SAAS;CACrD,IAAI,CAAC,eAAe,iBAAiB,KAAA,
|
|
1
|
+
{"version":3,"file":"admin-public-origin.mjs","names":[],"sources":["../../src/routers/admin-public-origin.ts"],"sourcesContent":["/**\n * The public origin, learnt from an admin's browser (ADR 0007, decision 6): the System page reports\n * `location.origin`, an admin confirms or replaces it, and background jobs read it from the database.\n * Site-admin and CSRF protected; with authentication off there is no admin, so nothing is learnt.\n */\nimport { normalizeOrigin } from \"@storyshelf/core/utils\";\nimport type { Context } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { requestOpenBuildStatesNow } from \"../git-status.ts\";\nimport { getStore } from \"../store.ts\";\nimport { flash } from \"./flash.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\n/** Register the public-origin routes of the System page. */\nexport function registerPublicOriginRoutes(app: ShelfRouter): void {\n app.post(\"/admin/public-origin/report\", handleReport);\n app.post(\"/admin/public-origin\", handleSet);\n app.post(\"/admin/public-origin/clear\", handleClear);\n}\n\n/** What an admin is told when the origin is not theirs to set. */\nconst FIXED_ORIGIN_MESSAGE =\n \"The public origin comes from PUBLIC_BASE_URL or the hosting platform, so there is nothing to confirm\";\n\n/** Whether the configuration or the platform answers: an origin an admin cannot confirm or replace. */\nasync function isFixed(): Promise<boolean> {\n const source = (await getStore().publicOrigin?.resolve())?.source;\n return source === \"config\" || source === \"platform\";\n}\n\n/** The origin a report names, once it matches what the browser says it used; throws 400 otherwise. */\nasync function reportedOrigin(c: Context): Promise<string> {\n const body = (await c.req.json().catch(() => null)) as { origin?: unknown } | null;\n const reported = typeof body?.origin === \"string\" ? normalizeOrigin(body.origin) : undefined;\n // The browser states the origin it used itself; a report that disagrees is not one.\n if (reported === undefined || c.req.header(\"origin\") !== reported) {\n throw new HTTPException(400, { message: \"Origin does not match the request\" });\n }\n return reported;\n}\n\n/** The page reports the origin it was loaded from; the first report is kept. */\nasync function handleReport(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { authEnabled, publicOrigin, user } = getStore();\n if (!authEnabled || publicOrigin === undefined || (await isFixed())) {\n return c.body(null, 204);\n }\n const reported = await reportedOrigin(c);\n const before = await publicOrigin.stored();\n const wasResolved = (await publicOrigin.url()) !== undefined;\n const stored = await publicOrigin.report(reported, user?.id);\n await resumeIfResolved(wasResolved);\n return c.json(stored, before === null ? 201 : 200);\n}\n\n/**\n * Notifications were paused and an origin now resolves (an admin confirmed one; a mere report\n * resolves nothing): re-request the state of open builds.\n */\nasync function resumeIfResolved(wasResolved: boolean): Promise<void> {\n if (!wasResolved && (await getStore().publicOrigin?.url()) !== undefined) {\n requestOpenBuildStatesNow().catch(() => 0);\n }\n}\n\n/** Store what an admin typed; the message says why not when it is no origin. */\nasync function storeTyped(c: Context): Promise<string> {\n if (await isFixed()) {\n return FIXED_ORIGIN_MESSAGE;\n }\n const { publicOrigin, user } = getStore();\n const origin = (await c.req.formData()).get(\"origin\");\n try {\n const wasResolved = (await publicOrigin?.url()) !== undefined;\n await publicOrigin?.set(typeof origin === \"string\" ? origin : \"\", user?.id);\n await resumeIfResolved(wasResolved);\n return \"Public origin saved\";\n } catch (error) {\n return error instanceof Error ? error.message : \"Invalid origin\";\n }\n}\n\n/** An admin confirmed the detected origin or entered another. */\nasync function handleSet(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { authEnabled, publicOrigin } = getStore();\n if (!authEnabled || publicOrigin === undefined) {\n flash(\n c,\n \"The public origin is learnt from an admin's browser, which needs authentication\",\n \"warning\",\n );\n return hxRedirect(c, \"/admin\");\n }\n const message = await storeTyped(c);\n flash(c, message, message === \"Public origin saved\" ? \"success\" : \"warning\");\n return hxRedirect(c, \"/admin\");\n}\n\n/** An admin dropped the stored origin; the next admin visit reports a fresh one. */\nasync function handleClear(c: Context): Promise<Response> {\n requireSiteAdmin(c);\n const { publicOrigin } = getStore();\n await publicOrigin?.clear();\n flash(c, \"Stored public origin removed\");\n return hxRedirect(c, \"/admin\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,SAAgB,2BAA2B,KAAwB;CACjE,IAAI,KAAK,+BAA+B,YAAY;CACpD,IAAI,KAAK,wBAAwB,SAAS;CAC1C,IAAI,KAAK,8BAA8B,WAAW;AACpD;;AAGA,MAAM,uBACJ;;AAGF,eAAe,UAA4B;CACzC,MAAM,UAAU,MAAM,SAAS,CAAC,CAAC,cAAc,QAAQ,EAAA,EAAI;CAC3D,OAAO,WAAW,YAAY,WAAW;AAC3C;;AAGA,eAAe,eAAe,GAA6B;CACzD,MAAM,OAAQ,MAAM,EAAE,IAAI,KAAK,CAAC,CAAC,YAAY,IAAI;CACjD,MAAM,WAAW,OAAO,MAAM,WAAW,WAAW,gBAAgB,KAAK,MAAM,IAAI,KAAA;CAEnF,IAAI,aAAa,KAAA,KAAa,EAAE,IAAI,OAAO,QAAQ,MAAM,UACvD,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,oCAAoC,CAAC;CAE/E,OAAO;AACT;;AAGA,eAAe,aAAa,GAA+B;CACzD,iBAAiB,CAAC;CAClB,MAAM,EAAE,aAAa,cAAc,SAAS,SAAS;CACrD,IAAI,CAAC,eAAe,iBAAiB,KAAA,KAAc,MAAM,QAAQ,GAC/D,OAAO,EAAE,KAAK,MAAM,GAAG;CAEzB,MAAM,WAAW,MAAM,eAAe,CAAC;CACvC,MAAM,SAAS,MAAM,aAAa,OAAO;CACzC,MAAM,cAAe,MAAM,aAAa,IAAI,MAAO,KAAA;CACnD,MAAM,SAAS,MAAM,aAAa,OAAO,UAAU,MAAM,EAAE;CAC3D,MAAM,iBAAiB,WAAW;CAClC,OAAO,EAAE,KAAK,QAAQ,WAAW,OAAO,MAAM,GAAG;AACnD;;;;;AAMA,eAAe,iBAAiB,aAAqC;CACnE,IAAI,CAAC,eAAgB,MAAM,SAAS,CAAC,CAAC,cAAc,IAAI,MAAO,KAAA,GAC7D,0BAA0B,CAAC,CAAC,YAAY,CAAC;AAE7C;;AAGA,eAAe,WAAW,GAA6B;CACrD,IAAI,MAAM,QAAQ,GAChB,OAAO;CAET,MAAM,EAAE,cAAc,SAAS,SAAS;CACxC,MAAM,UAAU,MAAM,EAAE,IAAI,SAAS,EAAA,CAAG,IAAI,QAAQ;CACpD,IAAI;EACF,MAAM,cAAe,MAAM,cAAc,IAAI,MAAO,KAAA;EACpD,MAAM,cAAc,IAAI,OAAO,WAAW,WAAW,SAAS,IAAI,MAAM,EAAE;EAC1E,MAAM,iBAAiB,WAAW;EAClC,OAAO;CACT,SAAS,OAAO;EACd,OAAO,iBAAiB,QAAQ,MAAM,UAAU;CAClD;AACF;;AAGA,eAAe,UAAU,GAA+B;CACtD,iBAAiB,CAAC;CAClB,MAAM,EAAE,aAAa,iBAAiB,SAAS;CAC/C,IAAI,CAAC,eAAe,iBAAiB,KAAA,GAAW;EAC9C,MACE,GACA,mFACA,SACF;EACA,OAAO,WAAW,GAAG,QAAQ;CAC/B;CACA,MAAM,UAAU,MAAM,WAAW,CAAC;CAClC,MAAM,GAAG,SAAS,YAAY,wBAAwB,YAAY,SAAS;CAC3E,OAAO,WAAW,GAAG,QAAQ;AAC/B;;AAGA,eAAe,YAAY,GAA+B;CACxD,iBAAiB,CAAC;CAClB,MAAM,EAAE,iBAAiB,SAAS;CAClC,MAAM,cAAc,MAAM;CAC1B,MAAM,GAAG,8BAA8B;CACvC,OAAO,WAAW,GAAG,QAAQ;AAC/B"}
|
package/dist/routers/admin.mjs
CHANGED
|
@@ -5,11 +5,11 @@ import { getStore } from "../store.mjs";
|
|
|
5
5
|
import { notifySystem } from "../notify.mjs";
|
|
6
6
|
import { reencryptWithCurrent } from "../credentials.mjs";
|
|
7
7
|
import { requireSiteAdmin } from "./helpers.mjs";
|
|
8
|
-
import { forbidden, purgeInputSchema, purgeSchema } from "./schemas.mjs";
|
|
8
|
+
import { errorSchema, forbidden, purgeInputSchema, purgeSchema } from "./schemas.mjs";
|
|
9
9
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
10
10
|
import { ProjectModel } from "@storyshelf/core/models";
|
|
11
11
|
import { Retention } from "@storyshelf/core/retention";
|
|
12
|
-
|
|
12
|
+
// src/routers/admin.ts
|
|
13
13
|
const purgeRoute = createRoute({
|
|
14
14
|
method: "post",
|
|
15
15
|
tags: ["Admin"],
|
|
@@ -21,6 +21,10 @@ const purgeRoute = createRoute({
|
|
|
21
21
|
content: { "application/json": { schema: purgeSchema } },
|
|
22
22
|
description: "Retention purge run"
|
|
23
23
|
},
|
|
24
|
+
503: {
|
|
25
|
+
content: { "application/json": { schema: errorSchema } },
|
|
26
|
+
description: "Testing retention requires atomic build lifecycle support"
|
|
27
|
+
},
|
|
24
28
|
...forbidden
|
|
25
29
|
}
|
|
26
30
|
});
|
|
@@ -61,6 +65,7 @@ function registerAdmin(app) {
|
|
|
61
65
|
});
|
|
62
66
|
app.openapi(purgeRoute, async (c) => {
|
|
63
67
|
requireSiteAdmin(c);
|
|
68
|
+
if (getStore().testing) return c.json({ message: "Build purge is unavailable for testing deployments until the database adapter supports safe baseline retention. Builds are retained; contact your administrator." }, 503);
|
|
64
69
|
const ttlDays = c.req.valid("json").ttlDays ?? getStore().config.purgeTtlDays ?? 30;
|
|
65
70
|
const removedBuilds = await purgeBuilds(ttlDays);
|
|
66
71
|
const branch = await purgeBranches();
|
|
@@ -74,7 +79,7 @@ function registerAdmin(app) {
|
|
|
74
79
|
removedBuilds,
|
|
75
80
|
removedBranches: branch.removedBranches,
|
|
76
81
|
removedBaselines: branch.removedBaselines
|
|
77
|
-
});
|
|
82
|
+
}, 200);
|
|
78
83
|
});
|
|
79
84
|
}
|
|
80
85
|
async function purgeBuilds(ttlDays) {
|
|
@@ -107,7 +112,6 @@ async function purgeBranches() {
|
|
|
107
112
|
removedBaselines: results.reduce((sum, r) => sum + r.removedBaselines, 0)
|
|
108
113
|
};
|
|
109
114
|
}
|
|
110
|
-
//#endregion
|
|
111
115
|
export { registerAdmin };
|
|
112
116
|
|
|
113
117
|
//# sourceMappingURL=admin.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"admin.mjs","names":["forbiddenResponse"],"sources":["../../src/routers/admin.ts"],"sourcesContent":["/* oxlint-disable typescript/promise-function-async -- admin helpers are async by design */\nimport { createRoute, z } from \"@hono/zod-openapi\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { Retention } from \"@storyshelf/core/retention\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { reencryptWithCurrent } from \"../credentials.ts\";\nimport { notifySystem } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport {
|
|
1
|
+
{"version":3,"file":"admin.mjs","names":["forbiddenResponse"],"sources":["../../src/routers/admin.ts"],"sourcesContent":["/* oxlint-disable typescript/promise-function-async -- admin helpers are async by design */\nimport { createRoute, z } from \"@hono/zod-openapi\";\nimport { ProjectModel } from \"@storyshelf/core/models\";\nimport { Retention } from \"@storyshelf/core/retention\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { reencryptWithCurrent } from \"../credentials.ts\";\nimport { notifySystem } from \"../notify.ts\";\nimport { getStore } from \"../store.ts\";\nimport { requireSiteAdmin } from \"./helpers.ts\";\nimport {\n errorSchema,\n forbidden as forbiddenResponse,\n purgeInputSchema,\n purgeSchema,\n} from \"./schemas.ts\";\nconst purgeRoute = createRoute({\n method: \"post\",\n tags: [\"Admin\"],\n summary: \"Run the retention purge\",\n path: \"/api/v1/admin/purge\",\n request: { body: { content: { \"application/json\": { schema: purgeInputSchema } } } },\n responses: {\n 200: {\n content: { \"application/json\": { schema: purgeSchema } },\n description: \"Retention purge run\",\n },\n 503: {\n content: { \"application/json\": { schema: errorSchema } },\n description: \"Testing retention requires atomic build lifecycle support\",\n },\n ...forbiddenResponse,\n },\n});\n\nconst reencryptSchema = z.object({\n reencrypted: z.number().int(),\n failed: z.number().int(),\n unreadable: z.number().int(),\n});\n\nconst reencryptRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/admin/credentials/reencrypt\",\n tags: [\"Admin\"],\n summary: \"Re-encrypt stored credentials with the current secret\",\n responses: {\n 200: {\n content: { \"application/json\": { schema: reencryptSchema } },\n description: \"Credentials re-encrypted with the current secret\",\n },\n 409: {\n content: { \"application/json\": { schema: z.object({ error: z.string() }) } },\n description: \"No previousSecret configured\",\n },\n ...forbiddenResponse,\n },\n});\n\n/** Register the site-admin endpoints: retention purge and credential re-encryption. */\nexport function registerAdmin(app: ShelfRouter): void {\n app.openapi(reencryptRoute, async (c) => {\n requireSiteAdmin(c);\n const { db, config } = getStore();\n if (!config.previousSecret) {\n return c.json({ error: \"No previousSecret is configured; nothing to re-encrypt\" }, 409);\n }\n const result = await reencryptWithCurrent(db, config);\n return c.json(\n {\n reencrypted: result.reencrypted,\n failed: result.failed,\n unreadable: result.unreadable.length,\n },\n 200,\n );\n });\n\n app.openapi(purgeRoute, async (c) => {\n requireSiteAdmin(c);\n // Until the database provides atomic reference admission and tombstones, deletion in a\n // testing deployment can race a baseline promotion. Do not expose unsafe purge.\n if (getStore().testing) {\n return c.json(\n {\n message:\n \"Build purge is unavailable for testing deployments until the database adapter supports safe baseline retention. Builds are retained; contact your administrator.\",\n },\n 503,\n );\n }\n const body = c.req.valid(\"json\");\n const ttlDays = body.ttlDays ?? getStore().config.purgeTtlDays ?? 30;\n const removedBuilds = await purgeBuilds(ttlDays);\n const branch = await purgeBranches();\n await notifySystem(\"sys:purge-completed\", {\n removedBuilds,\n removedBranches: branch.removedBranches,\n removedBaselines: branch.removedBaselines,\n ttlDays,\n });\n return c.json(\n {\n removedBuilds,\n removedBranches: branch.removedBranches,\n removedBaselines: branch.removedBaselines,\n },\n 200,\n );\n });\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nasync function purgeBuilds(ttlDays: number): Promise<number> {\n const projects = await new ProjectModel(getStore().db).list();\n const retention = new Retention(\n getStore().db,\n getStore().storage,\n {\n builds: getStore().db.tables.builds,\n buildLabels: getStore().db.tables.buildLabels,\n baselines: getStore().db.tables.baselines,\n },\n getStore().logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purge(project, { ttlDays, keepLatestPerBranch: true })),\n );\n return results.reduce((sum, result) => sum + result.removedBuilds, 0);\n}\n\n// oxlint-disable-next-line typescript/promise-function-async\nasync function purgeBranches(): Promise<{ removedBranches: number; removedBaselines: number }> {\n const branchTtl = getStore().config.branchTtlDays ?? 30;\n if (branchTtl === null) {\n return { removedBranches: 0, removedBaselines: 0 };\n }\n const projects = await new ProjectModel(getStore().db).list();\n const retention = new Retention(\n getStore().db,\n getStore().storage,\n {\n builds: getStore().db.tables.builds,\n buildLabels: getStore().db.tables.buildLabels,\n baselines: getStore().db.tables.baselines,\n },\n getStore().logger,\n );\n const results = await Promise.all(\n projects.map((project) => retention.purgeStaleBranches(project, branchTtl)),\n );\n const removedBranches = results.reduce(\n (sum: number, r: { removedBranches: number }) => sum + r.removedBranches,\n 0,\n );\n const removedBaselines = results.reduce(\n (sum: number, r: { removedBaselines: number }) => sum + r.removedBaselines,\n 0,\n );\n return { removedBranches, removedBaselines };\n}\n"],"mappings":";;;;;;;;;;;;AAeA,MAAM,aAAa,YAAY;CAC7B,QAAQ;CACR,MAAM,CAAC,OAAO;CACd,SAAS;CACT,MAAM;CACN,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,iBAAiB,EAAE,EAAE,EAAE;CACnF,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,YAAY,EAAE;GACvD,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;AAED,MAAM,kBAAkB,EAAE,OAAO;CAC/B,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI;CAC5B,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI;CACvB,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI;AAC7B,CAAC;AAED,MAAM,iBAAiB,YAAY;CACjC,QAAQ;CACR,MAAM;CACN,MAAM,CAAC,OAAO;CACd,SAAS;CACT,WAAW;EACT,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gBAAgB,EAAE;GAC3D,aAAa;EACf;EACA,KAAK;GACH,SAAS,EAAE,oBAAoB,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE;GAC3E,aAAa;EACf;EACA,GAAGA;CACL;AACF,CAAC;;AAGD,SAAgB,cAAc,KAAwB;CACpD,IAAI,QAAQ,gBAAgB,OAAO,MAAM;EACvC,iBAAiB,CAAC;EAClB,MAAM,EAAE,IAAI,WAAW,SAAS;EAChC,IAAI,CAAC,OAAO,gBACV,OAAO,EAAE,KAAK,EAAE,OAAO,yDAAyD,GAAG,GAAG;EAExF,MAAM,SAAS,MAAM,qBAAqB,IAAI,MAAM;EACpD,OAAO,EAAE,KACP;GACE,aAAa,OAAO;GACpB,QAAQ,OAAO;GACf,YAAY,OAAO,WAAW;EAChC,GACA,GACF;CACF,CAAC;CAED,IAAI,QAAQ,YAAY,OAAO,MAAM;EACnC,iBAAiB,CAAC;EAGlB,IAAI,SAAS,CAAC,CAAC,SACb,OAAO,EAAE,KACP,EACE,SACE,mKACJ,GACA,GACF;EAGF,MAAM,UADO,EAAE,IAAI,MAAM,MACN,CAAC,CAAC,WAAW,SAAS,CAAC,CAAC,OAAO,gBAAgB;EAClE,MAAM,gBAAgB,MAAM,YAAY,OAAO;EAC/C,MAAM,SAAS,MAAM,cAAc;EACnC,MAAM,aAAa,uBAAuB;GACxC;GACA,iBAAiB,OAAO;GACxB,kBAAkB,OAAO;GACzB;EACF,CAAC;EACD,OAAO,EAAE,KACP;GACE;GACA,iBAAiB,OAAO;GACxB,kBAAkB,OAAO;EAC3B,GACA,GACF;CACF,CAAC;AACH;AAGA,eAAe,YAAY,SAAkC;CAC3D,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK;CAC5D,MAAM,YAAY,IAAI,UACpB,SAAS,CAAC,CAAC,IACX,SAAS,CAAC,CAAC,SACX;EACE,QAAQ,SAAS,CAAC,CAAC,GAAG,OAAO;EAC7B,aAAa,SAAS,CAAC,CAAC,GAAG,OAAO;EAClC,WAAW,SAAS,CAAC,CAAC,GAAG,OAAO;CAClC,GACA,SAAS,CAAC,CAAC,MACb;CAIA,QAAO,MAHe,QAAQ,IAC5B,SAAS,KAAK,YAAY,UAAU,MAAM,SAAS;EAAE;EAAS,qBAAqB;CAAK,CAAC,CAAC,CAC5F,EAAA,CACe,QAAQ,KAAK,WAAW,MAAM,OAAO,eAAe,CAAC;AACtE;AAGA,eAAe,gBAAgF;CAC7F,MAAM,YAAY,SAAS,CAAC,CAAC,OAAO,iBAAiB;CACrD,IAAI,cAAc,MAChB,OAAO;EAAE,iBAAiB;EAAG,kBAAkB;CAAE;CAEnD,MAAM,WAAW,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK;CAC5D,MAAM,YAAY,IAAI,UACpB,SAAS,CAAC,CAAC,IACX,SAAS,CAAC,CAAC,SACX;EACE,QAAQ,SAAS,CAAC,CAAC,GAAG,OAAO;EAC7B,aAAa,SAAS,CAAC,CAAC,GAAG,OAAO;EAClC,WAAW,SAAS,CAAC,CAAC,GAAG,OAAO;CAClC,GACA,SAAS,CAAC,CAAC,MACb;CACA,MAAM,UAAU,MAAM,QAAQ,IAC5B,SAAS,KAAK,YAAY,UAAU,mBAAmB,SAAS,SAAS,CAAC,CAC5E;CASA,OAAO;EAAE,iBARe,QAAQ,QAC7B,KAAa,MAAmC,MAAM,EAAE,iBACzD,CAMqB;EAAG,kBAJD,QAAQ,QAC9B,KAAa,MAAoC,MAAM,EAAE,kBAC1D,CAEuC;CAAE;AAC7C"}
|
package/dist/routers/assets.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { assetManifest } from "../asset-manifest.mjs";
|
|
5
|
-
|
|
5
|
+
// src/routers/assets.ts
|
|
6
6
|
const IMMUTABLE = "public, max-age=31536000, immutable";
|
|
7
7
|
/**
|
|
8
8
|
* Serve a content-addressed asset. A matching hash is cacheable forever and
|
|
@@ -65,7 +65,6 @@ function registerAssets(app) {
|
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
|
-
//#endregion
|
|
69
68
|
export { registerAssets };
|
|
70
69
|
|
|
71
70
|
//# sourceMappingURL=assets.mjs.map
|
|
@@ -7,7 +7,7 @@ import { attemptLogSchema, attemptSchema, notFound as notFound$1, unauthorized }
|
|
|
7
7
|
import { VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
|
|
8
8
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
9
|
import { CaptureAttemptModel, CaptureLogModel } from "@storyshelf/core/models";
|
|
10
|
-
|
|
10
|
+
// src/routers/attempts.ts
|
|
11
11
|
/** Register the capture-attempt history and per-attempt log endpoints. */
|
|
12
12
|
function registerAttempts(app) {
|
|
13
13
|
app.openapi(listAttemptsRoute, async (c) => {
|
|
@@ -90,7 +90,6 @@ async function attemptForBuild(build, attemptNo) {
|
|
|
90
90
|
if (!attempt) notFound("Attempt not found");
|
|
91
91
|
return attempt;
|
|
92
92
|
}
|
|
93
|
-
//#endregion
|
|
94
93
|
export { registerAttempts };
|
|
95
94
|
|
|
96
95
|
//# sourceMappingURL=attempts.mjs.map
|
|
@@ -2,7 +2,7 @@ import "node:path";
|
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
4
|
import { MemberModel, ProjectGroupMappingModel, ProjectModel, UserModel } from "@storyshelf/core/models";
|
|
5
|
-
|
|
5
|
+
// src/routers/auth-sync.ts
|
|
6
6
|
const ROLE_RANK = {
|
|
7
7
|
viewer: 0,
|
|
8
8
|
developer: 1,
|
|
@@ -58,7 +58,6 @@ async function syncProjectMembership(members, mappings, projectId, userId, group
|
|
|
58
58
|
const existing = await members.get(projectId, userId);
|
|
59
59
|
if (!existing || existing.role !== top.role || existing.source !== `oidc:${top.groupName}`) await members.set(projectId, userId, top.role, `oidc:${top.groupName}`);
|
|
60
60
|
}
|
|
61
|
-
//#endregion
|
|
62
61
|
export { syncLoginMemberships };
|
|
63
62
|
|
|
64
63
|
//# sourceMappingURL=auth-sync.mjs.map
|
package/dist/routers/auth.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { UserModel } from "@storyshelf/core/models";
|
|
|
11
11
|
import { HTTPException } from "hono/http-exception";
|
|
12
12
|
import { SESSION_COOKIE } from "@storyshelf/core/types";
|
|
13
13
|
import { MIN_PASSWORD_LENGTH } from "@storyshelf/core/auth";
|
|
14
|
-
|
|
14
|
+
// src/routers/auth.ts
|
|
15
15
|
/** Provider ids are URL path segments: strict charset, bounded length. */
|
|
16
16
|
function validProviderId(value) {
|
|
17
17
|
return /^[a-zA-Z0-9_-]{1,64}$/u.test(value);
|
|
@@ -232,7 +232,6 @@ function registerEngineAuth(app, engine) {
|
|
|
232
232
|
return hxRedirect(c, "/auth/login");
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
|
-
//#endregion
|
|
236
235
|
export { forwardToEngine, registerEngineAuth };
|
|
237
236
|
|
|
238
237
|
//# sourceMappingURL=auth.mjs.map
|
|
@@ -4,7 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { getStore } from "../store.mjs";
|
|
5
5
|
import { BaselineModel, baselineStatus, isBaselineDrifted } from "@storyshelf/core/models";
|
|
6
6
|
import { HTTPException } from "hono/http-exception";
|
|
7
|
-
|
|
7
|
+
// src/routers/baseline-guard.ts
|
|
8
8
|
/** Refuse to promote a screenshot over a baseline the reviewer has not seen. */
|
|
9
9
|
/** Whether a snapshot is awaiting a review decision. */
|
|
10
10
|
function isOpenSnapshot(snapshot) {
|
|
@@ -71,13 +71,12 @@ async function loadBaselineView(project, build, snapshots) {
|
|
|
71
71
|
drifted: {}
|
|
72
72
|
};
|
|
73
73
|
for (const { snapshot, current } of entries) {
|
|
74
|
-
view.hasBaseline[snapshot.id] = Boolean(current);
|
|
74
|
+
view.hasBaseline[snapshot.id] = Boolean(snapshot.comparisonImagePath ?? current);
|
|
75
75
|
const status = baselineStatus(snapshot, current);
|
|
76
76
|
if (!build.isDefault && isOpenSnapshot(snapshot) && (status === "stale" || status === "removed")) view.drifted[snapshot.id] = status;
|
|
77
77
|
}
|
|
78
78
|
return view;
|
|
79
79
|
}
|
|
80
|
-
//#endregion
|
|
81
80
|
export { assertBaselineCurrent, baselineChanged, findDriftedSnapshots, isBaselineChanged, isOpenSnapshot, loadBaselineView, snapshotBaselineStatus };
|
|
82
81
|
|
|
83
82
|
//# sourceMappingURL=baseline-guard.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline-guard.mjs","names":[],"sources":["../../src/routers/baseline-guard.ts"],"sourcesContent":["/** Refuse to promote a screenshot over a baseline the reviewer has not seen. */\nimport {\n BaselineModel,\n baselineStatus,\n isBaselineDrifted,\n type BaselineStatus,\n} from \"@storyshelf/core/models\";\nimport type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { getStore } from \"../store.ts\";\n\n/** Whether a snapshot is awaiting a review decision. */\nexport function isOpenSnapshot(snapshot: Pick<Snapshot, \"status\">): boolean {\n return snapshot.status === \"new\" || snapshot.status === \"changed\";\n}\n\n/** Compare a snapshot's recorded baseline with the baseline that applies to it now. */\nexport async function snapshotBaselineStatus(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshot: Snapshot,\n): Promise<BaselineStatus> {\n const baselines = new BaselineModel(getStore().db);\n const current = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n return baselineStatus(snapshot, current);\n}\n\n/** Open snapshots whose diff no longer matches the current baseline. */\nexport async function findDriftedSnapshots(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshots: Snapshot[],\n): Promise<Map<string, BaselineStatus>> {\n const open = snapshots.filter((snapshot) => isOpenSnapshot(snapshot));\n const statuses = await Promise.all(\n open.map(async (snapshot) => await snapshotBaselineStatus(project, build, snapshot)),\n );\n const drifted = new Map<string, BaselineStatus>();\n for (const [index, snapshot] of open.entries()) {\n const status = statuses[index];\n if (status && isBaselineDrifted(status)) {\n drifted.set(snapshot.id, status);\n }\n }\n return drifted;\n}\n\n/** 409 telling the client the diff is out of date; recompute it or force the approval. */\nexport function baselineChanged(snapshotId: string, status: BaselineStatus): HTTPException {\n const body = {\n code: \"baseline_changed\",\n message:\n \"The baseline changed after this diff was computed. Re-diff the build, or approve with force.\",\n snapshotId,\n baselineStatus: status,\n };\n return new HTTPException(409, {\n res: new Response(JSON.stringify(body), {\n status: 409,\n headers: { \"content-type\": \"application/json\" },\n }),\n });\n}\n\n/** Throw {@link baselineChanged} when an open snapshot's baseline drifted and `force` is off. */\nexport async function assertBaselineCurrent(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshot: Snapshot,\n force: boolean,\n): Promise<void> {\n if (!isOpenSnapshot(snapshot)) {\n return;\n }\n const status = await snapshotBaselineStatus(project, build, snapshot);\n if (!isBaselineDrifted(status)) {\n return;\n }\n if (force) {\n getStore().logger.warn(\n { snapshotId: snapshot.id, baselineStatus: status, userId: getStore().user?.id ?? null },\n \"approved snapshot despite changed baseline (forced)\",\n );\n return;\n }\n throw baselineChanged(snapshot.id, status);\n}\n\n/** True when `error` is the 409 raised by {@link baselineChanged}. */\nexport function isBaselineChanged(error: unknown): boolean {\n return error instanceof HTTPException && error.status === 409;\n}\n\n/** Per-snapshot baseline facts the review pages need. */\nexport interface BaselineView {\n /** Snapshot ids that have a baseline to compare against. */\n hasBaseline: Record<string, boolean>;\n /** Open snapshots whose recorded baseline no longer matches the current one. */\n drifted: Record<string, \"stale\" | \"removed\">;\n}\n\n/** Resolve each snapshot's current baseline once and derive presence plus drift. */\nexport async function loadBaselineView(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\" | \"isDefault\">,\n snapshots: Snapshot[],\n): Promise<BaselineView> {\n const baselines = new BaselineModel(getStore().db);\n const entries = await Promise.all(\n snapshots.map(async (snapshot) => {\n const current = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n return { snapshot, current };\n }),\n );\n const view: BaselineView = { hasBaseline: {}, drifted: {} };\n for (const { snapshot, current } of entries) {\n view.hasBaseline[snapshot.id] = Boolean(current);\n const status = baselineStatus(snapshot, current);\n if (\n !build.isDefault &&\n isOpenSnapshot(snapshot) &&\n (status === \"stale\" || status === \"removed\")\n ) {\n view.drifted[snapshot.id] = status;\n }\n }\n return view;\n}\n"],"mappings":";;;;;;;;;AAYA,SAAgB,eAAe,UAA6C;CAC1E,OAAO,SAAS,WAAW,SAAS,SAAS,WAAW;AAC1D;;AAGA,eAAsB,uBACpB,SACA,OACA,UACyB;CAEzB,MAAM,UAAU,MAAM,IADA,cAAc,SAAS,CAAC,CAAC,EACjB,CAAC,CAAC,QAC9B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;CACA,OAAO,eAAe,UAAU,OAAO;AACzC;;AAGA,eAAsB,qBACpB,SACA,OACA,WACsC;CACtC,MAAM,OAAO,UAAU,QAAQ,aAAa,eAAe,QAAQ,CAAC;CACpE,MAAM,WAAW,MAAM,QAAQ,IAC7B,KAAK,IAAI,OAAO,aAAa,MAAM,uBAAuB,SAAS,OAAO,QAAQ,CAAC,CACrF;CACA,MAAM,0BAAU,IAAI,IAA4B;CAChD,KAAK,MAAM,CAAC,OAAO,aAAa,KAAK,QAAQ,GAAG;EAC9C,MAAM,SAAS,SAAS;EACxB,IAAI,UAAU,kBAAkB,MAAM,GACpC,QAAQ,IAAI,SAAS,IAAI,MAAM;CAEnC;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,YAAoB,QAAuC;CAQzF,OAAO,IAAI,cAAc,KAAK,EAC5B,KAAK,IAAI,SAAS,KAAK,UAAU;EAPjC,MAAM;EACN,SACE;EACF;EACA,gBAAgB;CAGoB,CAAC,GAAG;EACtC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC,EACH,CAAC;AACH;;AAGA,eAAsB,sBACpB,SACA,OACA,UACA,OACe;CACf,IAAI,CAAC,eAAe,QAAQ,GAC1B;CAEF,MAAM,SAAS,MAAM,uBAAuB,SAAS,OAAO,QAAQ;CACpE,IAAI,CAAC,kBAAkB,MAAM,GAC3B;CAEF,IAAI,OAAO;EACT,SAAS,CAAC,CAAC,OAAO,KAChB;GAAE,YAAY,SAAS;GAAI,gBAAgB;GAAQ,QAAQ,SAAS,CAAC,CAAC,MAAM,MAAM;EAAK,GACvF,qDACF;EACA;CACF;CACA,MAAM,gBAAgB,SAAS,IAAI,MAAM;AAC3C;;AAGA,SAAgB,kBAAkB,OAAyB;CACzD,OAAO,iBAAiB,iBAAiB,MAAM,WAAW;AAC5D;;AAWA,eAAsB,iBACpB,SACA,OACA,WACuB;CACvB,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,UAAU,MAAM,QAAQ,IAC5B,UAAU,IAAI,OAAO,aAAa;EAShC,OAAO;GAAE;GAAU,SAAA,MARG,UAAU,QAC9B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;EAC2B;CAC7B,CAAC,CACH;CACA,MAAM,OAAqB;EAAE,aAAa,CAAC;EAAG,SAAS,CAAC;CAAE;CAC1D,KAAK,MAAM,EAAE,UAAU,aAAa,SAAS;EAC3C,KAAK,YAAY,SAAS,MAAM,QAAQ,OAAO;EAC/
|
|
1
|
+
{"version":3,"file":"baseline-guard.mjs","names":[],"sources":["../../src/routers/baseline-guard.ts"],"sourcesContent":["/** Refuse to promote a screenshot over a baseline the reviewer has not seen. */\nimport {\n BaselineModel,\n baselineStatus,\n isBaselineDrifted,\n type BaselineStatus,\n} from \"@storyshelf/core/models\";\nimport type { Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { getStore } from \"../store.ts\";\n\n/** Whether a snapshot is awaiting a review decision. */\nexport function isOpenSnapshot(snapshot: Pick<Snapshot, \"status\">): boolean {\n return snapshot.status === \"new\" || snapshot.status === \"changed\";\n}\n\n/** Compare a snapshot's recorded baseline with the baseline that applies to it now. */\nexport async function snapshotBaselineStatus(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshot: Snapshot,\n): Promise<BaselineStatus> {\n const baselines = new BaselineModel(getStore().db);\n const current = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n return baselineStatus(snapshot, current);\n}\n\n/** Open snapshots whose diff no longer matches the current baseline. */\nexport async function findDriftedSnapshots(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshots: Snapshot[],\n): Promise<Map<string, BaselineStatus>> {\n const open = snapshots.filter((snapshot) => isOpenSnapshot(snapshot));\n const statuses = await Promise.all(\n open.map(async (snapshot) => await snapshotBaselineStatus(project, build, snapshot)),\n );\n const drifted = new Map<string, BaselineStatus>();\n for (const [index, snapshot] of open.entries()) {\n const status = statuses[index];\n if (status && isBaselineDrifted(status)) {\n drifted.set(snapshot.id, status);\n }\n }\n return drifted;\n}\n\n/** 409 telling the client the diff is out of date; recompute it or force the approval. */\nexport function baselineChanged(snapshotId: string, status: BaselineStatus): HTTPException {\n const body = {\n code: \"baseline_changed\",\n message:\n \"The baseline changed after this diff was computed. Re-diff the build, or approve with force.\",\n snapshotId,\n baselineStatus: status,\n };\n return new HTTPException(409, {\n res: new Response(JSON.stringify(body), {\n status: 409,\n headers: { \"content-type\": \"application/json\" },\n }),\n });\n}\n\n/** Throw {@link baselineChanged} when an open snapshot's baseline drifted and `force` is off. */\nexport async function assertBaselineCurrent(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\">,\n snapshot: Snapshot,\n force: boolean,\n): Promise<void> {\n if (!isOpenSnapshot(snapshot)) {\n return;\n }\n const status = await snapshotBaselineStatus(project, build, snapshot);\n if (!isBaselineDrifted(status)) {\n return;\n }\n if (force) {\n getStore().logger.warn(\n { snapshotId: snapshot.id, baselineStatus: status, userId: getStore().user?.id ?? null },\n \"approved snapshot despite changed baseline (forced)\",\n );\n return;\n }\n throw baselineChanged(snapshot.id, status);\n}\n\n/** True when `error` is the 409 raised by {@link baselineChanged}. */\nexport function isBaselineChanged(error: unknown): boolean {\n return error instanceof HTTPException && error.status === 409;\n}\n\n/** Per-snapshot baseline facts the review pages need. */\nexport interface BaselineView {\n /** Snapshot ids that have a baseline to compare against. */\n hasBaseline: Record<string, boolean>;\n /** Open snapshots whose recorded baseline no longer matches the current one. */\n drifted: Record<string, \"stale\" | \"removed\">;\n}\n\n/** Resolve each snapshot's current baseline once and derive presence plus drift. */\nexport async function loadBaselineView(\n project: Pick<Project, \"id\" | \"gitDefaultBranch\">,\n build: Pick<Build, \"gitBranch\" | \"isDefault\">,\n snapshots: Snapshot[],\n): Promise<BaselineView> {\n const baselines = new BaselineModel(getStore().db);\n const entries = await Promise.all(\n snapshots.map(async (snapshot) => {\n const current = await baselines.resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n return { snapshot, current };\n }),\n );\n const view: BaselineView = { hasBaseline: {}, drifted: {} };\n for (const { snapshot, current } of entries) {\n view.hasBaseline[snapshot.id] = Boolean(snapshot.comparisonImagePath ?? current);\n const status = baselineStatus(snapshot, current);\n if (\n !build.isDefault &&\n isOpenSnapshot(snapshot) &&\n (status === \"stale\" || status === \"removed\")\n ) {\n view.drifted[snapshot.id] = status;\n }\n }\n return view;\n}\n"],"mappings":";;;;;;;;;AAYA,SAAgB,eAAe,UAA6C;CAC1E,OAAO,SAAS,WAAW,SAAS,SAAS,WAAW;AAC1D;;AAGA,eAAsB,uBACpB,SACA,OACA,UACyB;CAEzB,MAAM,UAAU,MAAM,IADA,cAAc,SAAS,CAAC,CAAC,EACjB,CAAC,CAAC,QAC9B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;CACA,OAAO,eAAe,UAAU,OAAO;AACzC;;AAGA,eAAsB,qBACpB,SACA,OACA,WACsC;CACtC,MAAM,OAAO,UAAU,QAAQ,aAAa,eAAe,QAAQ,CAAC;CACpE,MAAM,WAAW,MAAM,QAAQ,IAC7B,KAAK,IAAI,OAAO,aAAa,MAAM,uBAAuB,SAAS,OAAO,QAAQ,CAAC,CACrF;CACA,MAAM,0BAAU,IAAI,IAA4B;CAChD,KAAK,MAAM,CAAC,OAAO,aAAa,KAAK,QAAQ,GAAG;EAC9C,MAAM,SAAS,SAAS;EACxB,IAAI,UAAU,kBAAkB,MAAM,GACpC,QAAQ,IAAI,SAAS,IAAI,MAAM;CAEnC;CACA,OAAO;AACT;;AAGA,SAAgB,gBAAgB,YAAoB,QAAuC;CAQzF,OAAO,IAAI,cAAc,KAAK,EAC5B,KAAK,IAAI,SAAS,KAAK,UAAU;EAPjC,MAAM;EACN,SACE;EACF;EACA,gBAAgB;CAGoB,CAAC,GAAG;EACtC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;CAChD,CAAC,EACH,CAAC;AACH;;AAGA,eAAsB,sBACpB,SACA,OACA,UACA,OACe;CACf,IAAI,CAAC,eAAe,QAAQ,GAC1B;CAEF,MAAM,SAAS,MAAM,uBAAuB,SAAS,OAAO,QAAQ;CACpE,IAAI,CAAC,kBAAkB,MAAM,GAC3B;CAEF,IAAI,OAAO;EACT,SAAS,CAAC,CAAC,OAAO,KAChB;GAAE,YAAY,SAAS;GAAI,gBAAgB;GAAQ,QAAQ,SAAS,CAAC,CAAC,MAAM,MAAM;EAAK,GACvF,qDACF;EACA;CACF;CACA,MAAM,gBAAgB,SAAS,IAAI,MAAM;AAC3C;;AAGA,SAAgB,kBAAkB,OAAyB;CACzD,OAAO,iBAAiB,iBAAiB,MAAM,WAAW;AAC5D;;AAWA,eAAsB,iBACpB,SACA,OACA,WACuB;CACvB,MAAM,YAAY,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE;CACjD,MAAM,UAAU,MAAM,QAAQ,IAC5B,UAAU,IAAI,OAAO,aAAa;EAShC,OAAO;GAAE;GAAU,SAAA,MARG,UAAU,QAC9B,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;EAC2B;CAC7B,CAAC,CACH;CACA,MAAM,OAAqB;EAAE,aAAa,CAAC;EAAG,SAAS,CAAC;CAAE;CAC1D,KAAK,MAAM,EAAE,UAAU,aAAa,SAAS;EAC3C,KAAK,YAAY,SAAS,MAAM,QAAQ,SAAS,uBAAuB,OAAO;EAC/E,MAAM,SAAS,eAAe,UAAU,OAAO;EAC/C,IACE,CAAC,MAAM,aACP,eAAe,QAAQ,MACtB,WAAW,WAAW,WAAW,YAElC,KAAK,QAAQ,SAAS,MAAM;CAEhC;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
import { readComparisonImage, validFrozenReference } from "./comparison-image.mjs";
|
|
6
|
+
import { BaselineModel, BuildModel, SnapshotModel, baselineStatus, resolveBaselineOrigin } from "@storyshelf/core/models";
|
|
7
|
+
// src/routers/baseline-reference.ts
|
|
8
|
+
async function loadBaselineReference(project, build, snapshot) {
|
|
9
|
+
if (snapshot.comparisonImagePath || snapshot.comparisonMetadata) return await frozenReference(project, build, snapshot);
|
|
10
|
+
return await legacyReference(project, build, snapshot);
|
|
11
|
+
}
|
|
12
|
+
async function legacyReference(project, build, snapshot) {
|
|
13
|
+
if (snapshot.baselineVersion === "none" && snapshot.baselineId === null) return { kind: "none" };
|
|
14
|
+
if (!snapshot.baselineId || !snapshot.baselineVersion) return { kind: "unavailable" };
|
|
15
|
+
const db = getStore().db;
|
|
16
|
+
const baseline = await new BaselineModel(db).resolve(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, project.gitDefaultBranch, snapshot.browser);
|
|
17
|
+
if (!baseline || baselineStatus(snapshot, baseline) !== "current") return { kind: "unavailable" };
|
|
18
|
+
const snapshots = new SnapshotModel(db);
|
|
19
|
+
const builds = new BuildModel(db);
|
|
20
|
+
const reference = await resolveBaselineOrigin(baseline, {
|
|
21
|
+
baseline: async (id) => await db.get(db.tables.baselines, id),
|
|
22
|
+
snapshot: async (id) => await snapshots.get(id),
|
|
23
|
+
build: async (id) => await builds.get(id)
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
kind: "known",
|
|
27
|
+
branch: baseline.branch,
|
|
28
|
+
defaultBranch: baseline.branch === project.gitDefaultBranch,
|
|
29
|
+
updatedAt: baseline.updatedAt,
|
|
30
|
+
imageAvailable: await imageExists(baseline.screenshotPath),
|
|
31
|
+
build: reference
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async function frozenReference(project, build, snapshot) {
|
|
35
|
+
const frozen = validFrozenReference(snapshot, project.id, build.id);
|
|
36
|
+
if (!frozen) return { kind: "unavailable" };
|
|
37
|
+
const { reference: metadata, path } = frozen;
|
|
38
|
+
const reference = metadata.sourceBuildId ? await new BuildModel(getStore().db).get(metadata.sourceBuildId) : null;
|
|
39
|
+
return {
|
|
40
|
+
kind: "known",
|
|
41
|
+
branch: metadata.branch,
|
|
42
|
+
defaultBranch: metadata.defaultBranch,
|
|
43
|
+
updatedAt: metadata.baselineVersion,
|
|
44
|
+
imageAvailable: Boolean(await readComparisonImage(path, metadata.imageSha256)),
|
|
45
|
+
build: reference?.projectId === project.id ? reference : null
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** A separate, read-only view of the current default-branch reference. */
|
|
49
|
+
async function loadDefaultComparison(project, build, snapshot) {
|
|
50
|
+
if (build.gitBranch === project.gitDefaultBranch) return null;
|
|
51
|
+
const { db } = getStore();
|
|
52
|
+
const baseline = await new BaselineModel(db).getFor(project.id, snapshot.storyId, snapshot.viewportName, project.gitDefaultBranch, snapshot.browser);
|
|
53
|
+
if (!baseline || baseline.id === snapshot.baselineId && baseline.updatedAt === snapshot.baselineVersion) return null;
|
|
54
|
+
const environment = !baseline.infraHash || !snapshot.infraHash ? "unknown" : baseline.infraHash === snapshot.infraHash ? "matching" : "different";
|
|
55
|
+
return {
|
|
56
|
+
id: baseline.id,
|
|
57
|
+
version: baseline.updatedAt,
|
|
58
|
+
branch: baseline.branch,
|
|
59
|
+
available: await imageExists(baseline.screenshotPath),
|
|
60
|
+
environment
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function imageExists(path) {
|
|
64
|
+
try {
|
|
65
|
+
return await getStore().storage.exists(path);
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export { loadBaselineReference, loadDefaultComparison };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=baseline-reference.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline-reference.mjs","names":[],"sources":["../../src/routers/baseline-reference.ts"],"sourcesContent":["import {\n BaselineModel,\n BuildModel,\n SnapshotModel,\n baselineStatus,\n resolveBaselineOrigin,\n} from \"@storyshelf/core/models\";\nimport type { Baseline, Build, Project, Snapshot } from \"@storyshelf/core/schema\";\nimport { getStore } from \"../store.ts\";\nimport { readComparisonImage, validFrozenReference } from \"./comparison-image.ts\";\n\n/** Reference information is only reliable while the recorded baseline version still exists. */\nexport type BaselineReference =\n | { kind: \"none\" }\n | { kind: \"unavailable\" }\n | {\n kind: \"known\";\n branch: string;\n defaultBranch: boolean;\n updatedAt: string;\n build: Build | null;\n imageAvailable: boolean;\n };\n\nexport async function loadBaselineReference(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n): Promise<BaselineReference> {\n if (snapshot.comparisonImagePath || snapshot.comparisonMetadata) {\n return await frozenReference(project, build, snapshot);\n }\n return await legacyReference(project, build, snapshot);\n}\n\nasync function legacyReference(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n): Promise<BaselineReference> {\n if (snapshot.baselineVersion === \"none\" && snapshot.baselineId === null) return { kind: \"none\" };\n if (!snapshot.baselineId || !snapshot.baselineVersion) return { kind: \"unavailable\" };\n const db = getStore().db;\n const baseline = await new BaselineModel(db).resolve(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n build.gitBranch,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n if (!baseline || baselineStatus(snapshot, baseline) !== \"current\") return { kind: \"unavailable\" };\n const snapshots = new SnapshotModel(db);\n const builds = new BuildModel(db);\n const reference = await resolveBaselineOrigin(baseline, {\n baseline: async (id) => (await db.get(db.tables.baselines, id)) as unknown as Baseline | null,\n snapshot: async (id) => await snapshots.get(id),\n build: async (id) => await builds.get(id),\n });\n return {\n kind: \"known\",\n branch: baseline.branch,\n defaultBranch: baseline.branch === project.gitDefaultBranch,\n updatedAt: baseline.updatedAt,\n imageAvailable: await imageExists(baseline.screenshotPath),\n build: reference,\n };\n}\n\nasync function frozenReference(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n): Promise<BaselineReference> {\n const frozen = validFrozenReference(snapshot, project.id, build.id);\n if (!frozen) return { kind: \"unavailable\" };\n const { reference: metadata, path } = frozen;\n const reference = metadata.sourceBuildId\n ? await new BuildModel(getStore().db).get(metadata.sourceBuildId)\n : null;\n return {\n kind: \"known\",\n branch: metadata.branch,\n defaultBranch: metadata.defaultBranch,\n updatedAt: metadata.baselineVersion,\n imageAvailable: Boolean(await readComparisonImage(path, metadata.imageSha256)),\n build: reference?.projectId === project.id ? reference : null,\n };\n}\n\nexport interface DefaultComparison {\n id: string;\n version: string;\n branch: string;\n available: boolean;\n environment: \"unknown\" | \"matching\" | \"different\";\n}\n\n/** A separate, read-only view of the current default-branch reference. */\nexport async function loadDefaultComparison(\n project: Project,\n build: Build,\n snapshot: Snapshot,\n): Promise<DefaultComparison | null> {\n if (build.gitBranch === project.gitDefaultBranch) return null;\n const { db } = getStore();\n const baseline = await new BaselineModel(db).getFor(\n project.id,\n snapshot.storyId,\n snapshot.viewportName,\n project.gitDefaultBranch,\n snapshot.browser,\n );\n if (\n !baseline ||\n (baseline.id === snapshot.baselineId && baseline.updatedAt === snapshot.baselineVersion)\n )\n return null;\n const environment =\n !baseline.infraHash || !snapshot.infraHash\n ? \"unknown\"\n : baseline.infraHash === snapshot.infraHash\n ? \"matching\"\n : \"different\";\n return {\n id: baseline.id,\n version: baseline.updatedAt,\n branch: baseline.branch,\n available: await imageExists(baseline.screenshotPath),\n environment,\n };\n}\n\nasync function imageExists(path: string): Promise<boolean> {\n try {\n return await getStore().storage.exists(path);\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;;AAwBA,eAAsB,sBACpB,SACA,OACA,UAC4B;CAC5B,IAAI,SAAS,uBAAuB,SAAS,oBAC3C,OAAO,MAAM,gBAAgB,SAAS,OAAO,QAAQ;CAEvD,OAAO,MAAM,gBAAgB,SAAS,OAAO,QAAQ;AACvD;AAEA,eAAe,gBACb,SACA,OACA,UAC4B;CAC5B,IAAI,SAAS,oBAAoB,UAAU,SAAS,eAAe,MAAM,OAAO,EAAE,MAAM,OAAO;CAC/F,IAAI,CAAC,SAAS,cAAc,CAAC,SAAS,iBAAiB,OAAO,EAAE,MAAM,cAAc;CACpF,MAAM,KAAK,SAAS,CAAC,CAAC;CACtB,MAAM,WAAW,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,QAC3C,QAAQ,IACR,SAAS,SACT,SAAS,cACT,MAAM,WACN,QAAQ,kBACR,SAAS,OACX;CACA,IAAI,CAAC,YAAY,eAAe,UAAU,QAAQ,MAAM,WAAW,OAAO,EAAE,MAAM,cAAc;CAChG,MAAM,YAAY,IAAI,cAAc,EAAE;CACtC,MAAM,SAAS,IAAI,WAAW,EAAE;CAChC,MAAM,YAAY,MAAM,sBAAsB,UAAU;EACtD,UAAU,OAAO,OAAQ,MAAM,GAAG,IAAI,GAAG,OAAO,WAAW,EAAE;EAC7D,UAAU,OAAO,OAAO,MAAM,UAAU,IAAI,EAAE;EAC9C,OAAO,OAAO,OAAO,MAAM,OAAO,IAAI,EAAE;CAC1C,CAAC;CACD,OAAO;EACL,MAAM;EACN,QAAQ,SAAS;EACjB,eAAe,SAAS,WAAW,QAAQ;EAC3C,WAAW,SAAS;EACpB,gBAAgB,MAAM,YAAY,SAAS,cAAc;EACzD,OAAO;CACT;AACF;AAEA,eAAe,gBACb,SACA,OACA,UAC4B;CAC5B,MAAM,SAAS,qBAAqB,UAAU,QAAQ,IAAI,MAAM,EAAE;CAClE,IAAI,CAAC,QAAQ,OAAO,EAAE,MAAM,cAAc;CAC1C,MAAM,EAAE,WAAW,UAAU,SAAS;CACtC,MAAM,YAAY,SAAS,gBACvB,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,SAAS,aAAa,IAC9D;CACJ,OAAO;EACL,MAAM;EACN,QAAQ,SAAS;EACjB,eAAe,SAAS;EACxB,WAAW,SAAS;EACpB,gBAAgB,QAAQ,MAAM,oBAAoB,MAAM,SAAS,WAAW,CAAC;EAC7E,OAAO,WAAW,cAAc,QAAQ,KAAK,YAAY;CAC3D;AACF;;AAWA,eAAsB,sBACpB,SACA,OACA,UACmC;CACnC,IAAI,MAAM,cAAc,QAAQ,kBAAkB,OAAO;CACzD,MAAM,EAAE,OAAO,SAAS;CACxB,MAAM,WAAW,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,OAC3C,QAAQ,IACR,SAAS,SACT,SAAS,cACT,QAAQ,kBACR,SAAS,OACX;CACA,IACE,CAAC,YACA,SAAS,OAAO,SAAS,cAAc,SAAS,cAAc,SAAS,iBAExE,OAAO;CACT,MAAM,cACJ,CAAC,SAAS,aAAa,CAAC,SAAS,YAC7B,YACA,SAAS,cAAc,SAAS,YAC9B,aACA;CACR,OAAO;EACL,IAAI,SAAS;EACb,SAAS,SAAS;EAClB,QAAQ,SAAS;EACjB,WAAW,MAAM,YAAY,SAAS,cAAc;EACpD;CACF;AACF;AAEA,eAAe,YAAY,MAAgC;CACzD,IAAI;EACF,OAAO,MAAM,SAAS,CAAC,CAAC,QAAQ,OAAO,IAAI;CAC7C,QAAQ;EACN,OAAO;CACT;AACF"}
|
|
@@ -4,7 +4,7 @@ import.meta.url;
|
|
|
4
4
|
import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
|
|
5
5
|
import { BASELINE_ARCHIVE_LIMITS } from "@storyshelf/core/models";
|
|
6
6
|
import { HTTPException } from "hono/http-exception";
|
|
7
|
-
|
|
7
|
+
// src/routers/baselines.handlers.ts
|
|
8
8
|
/** HTTP helpers for the baseline export/import router. */
|
|
9
9
|
/** Ceiling for a buffered import archive, whatever `maxUploadBytes` allows. */
|
|
10
10
|
const MAX_BASELINE_IMPORT_BYTES = BASELINE_ARCHIVE_LIMITS.maxArchiveBytes;
|
|
@@ -58,7 +58,6 @@ function toWebStream(chunks) {
|
|
|
58
58
|
function archiveFileName(slug, branch) {
|
|
59
59
|
return `${(branch ? `${slug}-baselines-${branch}` : `${slug}-baselines`).replaceAll(/[^\w.-]+/gu, "_")}.zip`;
|
|
60
60
|
}
|
|
61
|
-
//#endregion
|
|
62
61
|
export { MAX_BASELINE_IMPORT_BYTES, archiveFileName, importByteLimit, readCappedBody, toWebStream };
|
|
63
62
|
|
|
64
63
|
//# sourceMappingURL=baselines.handlers.mjs.map
|
|
@@ -8,7 +8,7 @@ import { archiveFileName, importByteLimit, readCappedBody, toWebStream } from ".
|
|
|
8
8
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
9
|
import { BaselineArchiveError, BaselineTransferModel } from "@storyshelf/core/models";
|
|
10
10
|
import { HTTPException } from "hono/http-exception";
|
|
11
|
-
|
|
11
|
+
// src/routers/baselines.ts
|
|
12
12
|
/**
|
|
13
13
|
* Baseline portability endpoints: export a project's baselines as a zip
|
|
14
14
|
* (`manifest.json` + PNGs, format v1) and import (upsert) the same format.
|
|
@@ -119,7 +119,6 @@ function registerBaselines(app) {
|
|
|
119
119
|
return c.json(result, 200);
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
//#endregion
|
|
123
122
|
export { registerBaselines };
|
|
124
123
|
|
|
125
124
|
//# sourceMappingURL=baselines.mjs.map
|
|
@@ -11,7 +11,7 @@ import { createRoute, z } from "@hono/zod-openapi";
|
|
|
11
11
|
import { BuildModel, CaptureAttemptModel } from "@storyshelf/core/models";
|
|
12
12
|
import { HTTPException } from "hono/http-exception";
|
|
13
13
|
import { CANCELLABLE_BUILD_STATUSES } from "@storyshelf/core/types";
|
|
14
|
-
|
|
14
|
+
// src/routers/build-cancel.ts
|
|
15
15
|
const cancelBuildRoute = createRoute({
|
|
16
16
|
method: "post",
|
|
17
17
|
tags: ["Builds"],
|
|
@@ -62,7 +62,6 @@ async function cancelBuild(buildId) {
|
|
|
62
62
|
await announceBuildStatus(buildId, "cancelled");
|
|
63
63
|
return updated;
|
|
64
64
|
}
|
|
65
|
-
//#endregion
|
|
66
65
|
export { registerBuildCancel };
|
|
67
66
|
|
|
68
67
|
//# sourceMappingURL=build-cancel.mjs.map
|
|
@@ -7,7 +7,7 @@ import { notFound, unauthorized } from "./schemas.mjs";
|
|
|
7
7
|
import { VIEW_ROLES, buildForProject } from "./builds.handlers.mjs";
|
|
8
8
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
9
9
|
import { changesetOf } from "@storyshelf/core/models";
|
|
10
|
-
|
|
10
|
+
// src/routers/build-changeset.ts
|
|
11
11
|
const component = z.object({
|
|
12
12
|
importPath: z.string(),
|
|
13
13
|
title: z.string(),
|
|
@@ -64,7 +64,6 @@ function registerBuildChangeset(app) {
|
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
|
-
//#endregion
|
|
68
67
|
export { registerBuildChangeset };
|
|
69
68
|
|
|
70
69
|
//# sourceMappingURL=build-changeset.mjs.map
|
|
@@ -8,7 +8,7 @@ import { announceBuildStatus } from "./builds.handlers.mjs";
|
|
|
8
8
|
import { publishingOf } from "@storyshelf/core/capture";
|
|
9
9
|
import { BuildModel, BuildReviewModel, deriveReviewStatus, latestVerdicts } from "@storyshelf/core/models";
|
|
10
10
|
import { HTTPException } from "hono/http-exception";
|
|
11
|
-
|
|
11
|
+
// src/routers/build-reviews.handlers.ts
|
|
12
12
|
/**
|
|
13
13
|
* Build-level verdicts (ADR 0013, decision 3): giving one and reading where a build stands. The
|
|
14
14
|
* routes are classed `review`, so `requireReview` answers 404 for a project with review off.
|
|
@@ -80,7 +80,6 @@ async function decideBuild(project, build) {
|
|
|
80
80
|
function approvalsRequired(project) {
|
|
81
81
|
return Math.max(1, project.reviewApprovals ?? 1);
|
|
82
82
|
}
|
|
83
|
-
//#endregion
|
|
84
83
|
export { giveVerdict, reviewStateOf };
|
|
85
84
|
|
|
86
85
|
//# sourceMappingURL=build-reviews.handlers.mjs.map
|