@storyshelf/core 0.1.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -46
- package/dist/adapter/auth.d.mts +4 -4
- package/dist/adapter/auth.mjs.map +1 -1
- package/dist/{adapters → adapter}/capture-queue.d.mts +6 -4
- package/dist/adapter/capture-queue.mjs +4 -0
- package/dist/adapter/capture-runner.d.mts +7 -7
- package/dist/adapter/database.d.mts +20 -9
- package/dist/adapter/database.mjs +60 -1
- package/dist/adapter/database.mjs.map +1 -0
- package/dist/adapter/git-host/comments.d.mts +41 -0
- package/dist/adapter/git-host/comments.mjs +54 -0
- package/dist/adapter/git-host/comments.mjs.map +1 -0
- package/dist/adapter/git-host/helpers.d.mts +1 -0
- package/dist/adapter/git-host/helpers.mjs +1 -0
- package/dist/adapter/git-host/helpers.mjs.map +1 -1
- package/dist/adapter/git-host.d.mts +3 -7
- package/dist/adapter/init.d.mts +58 -0
- package/dist/adapter/init.mjs +93 -0
- package/dist/adapter/init.mjs.map +1 -0
- package/dist/adapter/metadata.d.mts +83 -0
- package/dist/adapter/metadata.mjs +4 -0
- package/dist/adapter/storage.d.mts +15 -4
- package/dist/adapters/webhook-events.mjs +20 -4
- package/dist/adapters/webhook-events.mjs.map +1 -1
- package/dist/capture/adapter.d.mts +7 -1
- package/dist/capture/adapter.mjs +3 -0
- package/dist/capture/adapter.mjs.map +1 -1
- package/dist/capture/dispatch.mjs +150 -0
- package/dist/capture/dispatch.mjs.map +1 -0
- package/dist/capture/orchestrator.d.mts +9 -0
- package/dist/capture/orchestrator.mjs +11 -44
- package/dist/capture/orchestrator.mjs.map +1 -1
- package/dist/capture/pipeline.d.mts +4 -1
- package/dist/capture/pipeline.mjs +6 -6
- package/dist/capture/pipeline.mjs.map +1 -1
- package/dist/capture/queue.d.mts +4 -1
- package/dist/capture/queue.mjs +3 -1
- package/dist/capture/queue.mjs.map +1 -1
- package/dist/capture/skip-checks.mjs +76 -0
- package/dist/capture/skip-checks.mjs.map +1 -0
- package/dist/capture/statics.mjs +124 -0
- package/dist/capture/statics.mjs.map +1 -0
- package/dist/capture/status-fanout.mjs +1 -0
- package/dist/capture/status-fanout.mjs.map +1 -1
- package/dist/capture/storybook.d.mts +1 -0
- package/dist/capture/storybook.mjs +1 -0
- package/dist/capture/storybook.mjs.map +1 -1
- package/dist/capture.d.mts +8 -0
- package/dist/capture.mjs +9 -0
- package/dist/config.d.mts +37 -22
- package/dist/config.mjs +28 -2
- package/dist/config.mjs.map +1 -1
- package/dist/ddl.d.mts +2 -1
- package/dist/ddl.mjs +2 -1
- package/dist/ddl.mjs.map +1 -1
- package/dist/diff/engine.d.mts +8 -0
- package/dist/diff/engine.mjs +8 -0
- package/dist/diff/engine.mjs.map +1 -1
- package/dist/diff/options.d.mts +3 -1
- package/dist/diff.d.mts +5 -0
- package/dist/diff.mjs +6 -0
- package/dist/index.d.mts +34 -24
- package/dist/index.mjs +211 -252
- package/dist/index.mjs.map +1 -1
- package/dist/logger.d.mts +3 -3
- package/dist/logger.mjs.map +1 -1
- package/dist/middleware/auth-gate.mjs +36 -0
- package/dist/middleware/auth-gate.mjs.map +1 -0
- package/dist/middleware/csrf.mjs +1 -0
- package/dist/middleware/csrf.mjs.map +1 -1
- package/dist/middleware/rate-limit.mjs +1 -0
- package/dist/middleware/rate-limit.mjs.map +1 -1
- package/dist/middleware/request-log.mjs +28 -0
- package/dist/middleware/request-log.mjs.map +1 -0
- package/dist/middleware/store-scope.mjs +29 -0
- package/dist/middleware/store-scope.mjs.map +1 -0
- package/dist/models/baseline.mjs +9 -5
- package/dist/models/baseline.mjs.map +1 -1
- package/dist/models/build.mjs +5 -1
- package/dist/models/build.mjs.map +1 -1
- package/dist/models/comment.mjs +4 -1
- package/dist/models/comment.mjs.map +1 -1
- package/dist/models/label.mjs +3 -1
- package/dist/models/label.mjs.map +1 -1
- package/dist/models/member.mjs +2 -1
- package/dist/models/member.mjs.map +1 -1
- package/dist/models/project.mjs +2 -1
- package/dist/models/project.mjs.map +1 -1
- package/dist/models/snapshot.mjs +2 -1
- package/dist/models/snapshot.mjs.map +1 -1
- package/dist/models/status-config.mjs +2 -1
- package/dist/models/status-config.mjs.map +1 -1
- package/dist/models/token.mjs +2 -1
- package/dist/models/token.mjs.map +1 -1
- package/dist/models/webhook.mjs +12 -3
- package/dist/models/webhook.mjs.map +1 -1
- package/dist/pages/build-detail.mjs +10 -4
- package/dist/pages/build-detail.mjs.map +1 -1
- package/dist/pages/build-diff-comments.mjs +113 -0
- package/dist/pages/build-diff-comments.mjs.map +1 -0
- package/dist/pages/build-diff-header.mjs +163 -0
- package/dist/pages/build-diff-header.mjs.map +1 -0
- package/dist/pages/build-diff-nav.mjs +66 -0
- package/dist/pages/build-diff-nav.mjs.map +1 -0
- package/dist/pages/build-diff-viewer.mjs +197 -0
- package/dist/pages/build-diff-viewer.mjs.map +1 -0
- package/dist/pages/build-diff.mjs +66 -413
- package/dist/pages/build-diff.mjs.map +1 -1
- package/dist/pages/compute-jobs.mjs +3 -1
- package/dist/pages/compute-jobs.mjs.map +1 -1
- package/dist/pages/label-detail.mjs +4 -3
- package/dist/pages/label-detail.mjs.map +1 -1
- package/dist/pages/login.mjs +1 -0
- package/dist/pages/login.mjs.map +1 -1
- package/dist/pages/project-builds.mjs +6 -3
- package/dist/pages/project-builds.mjs.map +1 -1
- package/dist/pages/project-create.mjs +1 -0
- package/dist/pages/project-create.mjs.map +1 -1
- package/dist/pages/project-settings.mjs +1 -0
- package/dist/pages/project-settings.mjs.map +1 -1
- package/dist/pages/projects.mjs +12 -6
- package/dist/pages/projects.mjs.map +1 -1
- package/dist/pages/root.mjs +7 -3
- package/dist/pages/root.mjs.map +1 -1
- package/dist/pages/settings-general.mjs +1 -0
- package/dist/pages/settings-general.mjs.map +1 -1
- package/dist/pages/settings-labels.mjs +2 -1
- package/dist/pages/settings-labels.mjs.map +1 -1
- package/dist/pages/settings-members.mjs +2 -1
- package/dist/pages/settings-members.mjs.map +1 -1
- package/dist/pages/settings-status.mjs +3 -1
- package/dist/pages/settings-status.mjs.map +1 -1
- package/dist/pages/settings-tests.mjs +4 -3
- package/dist/pages/settings-tests.mjs.map +1 -1
- package/dist/pages/settings-tokens.mjs +1 -0
- package/dist/pages/settings-tokens.mjs.map +1 -1
- package/dist/pages/settings-webhooks.mjs +1 -0
- package/dist/pages/settings-webhooks.mjs.map +1 -1
- package/dist/pages/storybook.mjs +38 -1
- package/dist/pages/storybook.mjs.map +1 -1
- package/dist/{utils/paths.d.mts → paths.d.mts} +5 -0
- package/dist/{utils/paths.mjs → paths.mjs} +5 -0
- package/dist/paths.mjs.map +1 -0
- package/dist/retention/purge.mjs +2 -1
- package/dist/retention/purge.mjs.map +1 -1
- package/dist/routers/admin.mjs +2 -1
- package/dist/routers/admin.mjs.map +1 -1
- package/dist/routers/assets.mjs +1 -0
- package/dist/routers/assets.mjs.map +1 -1
- package/dist/routers/auth.mjs +1 -0
- package/dist/routers/auth.mjs.map +1 -1
- package/dist/routers/builds.handlers.mjs +114 -19
- package/dist/routers/builds.handlers.mjs.map +1 -1
- package/dist/routers/builds.mjs +55 -34
- package/dist/routers/builds.mjs.map +1 -1
- package/dist/routers/comments.mjs +1 -0
- package/dist/routers/comments.mjs.map +1 -1
- package/dist/routers/health.mjs +137 -0
- package/dist/routers/health.mjs.map +1 -0
- package/dist/routers/helpers.mjs +8 -2
- package/dist/routers/helpers.mjs.map +1 -1
- package/dist/routers/htmx.mjs +2 -0
- package/dist/routers/htmx.mjs.map +1 -1
- package/dist/routers/labels.mjs +2 -1
- package/dist/routers/labels.mjs.map +1 -1
- package/dist/routers/media.mjs +3 -2
- package/dist/routers/media.mjs.map +1 -1
- package/dist/routers/members.mjs +1 -0
- package/dist/routers/members.mjs.map +1 -1
- package/dist/routers/projects.mjs +2 -1
- package/dist/routers/projects.mjs.map +1 -1
- package/dist/routers/schemas.mjs +25 -0
- package/dist/routers/schemas.mjs.map +1 -1
- package/dist/routers/settings-general.mjs +95 -0
- package/dist/routers/settings-general.mjs.map +1 -0
- package/dist/routers/settings-labels.mjs +49 -0
- package/dist/routers/settings-labels.mjs.map +1 -0
- package/dist/routers/settings-members.mjs +30 -0
- package/dist/routers/settings-members.mjs.map +1 -0
- package/dist/routers/settings-status.mjs +73 -0
- package/dist/routers/settings-status.mjs.map +1 -0
- package/dist/routers/settings-tokens.mjs +40 -0
- package/dist/routers/settings-tokens.mjs.map +1 -0
- package/dist/routers/settings-webhooks.mjs +63 -0
- package/dist/routers/settings-webhooks.mjs.map +1 -0
- package/dist/routers/settings.handlers.mjs +93 -0
- package/dist/routers/settings.handlers.mjs.map +1 -0
- package/dist/routers/settings.mjs +14 -246
- package/dist/routers/settings.mjs.map +1 -1
- package/dist/routers/snapshots.mjs +2 -1
- package/dist/routers/snapshots.mjs.map +1 -1
- package/dist/routers/status-configs.mjs +2 -1
- package/dist/routers/status-configs.mjs.map +1 -1
- package/dist/routers/storybook.mjs +12 -4
- package/dist/routers/storybook.mjs.map +1 -1
- package/dist/routers/tokens.mjs +2 -1
- package/dist/routers/tokens.mjs.map +1 -1
- package/dist/routers/ui.mjs +5 -4
- package/dist/routers/ui.mjs.map +1 -1
- package/dist/routers/webhooks.mjs +7 -6
- package/dist/routers/webhooks.mjs.map +1 -1
- package/dist/schema/baseline.d.mts +18 -0
- package/dist/schema/baseline.mjs +22 -0
- package/dist/schema/baseline.mjs.map +1 -0
- package/dist/schema/build.d.mts +26 -0
- package/dist/schema/build.mjs +29 -0
- package/dist/schema/build.mjs.map +1 -0
- package/dist/schema/comment.d.mts +19 -0
- package/dist/schema/comment.mjs +26 -0
- package/dist/schema/comment.mjs.map +1 -0
- package/dist/schema/label.d.mts +25 -0
- package/dist/schema/label.mjs +30 -0
- package/dist/schema/label.mjs.map +1 -0
- package/dist/schema/member.d.mts +15 -0
- package/dist/schema/member.mjs +19 -0
- package/dist/schema/member.mjs.map +1 -0
- package/dist/schema/project.d.mts +34 -0
- package/dist/schema/project.mjs +25 -0
- package/dist/schema/project.mjs.map +1 -0
- package/dist/schema/snapshot.d.mts +30 -0
- package/dist/schema/snapshot.mjs +34 -0
- package/dist/schema/snapshot.mjs.map +1 -0
- package/dist/schema/status-config.d.mts +16 -0
- package/dist/schema/status-config.mjs +20 -0
- package/dist/schema/status-config.mjs.map +1 -0
- package/dist/schema/token.d.mts +15 -0
- package/dist/schema/token.mjs +19 -0
- package/dist/schema/token.mjs.map +1 -0
- package/dist/schema/user.d.mts +17 -0
- package/dist/schema/user.mjs +19 -0
- package/dist/schema/user.mjs.map +1 -0
- package/dist/schema/webhook.d.mts +16 -0
- package/dist/schema/webhook.mjs +20 -0
- package/dist/schema/webhook.mjs.map +1 -0
- package/dist/schema.d.mts +52 -4433
- package/dist/schema.mjs +37 -139
- package/dist/schema.mjs.map +1 -1
- package/dist/store.mjs +2 -0
- package/dist/store.mjs.map +1 -1
- package/dist/types.d.mts +13 -2
- package/dist/types.mjs +8 -0
- package/dist/types.mjs.map +1 -1
- package/dist/ui/document.mjs +1 -0
- package/dist/ui/document.mjs.map +1 -1
- package/dist/ui/feedback.mjs +24 -0
- package/dist/ui/feedback.mjs.map +1 -0
- package/dist/ui/{components.mjs → forms.mjs} +6 -16
- package/dist/ui/forms.mjs.map +1 -0
- package/dist/ui/styles.mjs +1 -0
- package/dist/ui/styles.mjs.map +1 -1
- package/dist/ui/theme.mjs +2 -0
- package/dist/ui/theme.mjs.map +1 -1
- package/dist/urls.d.mts +8 -0
- package/dist/urls.mjs +7 -0
- package/dist/urls.mjs.map +1 -1
- package/dist/utils/hash.mjs +3 -0
- package/dist/utils/hash.mjs.map +1 -1
- package/dist/utils/ulid.mjs +2 -0
- package/dist/utils/ulid.mjs.map +1 -1
- package/package.json +47 -45
- package/dist/adapters/metadata.d.mts +0 -24
- package/dist/models/baseline.d.mts +0 -20
- package/dist/models/build.d.mts +0 -50
- package/dist/models/comment.d.mts +0 -20
- package/dist/models/label.d.mts +0 -49
- package/dist/models/member.d.mts +0 -27
- package/dist/models/project.d.mts +0 -42
- package/dist/models/snapshot.d.mts +0 -46
- package/dist/models/token.d.mts +0 -33
- package/dist/models/webhook.d.mts +0 -37
- package/dist/retention/purge.d.mts +0 -28
- package/dist/ui/components.mjs.map +0 -1
- package/dist/ui/document.d.mts +0 -8
- package/dist/utils/paths.mjs.map +0 -1
- package/dist/utils/ulid.d.mts +0 -8
package/dist/logger.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.mjs","names":[],"sources":["../src/logger.ts"],"sourcesContent":["import pino from \"pino\";\nimport type { Logger } from \"pino\";\n\nexport type { Logger };\n\n/** A pino worker transport to attach to the logger output. */\nexport interface PinoTransport {\n /** Package name or absolute path of the transport module. */\n target: string;\n /** Options passed to the transport worker. */\n options?: Record<string, unknown>;\n}\n\n/** Configuration for constructing the shelf logger. */\nexport interface LoggerOptions {\n /** Minimum level to emit. Defaults to `\"info\"`. */\n level?: string;\n /** Extra pino worker transports appended to the default stdout sink. */\n transports?: PinoTransport[];\n /** Deployment environment recorded in the `env` base field. Defaults to `NODE_ENV`. */\n env?: string;\n}\n\n/**\n * Create the structured JSON logger used across StoryShelf.\n *\n * Logging conventions:\n * - Always log structured objects, never interpolate into the message:\n * `logger.info({ projectId, buildId }, \"build started\")`.\n * - Attach errors as an `err` child field rather than stringifying into the message:\n * `logger.error({ err }, \"capture failed\")`.\n * - Derive scoped child loggers for background work:\n * `const captureLogger = logger.child({ buildId })`.\n *\n * The default sink is stdout. Hosted observability platforms (Sentry, PostHog,\n * Datadog, GCP, OTEL collector, etc.) are added as pino worker `transports` —\n * they are sinks for this logger, not standalone loggers.\n */\nexport function createShelfLogger(options: LoggerOptions = {}): Logger {\n const targets = [\n { target: \"pino/file\", options: { destination: 1 } },\n ...(options.transports ?? []),\n ];\n const transport = pino.transport({ targets });\n return pino(\n {\n level: options.level ?? \"info\",\n base: { env: options.env ?? process.env[\"NODE_ENV\"] },\n },\n transport,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"logger.mjs","names":[],"sources":["../src/logger.ts"],"sourcesContent":["import pino from \"pino\";\nimport type { Logger } from \"pino\";\n\n/** Pino logger type shared across StoryShelf. */\nexport type { Logger };\n\n/** A pino worker transport to attach to the logger output. */\nexport interface PinoTransport {\n /** Package name or absolute path of the transport module. */\n target: string;\n /** Options passed to the transport worker. */\n options?: Record<string, unknown>;\n}\n\n/** Configuration for constructing the shelf logger. */\nexport interface LoggerOptions {\n /** Minimum level to emit. Defaults to `\"info\"`. */\n level?: string;\n /** Extra pino worker transports appended to the default stdout sink. */\n transports?: PinoTransport[];\n /** Deployment environment recorded in the `env` base field. Defaults to `NODE_ENV`. */\n env?: string;\n}\n\n/**\n * Create the structured JSON logger used across StoryShelf.\n *\n * Logging conventions:\n * - Always log structured objects, never interpolate into the message:\n * `logger.info({ projectId, buildId }, \"build started\")`.\n * - Attach errors as an `err` child field rather than stringifying into the message:\n * `logger.error({ err }, \"capture failed\")`.\n * - Derive scoped child loggers for background work:\n * `const captureLogger = logger.child({ buildId })`.\n *\n * The default sink is stdout. Hosted observability platforms (Sentry, PostHog,\n * Datadog, GCP, OTEL collector, etc.) are added as pino worker `transports` —\n * they are sinks for this logger, not standalone loggers.\n */\nexport function createShelfLogger(options: LoggerOptions = {}): Logger {\n const targets = [\n { target: \"pino/file\", options: { destination: 1 } },\n ...(options.transports ?? []),\n ];\n const transport = pino.transport({ targets });\n return pino(\n {\n level: options.level ?? \"info\",\n base: { env: options.env ?? process.env[\"NODE_ENV\"] },\n },\n transport,\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,kBAAkB,UAAyB,CAAC,GAAW;CACrE,MAAM,UAAU,CACd;EAAE,QAAQ;EAAa,SAAS,EAAE,aAAa,EAAE;CAAE,GACnD,GAAI,QAAQ,cAAc,CAAC,CAC7B;CACA,MAAM,YAAY,KAAK,UAAU,EAAE,QAAQ,CAAC;CAC5C,OAAO,KACL;EACE,OAAO,QAAQ,SAAS;EACxB,MAAM,EAAE,KAAK,QAAQ,OAAO,QAAQ,IAAI,YAAY;CACtD,GACA,SACF;AACF"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { getStore } from "../store.mjs";
|
|
5
|
+
//#region src/middleware/auth-gate.ts
|
|
6
|
+
/** Resolve the request user from the auth adapter (null when auth is off or a token is used). */
|
|
7
|
+
async function resolveRequestUser(c, auth) {
|
|
8
|
+
if (!auth) return null;
|
|
9
|
+
if (c.req.header("authorization")) return null;
|
|
10
|
+
return await auth.check(c.req.raw);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML
|
|
14
|
+
* requests redirect to login. API/auth routes answer 401/403 themselves, and
|
|
15
|
+
* published-Storybook routes enforce their own auth (public builds are
|
|
16
|
+
* viewable without a session, ADR 0011).
|
|
17
|
+
*/
|
|
18
|
+
function authGate() {
|
|
19
|
+
return async (c, next) => {
|
|
20
|
+
const { user, authEnabled: isAuthEnabled } = getStore();
|
|
21
|
+
const { path } = c.req;
|
|
22
|
+
if (!isAuthEnabled || user || path.startsWith("/api/") || path.startsWith("/auth/") || path.startsWith("/assets/") || path.startsWith("/projects/") && path.includes("/storybook")) {
|
|
23
|
+
await next();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (c.req.header("HX-Request") === "true") {
|
|
27
|
+
c.header("HX-Redirect", "/auth/login");
|
|
28
|
+
return c.body(null, 204);
|
|
29
|
+
}
|
|
30
|
+
return c.redirect("/auth/login", 302);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { authGate, resolveRequestUser };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=auth-gate.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-gate.mjs","names":[],"sources":["../../src/middleware/auth-gate.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\nimport type { AuthAdapter, AuthUser } from \"../adapters/auth.ts\";\nimport { getStore } from \"../store.ts\";\n\n/** Resolve the request user from the auth adapter (null when auth is off or a token is used). */\nexport async function resolveRequestUser(\n c: Pick<Context, \"req\">,\n auth?: AuthAdapter,\n): Promise<AuthUser | null> {\n if (!auth) {\n return null;\n }\n if (c.req.header(\"authorization\")) {\n return null;\n }\n return await auth.check(c.req.raw);\n}\n\n/**\n * Gate the server-rendered UI behind auth (ADR 0008): unauthenticated HTML\n * requests redirect to login. API/auth routes answer 401/403 themselves, and\n * published-Storybook routes enforce their own auth (public builds are\n * viewable without a session, ADR 0011).\n */\nexport function authGate() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const { user, authEnabled: isAuthEnabled } = getStore();\n const { path } = c.req;\n if (\n !isAuthEnabled ||\n user ||\n path.startsWith(\"/api/\") ||\n path.startsWith(\"/auth/\") ||\n path.startsWith(\"/assets/\") ||\n (path.startsWith(\"/projects/\") && path.includes(\"/storybook\"))\n ) {\n await next();\n return;\n }\n if (c.req.header(\"HX-Request\") === \"true\") {\n c.header(\"HX-Redirect\", \"/auth/login\");\n return c.body(null, 204);\n }\n return c.redirect(\"/auth/login\", 302);\n };\n}\n"],"mappings":";;;;;;AAKA,eAAsB,mBACpB,GACA,MAC0B;CAC1B,IAAI,CAAC,MACH,OAAO;CAET,IAAI,EAAE,IAAI,OAAO,eAAe,GAC9B,OAAO;CAET,OAAO,MAAM,KAAK,MAAM,EAAE,IAAI,GAAG;AACnC;;;;;;;AAQA,SAAgB,WAAW;CAEzB,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,EAAE,MAAM,aAAa,kBAAkB,SAAS;EACtD,MAAM,EAAE,SAAS,EAAE;EACnB,IACE,CAAC,iBACD,QACA,KAAK,WAAW,OAAO,KACvB,KAAK,WAAW,QAAQ,KACxB,KAAK,WAAW,UAAU,KACzB,KAAK,WAAW,YAAY,KAAK,KAAK,SAAS,YAAY,GAC5D;GACA,MAAM,KAAK;GACX;EACF;EACA,IAAI,EAAE,IAAI,OAAO,YAAY,MAAM,QAAQ;GACzC,EAAE,OAAO,eAAe,aAAa;GACrC,OAAO,EAAE,KAAK,MAAM,GAAG;EACzB;EACA,OAAO,EAAE,SAAS,eAAe,GAAG;CACtC;AACF"}
|
package/dist/middleware/csrf.mjs
CHANGED
|
@@ -18,6 +18,7 @@ function verifyToken(token, sessionId) {
|
|
|
18
18
|
const age = Date.now() - timestamp;
|
|
19
19
|
return age > 0 && age < 864e5;
|
|
20
20
|
}
|
|
21
|
+
/** Hono middleware issuing CSRF tokens on safe methods and verifying them on writes. */
|
|
21
22
|
function csrf() {
|
|
22
23
|
return async (c, next) => {
|
|
23
24
|
const { method } = c.req;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csrf.mjs","names":[],"sources":["../../src/middleware/csrf.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"csrf.mjs","names":[],"sources":["../../src/middleware/csrf.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\nimport { createHash, randomBytes } from \"node:crypto\";\n\nconst CSRF_SECRET = process.env[\"CSRF_SECRET\"] ?? randomBytes(32).toString(\"hex\");\n\nfunction generateToken(sessionId: string): string {\n const timestamp = Date.now().toString(36);\n const payload = `${sessionId}:${timestamp}`;\n const signature = createHash(\"sha256\")\n .update(`${CSRF_SECRET}:${payload}`)\n .digest(\"hex\")\n .slice(0, 16);\n return `${payload}:${signature}`;\n}\n\nfunction verifyToken(token: string, sessionId: string): boolean {\n const parts = token.split(\":\");\n if (parts.length !== 3) {\n return false;\n }\n const [payloadTimestamp, signature] = parts.slice(1);\n if (!payloadTimestamp || !signature) {\n return false;\n }\n const expectedSignature = createHash(\"sha256\")\n .update(`${CSRF_SECRET}:${sessionId}:${payloadTimestamp}`)\n .digest(\"hex\")\n .slice(0, 16);\n if (signature !== expectedSignature) {\n return false;\n }\n const timestamp = Number.parseInt(payloadTimestamp, 36);\n const age = Date.now() - timestamp;\n return age > 0 && age < 24 * 60 * 60 * 1000;\n}\n\n/** Hono middleware issuing CSRF tokens on safe methods and verifying them on writes. */\nexport function csrf() {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const { method } = c.req;\n if (method === \"GET\" || method === \"HEAD\" || method === \"OPTIONS\") {\n const sessionId = c.req.header(\"session-id\") ?? \"default\";\n const token = generateToken(sessionId);\n c.header(\"X-CSRF-Token\", token);\n await next();\n return;\n }\n const sessionId = c.req.header(\"session-id\") ?? \"default\";\n const token = c.req.header(\"x-csrf-token\") ?? c.req.query(\"csrf_token\");\n if (!token || !verifyToken(token, sessionId)) {\n return c.json({ error: \"Invalid CSRF token\" }, 403);\n }\n await next();\n };\n}\n\n/** Generate a CSRF token for the given session. */\nexport function getCsrfToken(sessionId: string): string {\n return generateToken(sessionId);\n}\n"],"mappings":";;;;;AAGA,MAAM,cAAc,QAAQ,IAAI,kBAAkB,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;AAEhF,SAAS,cAAc,WAA2B;CAEhD,MAAM,UAAU,GAAG,UAAU,GADX,KAAK,IAAI,CAAC,CAAC,SAAS,EACE;CAKxC,OAAO,GAAG,QAAQ,GAJA,WAAW,QAAQ,CAAC,CACnC,OAAO,GAAG,YAAY,GAAG,SAAS,CAAC,CACnC,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,EACiB;AAC/B;AAEA,SAAS,YAAY,OAAe,WAA4B;CAC9D,MAAM,QAAQ,MAAM,MAAM,GAAG;CAC7B,IAAI,MAAM,WAAW,GACnB,OAAO;CAET,MAAM,CAAC,kBAAkB,aAAa,MAAM,MAAM,CAAC;CACnD,IAAI,CAAC,oBAAoB,CAAC,WACxB,OAAO;CAMT,IAAI,cAJsB,WAAW,QAAQ,CAAC,CAC3C,OAAO,GAAG,YAAY,GAAG,UAAU,GAAG,kBAAkB,CAAC,CACzD,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,EACsB,GAChC,OAAO;CAET,MAAM,YAAY,OAAO,SAAS,kBAAkB,EAAE;CACtD,MAAM,MAAM,KAAK,IAAI,IAAI;CACzB,OAAO,MAAM,KAAK,MAAM;AAC1B;;AAGA,SAAgB,OAAO;CAErB,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,EAAE,WAAW,EAAE;EACrB,IAAI,WAAW,SAAS,WAAW,UAAU,WAAW,WAAW;GAEjE,MAAM,QAAQ,cADI,EAAE,IAAI,OAAO,YAAY,KAAK,SACX;GACrC,EAAE,OAAO,gBAAgB,KAAK;GAC9B,MAAM,KAAK;GACX;EACF;EACA,MAAM,YAAY,EAAE,IAAI,OAAO,YAAY,KAAK;EAChD,MAAM,QAAQ,EAAE,IAAI,OAAO,cAAc,KAAK,EAAE,IAAI,MAAM,YAAY;EACtE,IAAI,CAAC,SAAS,CAAC,YAAY,OAAO,SAAS,GACzC,OAAO,EAAE,KAAK,EAAE,OAAO,qBAAqB,GAAG,GAAG;EAEpD,MAAM,KAAK;CACb;AACF"}
|
|
@@ -8,6 +8,7 @@ function cleanup() {
|
|
|
8
8
|
for (const [key, entry] of stores) if (now > entry.resetAt) stores.delete(key);
|
|
9
9
|
}
|
|
10
10
|
setInterval(cleanup, 6e4).unref();
|
|
11
|
+
/** Hono middleware limiting requests per key within a sliding window. */
|
|
11
12
|
function rateLimit(options) {
|
|
12
13
|
const { windowMs, max, keyGenerator } = options;
|
|
13
14
|
return async (c, next) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\n\ninterface RateLimitEntry {\n count: number;\n resetAt: number;\n}\n\ninterface RateLimitOptions {\n windowMs: number;\n max: number;\n keyGenerator?: (c: Context) => string;\n}\n\nconst stores = new Map<string, RateLimitEntry>();\n\nfunction cleanup(): void {\n const now = Date.now();\n for (const [key, entry] of stores) {\n if (now > entry.resetAt) {\n stores.delete(key);\n }\n }\n}\n\nsetInterval(cleanup, 60_000).unref();\n\nexport function rateLimit(options: RateLimitOptions) {\n const { windowMs, max, keyGenerator } = options;\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const key = keyGenerator ? keyGenerator(c) : (c.req.header(\"x-forwarded-for\") ?? \"anonymous\");\n const now = Date.now();\n let entry = stores.get(key);\n if (!entry || now > entry.resetAt) {\n entry = { count: 0, resetAt: now + windowMs };\n stores.set(key, entry);\n }\n entry.count += 1;\n if (entry.count > max) {\n const retryAfter = Math.ceil((entry.resetAt - now) / 1000);\n c.header(\"Retry-After\", String(retryAfter));\n return c.json({ error: \"Too many requests\" }, 429);\n }\n c.header(\"X-RateLimit-Limit\", String(max));\n c.header(\"X-RateLimit-Remaining\", String(Math.max(0, max - entry.count)));\n await next();\n };\n}\n"],"mappings":";;;;AAaA,MAAM,yBAAS,IAAI,IAA4B;AAE/C,SAAS,UAAgB;CACvB,MAAM,MAAM,KAAK,IAAI;CACrB,KAAK,MAAM,CAAC,KAAK,UAAU,QACzB,IAAI,MAAM,MAAM,SACd,OAAO,OAAO,GAAG;AAGvB;AAEA,YAAY,SAAS,GAAM,CAAC,CAAC,MAAM
|
|
1
|
+
{"version":3,"file":"rate-limit.mjs","names":[],"sources":["../../src/middleware/rate-limit.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\n\ninterface RateLimitEntry {\n count: number;\n resetAt: number;\n}\n\ninterface RateLimitOptions {\n windowMs: number;\n max: number;\n keyGenerator?: (c: Context) => string;\n}\n\nconst stores = new Map<string, RateLimitEntry>();\n\nfunction cleanup(): void {\n const now = Date.now();\n for (const [key, entry] of stores) {\n if (now > entry.resetAt) {\n stores.delete(key);\n }\n }\n}\n\nsetInterval(cleanup, 60_000).unref();\n\n/** Hono middleware limiting requests per key within a sliding window. */\nexport function rateLimit(options: RateLimitOptions) {\n const { windowMs, max, keyGenerator } = options;\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const key = keyGenerator ? keyGenerator(c) : (c.req.header(\"x-forwarded-for\") ?? \"anonymous\");\n const now = Date.now();\n let entry = stores.get(key);\n if (!entry || now > entry.resetAt) {\n entry = { count: 0, resetAt: now + windowMs };\n stores.set(key, entry);\n }\n entry.count += 1;\n if (entry.count > max) {\n const retryAfter = Math.ceil((entry.resetAt - now) / 1000);\n c.header(\"Retry-After\", String(retryAfter));\n return c.json({ error: \"Too many requests\" }, 429);\n }\n c.header(\"X-RateLimit-Limit\", String(max));\n c.header(\"X-RateLimit-Remaining\", String(Math.max(0, max - entry.count)));\n await next();\n };\n}\n"],"mappings":";;;;AAaA,MAAM,yBAAS,IAAI,IAA4B;AAE/C,SAAS,UAAgB;CACvB,MAAM,MAAM,KAAK,IAAI;CACrB,KAAK,MAAM,CAAC,KAAK,UAAU,QACzB,IAAI,MAAM,MAAM,SACd,OAAO,OAAO,GAAG;AAGvB;AAEA,YAAY,SAAS,GAAM,CAAC,CAAC,MAAM;;AAGnC,SAAgB,UAAU,SAA2B;CACnD,MAAM,EAAE,UAAU,KAAK,iBAAiB;CAExC,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,MAAM,eAAe,aAAa,CAAC,IAAK,EAAE,IAAI,OAAO,iBAAiB,KAAK;EACjF,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,QAAQ,OAAO,IAAI,GAAG;EAC1B,IAAI,CAAC,SAAS,MAAM,MAAM,SAAS;GACjC,QAAQ;IAAE,OAAO;IAAG,SAAS,MAAM;GAAS;GAC5C,OAAO,IAAI,KAAK,KAAK;EACvB;EACA,MAAM,SAAS;EACf,IAAI,MAAM,QAAQ,KAAK;GACrB,MAAM,aAAa,KAAK,MAAM,MAAM,UAAU,OAAO,GAAI;GACzD,EAAE,OAAO,eAAe,OAAO,UAAU,CAAC;GAC1C,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;EACnD;EACA,EAAE,OAAO,qBAAqB,OAAO,GAAG,CAAC;EACzC,EAAE,OAAO,yBAAyB,OAAO,KAAK,IAAI,GAAG,MAAM,MAAM,KAAK,CAAC,CAAC;EACxE,MAAM,KAAK;CACb;AACF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
//#region src/middleware/request-log.ts
|
|
5
|
+
/** Hono middleware logging request start/end with duration. */
|
|
6
|
+
function requestLogging(logger) {
|
|
7
|
+
return async (c, next) => {
|
|
8
|
+
const started = performance.now();
|
|
9
|
+
const id = c.get("requestId");
|
|
10
|
+
logger.info({
|
|
11
|
+
reqId: id,
|
|
12
|
+
method: c.req.method,
|
|
13
|
+
url: c.req.path
|
|
14
|
+
}, "request start");
|
|
15
|
+
await next();
|
|
16
|
+
logger.info({
|
|
17
|
+
reqId: id,
|
|
18
|
+
method: c.req.method,
|
|
19
|
+
url: c.req.path,
|
|
20
|
+
status: c.res.status,
|
|
21
|
+
durationMs: Math.round(performance.now() - started)
|
|
22
|
+
}, "request end");
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { requestLogging };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=request-log.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-log.mjs","names":[],"sources":["../../src/middleware/request-log.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\nimport type { Logger } from \"../logger.ts\";\n\n/** Hono middleware logging request start/end with duration. */\nexport function requestLogging(logger: Logger) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const started = performance.now();\n const id = c.get(\"requestId\");\n logger.info({ reqId: id, method: c.req.method, url: c.req.path }, \"request start\");\n await next();\n logger.info(\n {\n reqId: id,\n method: c.req.method,\n url: c.req.path,\n status: c.res.status,\n durationMs: Math.round(performance.now() - started),\n },\n \"request end\",\n );\n };\n}\n"],"mappings":";;;;;AAIA,SAAgB,eAAe,QAAgB;CAE7C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,UAAU,YAAY,IAAI;EAChC,MAAM,KAAK,EAAE,IAAI,WAAW;EAC5B,OAAO,KAAK;GAAE,OAAO;GAAI,QAAQ,EAAE,IAAI;GAAQ,KAAK,EAAE,IAAI;EAAK,GAAG,eAAe;EACjF,MAAM,KAAK;EACX,OAAO,KACL;GACE,OAAO;GACP,QAAQ,EAAE,IAAI;GACd,KAAK,EAAE,IAAI;GACX,QAAQ,EAAE,IAAI;GACd,YAAY,KAAK,MAAM,YAAY,IAAI,IAAI,OAAO;EACpD,GACA,aACF;CACF;AACF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
import { runWithStore } from "../store.mjs";
|
|
5
|
+
//#region src/middleware/store-scope.ts
|
|
6
|
+
/** Hono middleware running downstream handlers inside the request store. */
|
|
7
|
+
function storeScope(deps) {
|
|
8
|
+
return async (c, next) => {
|
|
9
|
+
const user = await deps.resolveUser(c);
|
|
10
|
+
await runWithStore({
|
|
11
|
+
db: deps.db,
|
|
12
|
+
storage: deps.storage,
|
|
13
|
+
config: deps.config,
|
|
14
|
+
ui: deps.ui,
|
|
15
|
+
logger: deps.logger,
|
|
16
|
+
user,
|
|
17
|
+
authEnabled: deps.authEnabled,
|
|
18
|
+
enqueueCapture: deps.enqueueCapture,
|
|
19
|
+
captureQueue: deps.captureQueue,
|
|
20
|
+
gitHosts: deps.gitHosts
|
|
21
|
+
}, async () => {
|
|
22
|
+
await next();
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { storeScope };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=store-scope.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-scope.mjs","names":[],"sources":["../../src/middleware/store-scope.ts"],"sourcesContent":["import type { Context, Next } from \"hono\";\nimport type { AuthUser } from \"../adapters/auth.ts\";\nimport type { CaptureQueue } from \"../adapters/capture-queue.ts\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport type { ShelfConfig, UIConfig } from \"../config.ts\";\nimport type { Logger } from \"../logger.ts\";\nimport { runWithStore } from \"../store.ts\";\n\n/** Dependencies scoped into the request store for router handlers. */\nexport interface StoreScopeDeps {\n db: DatabaseAdapter;\n storage: StorageAdapter;\n config: ShelfConfig;\n ui: UIConfig;\n logger: Logger;\n authEnabled: boolean;\n enqueueCapture?: (buildId: string, reqId?: string) => Promise<void>;\n captureQueue: CaptureQueue | null;\n gitHosts: GitHostProvider[];\n resolveUser: (c: Context) => Promise<AuthUser | null>;\n}\n\n/** Hono middleware running downstream handlers inside the request store. */\nexport function storeScope(deps: StoreScopeDeps) {\n // oxlint-disable-next-line typescript/no-invalid-void-type -- Hono middleware may not return Response\n return async (c: Context, next: Next): Promise<Response | void> => {\n const user = await deps.resolveUser(c);\n await runWithStore(\n {\n db: deps.db,\n storage: deps.storage,\n config: deps.config,\n ui: deps.ui,\n logger: deps.logger,\n user,\n authEnabled: deps.authEnabled,\n enqueueCapture: deps.enqueueCapture,\n captureQueue: deps.captureQueue,\n gitHosts: deps.gitHosts,\n },\n async () => {\n await next();\n },\n );\n };\n}\n"],"mappings":";;;;;;AAyBA,SAAgB,WAAW,MAAsB;CAE/C,OAAO,OAAO,GAAY,SAAyC;EACjE,MAAM,OAAO,MAAM,KAAK,YAAY,CAAC;EACrC,MAAM,aACJ;GACE,IAAI,KAAK;GACT,SAAS,KAAK;GACd,QAAQ,KAAK;GACb,IAAI,KAAK;GACT,QAAQ,KAAK;GACb;GACA,aAAa,KAAK;GAClB,gBAAgB,KAAK;GACrB,cAAc,KAAK;GACnB,UAAU,KAAK;EACjB,GACA,YAAY;GACV,MAAM,KAAK;EACb,CACF;CACF;AACF"}
|
package/dist/models/baseline.mjs
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { baselines } from "../schema.mjs";
|
|
5
4
|
import { ulid } from "../utils/ulid.mjs";
|
|
6
5
|
import { emitWebhookEvent } from "../adapters/webhook-events.mjs";
|
|
7
|
-
import {
|
|
6
|
+
import { baselines } from "../schema/baseline.mjs";
|
|
7
|
+
import { baselinePath } from "../paths.mjs";
|
|
8
8
|
import { and, eq } from "drizzle-orm";
|
|
9
9
|
//#region src/models/baseline.ts
|
|
10
|
+
/** Per-branch baseline screenshots with default-branch fallback. */
|
|
11
|
+
/** Data operations for baseline screenshots. */
|
|
10
12
|
var BaselineModel = class {
|
|
11
13
|
db;
|
|
12
14
|
storage;
|
|
13
|
-
|
|
15
|
+
secret;
|
|
16
|
+
constructor(db, storage, secret) {
|
|
14
17
|
this.db = db;
|
|
15
18
|
this.storage = storage;
|
|
19
|
+
this.secret = secret;
|
|
16
20
|
}
|
|
17
21
|
async getFor(projectId, storyId, viewport, branch) {
|
|
18
22
|
return (await this.db.list(baselines, {
|
|
@@ -48,7 +52,7 @@ var BaselineModel = class {
|
|
|
48
52
|
viewport,
|
|
49
53
|
branch,
|
|
50
54
|
snapshotId
|
|
51
|
-
});
|
|
55
|
+
}, this.secret);
|
|
52
56
|
} else {
|
|
53
57
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
54
58
|
baseline = await this.db.insert(baselines, {
|
|
@@ -68,7 +72,7 @@ var BaselineModel = class {
|
|
|
68
72
|
viewport,
|
|
69
73
|
branch,
|
|
70
74
|
snapshotId
|
|
71
|
-
});
|
|
75
|
+
}, this.secret);
|
|
72
76
|
}
|
|
73
77
|
return baseline;
|
|
74
78
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline.mjs","names":[],"sources":["../../src/models/baseline.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"baseline.mjs","names":[],"sources":["../../src/models/baseline.ts"],"sourcesContent":["/** Per-branch baseline screenshots with default-branch fallback. */\nimport { and, eq } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport type { StorageAdapter } from \"../adapters/storage.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { baselines } from \"../schema/baseline.ts\";\nimport type { Baseline } from \"../schema/baseline.ts\";\nimport { baselinePath } from \"../utils/paths.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Data operations for baseline screenshots. */\nexport class BaselineModel {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly storage: StorageAdapter,\n private readonly secret?: string,\n ) {}\n\n async getFor(\n projectId: string,\n storyId: string,\n viewport: string,\n branch: string,\n ): Promise<Baseline | null> {\n const rows = await this.db.list(baselines, {\n where: and(\n eq(baselines.projectId, projectId),\n eq(baselines.storyId, storyId),\n eq(baselines.viewportName, viewport),\n eq(baselines.branch, branch),\n ),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n async resolve(\n projectId: string,\n storyId: string,\n viewport: string,\n branch: string,\n defaultBranch: string,\n ): Promise<Baseline | null> {\n if (branch !== defaultBranch) {\n const own = await this.getFor(projectId, storyId, viewport, branch);\n if (own) {\n return own;\n }\n }\n return this.getFor(projectId, storyId, viewport, defaultBranch);\n }\n\n async read(baseline: Baseline): Promise<Buffer> {\n return await this.storage.read(baseline.screenshotPath);\n }\n\n async upsert(\n projectId: string,\n storyId: string,\n viewport: string,\n branch: string,\n snapshotId: string,\n sourcePath: string,\n ): Promise<Baseline> {\n const screenshotPath = baselinePath(projectId, branch, storyId, viewport);\n const source = await this.storage.read(sourcePath);\n await this.storage.write(screenshotPath, source);\n\n const existing = await this.getFor(projectId, storyId, viewport, branch);\n let baseline: Baseline;\n if (existing) {\n baseline = await this.db.update(baselines, existing.id, {\n snapshotId,\n screenshotPath,\n updatedAt: new Date().toISOString(),\n });\n await emitWebhookEvent(this.db, projectId, \"baseline:updated\", {\n baselineId: baseline.id,\n storyId,\n viewport,\n branch,\n snapshotId,\n }, this.secret);\n } else {\n const now = new Date().toISOString();\n baseline = await this.db.insert(baselines, {\n id: ulid(),\n projectId,\n storyId,\n viewportName: viewport,\n branch,\n snapshotId,\n screenshotPath,\n createdAt: now,\n updatedAt: now,\n });\n await emitWebhookEvent(this.db, projectId, \"baseline:created\", {\n baselineId: baseline.id,\n storyId,\n viewport,\n branch,\n snapshotId,\n }, this.secret);\n }\n return baseline;\n }\n\n async list(projectId: string): Promise<Baseline[]> {\n return await this.db.list(baselines, { where: eq(baselines.projectId, projectId) });\n }\n\n async removeOrphans(projectId: string, validStoryIds: Set<string>): Promise<number> {\n const all = await this.list(projectId);\n const toRemove = all.filter((baseline) => !validStoryIds.has(baseline.storyId));\n await Promise.all(\n toRemove.map(async (baseline) => {\n await this.db.remove(baselines, baseline.id);\n }),\n );\n return toRemove.length;\n }\n}\n"],"mappings":";;;;;;;;;;;AAWA,IAAa,gBAAb,MAA2B;CAEN;CACA;CACA;CAHnB,YACE,IACA,SACA,QACA;EAHiB,KAAA,KAAA;EACA,KAAA,UAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,OACJ,WACA,SACA,UACA,QAC0B;EAU1B,QAAO,MATY,KAAK,GAAG,KAAK,WAAW;GACzC,OAAO,IACL,GAAG,UAAU,WAAW,SAAS,GACjC,GAAG,UAAU,SAAS,OAAO,GAC7B,GAAG,UAAU,cAAc,QAAQ,GACnC,GAAG,UAAU,QAAQ,MAAM,CAC7B;GACA,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;CAEA,MAAM,QACJ,WACA,SACA,UACA,QACA,eAC0B;EAC1B,IAAI,WAAW,eAAe;GAC5B,MAAM,MAAM,MAAM,KAAK,OAAO,WAAW,SAAS,UAAU,MAAM;GAClE,IAAI,KACF,OAAO;EAEX;EACA,OAAO,KAAK,OAAO,WAAW,SAAS,UAAU,aAAa;CAChE;CAEA,MAAM,KAAK,UAAqC;EAC9C,OAAO,MAAM,KAAK,QAAQ,KAAK,SAAS,cAAc;CACxD;CAEA,MAAM,OACJ,WACA,SACA,UACA,QACA,YACA,YACmB;EACnB,MAAM,iBAAiB,aAAa,WAAW,QAAQ,SAAS,QAAQ;EACxE,MAAM,SAAS,MAAM,KAAK,QAAQ,KAAK,UAAU;EACjD,MAAM,KAAK,QAAQ,MAAM,gBAAgB,MAAM;EAE/C,MAAM,WAAW,MAAM,KAAK,OAAO,WAAW,SAAS,UAAU,MAAM;EACvE,IAAI;EACJ,IAAI,UAAU;GACZ,WAAW,MAAM,KAAK,GAAG,OAAO,WAAW,SAAS,IAAI;IACtD;IACA;IACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;GACpC,CAAC;GACD,MAAM,iBAAiB,KAAK,IAAI,WAAW,oBAAoB;IAC7D,YAAY,SAAS;IACrB;IACA;IACA;IACA;GACF,GAAG,KAAK,MAAM;EAChB,OAAO;GACL,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;GACnC,WAAW,MAAM,KAAK,GAAG,OAAO,WAAW;IACzC,IAAI,KAAK;IACT;IACA;IACA,cAAc;IACd;IACA;IACA;IACA,WAAW;IACX,WAAW;GACb,CAAC;GACD,MAAM,iBAAiB,KAAK,IAAI,WAAW,oBAAoB;IAC7D,YAAY,SAAS;IACrB;IACA;IACA;IACA;GACF,GAAG,KAAK,MAAM;EAChB;EACA,OAAO;CACT;CAEA,MAAM,KAAK,WAAwC;EACjD,OAAO,MAAM,KAAK,GAAG,KAAK,WAAW,EAAE,OAAO,GAAG,UAAU,WAAW,SAAS,EAAE,CAAC;CACpF;CAEA,MAAM,cAAc,WAAmB,eAA6C;EAElF,MAAM,YAAW,MADC,KAAK,KAAK,SAAS,EAAA,CAChB,QAAQ,aAAa,CAAC,cAAc,IAAI,SAAS,OAAO,CAAC;EAC9E,MAAM,QAAQ,IACZ,SAAS,IAAI,OAAO,aAAa;GAC/B,MAAM,KAAK,GAAG,OAAO,WAAW,SAAS,EAAE;EAC7C,CAAC,CACH;EACA,OAAO,SAAS;CAClB;AACF"}
|
package/dist/models/build.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import {
|
|
4
|
+
import { builds } from "../schema/build.mjs";
|
|
5
|
+
import { buildLabels } from "../schema/label.mjs";
|
|
6
|
+
import { snapshots } from "../schema/snapshot.mjs";
|
|
5
7
|
import { ulid } from "../utils/ulid.mjs";
|
|
6
8
|
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
7
9
|
//#region src/models/build.ts
|
|
10
|
+
/** Build records, status transitions, and publication helpers. */
|
|
8
11
|
/**
|
|
9
12
|
* Return whether a build is publicly viewable without a session.
|
|
10
13
|
*
|
|
@@ -17,6 +20,7 @@ function isPublicBuild(project, build) {
|
|
|
17
20
|
if (!project.publicBranchRegex) return false;
|
|
18
21
|
return new RegExp(project.publicBranchRegex, "u").test(build.gitBranch);
|
|
19
22
|
}
|
|
23
|
+
/** Data operations for build records. */
|
|
20
24
|
var BuildModel = class {
|
|
21
25
|
db;
|
|
22
26
|
constructor(db) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.mjs","names":[],"sources":["../../src/models/build.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"build.mjs","names":[],"sources":["../../src/models/build.ts"],"sourcesContent":["/** Build records, status transitions, and publication helpers. */\nimport { and, desc, eq, inArray } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { builds } from \"../schema/build.ts\";\nimport type { Build } from \"../schema/build.ts\";\nimport { buildLabels } from \"../schema/label.ts\";\nimport { snapshots } from \"../schema/snapshot.ts\";\nimport type { BuildStatus } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/**\n * Return whether a build is publicly viewable without a session.\n *\n * A build is public iff `builds.public` is set or its branch matches the\n * project's `public_branch_regex` (ADR 0011). Supplying an empty/nonexistent\n * regex makes every build require auth.\n */\nexport function isPublicBuild(\n project: { publicBranchRegex: string | null },\n build: Pick<Build, \"public\" | \"gitBranch\">,\n): boolean {\n if (build.public) {\n return true;\n }\n if (!project.publicBranchRegex) {\n return false;\n }\n return new RegExp(project.publicBranchRegex, \"u\").test(build.gitBranch);\n}\n\n/** Input for creating a build. */\nexport interface BuildCreateInput {\n gitSha: string;\n gitBranch: string;\n isDefault?: boolean;\n authorEmail?: string;\n authorName?: string;\n message?: string;\n public?: boolean;\n}\n\n/** Filter options for listing builds. */\nexport interface BuildListFilter {\n status?: BuildStatus;\n branch?: string;\n labelKey?: string;\n labelValue?: string;\n}\n\n/** Data operations for build records. */\nexport class BuildModel {\n constructor(private readonly db: DatabaseAdapter) {}\n\n async create(projectId: string, input: BuildCreateInput): Promise<Build> {\n const now = new Date().toISOString();\n return await this.db.insert(builds, {\n id: ulid(),\n projectId,\n gitSha: input.gitSha,\n gitBranch: input.gitBranch,\n isDefault: input.isDefault ?? false,\n authorEmail: input.authorEmail,\n authorName: input.authorName,\n message: input.message,\n public: input.public ?? false,\n status: \"pending\",\n createdAt: now,\n updatedAt: now,\n });\n }\n\n async get(id: string): Promise<Build | null> {\n return await this.db.get(builds, id);\n }\n\n async list(projectId: string, filter: BuildListFilter = {}): Promise<Build[]> {\n const conditions = [eq(builds.projectId, projectId)];\n if (filter.status) {\n conditions.push(eq(builds.status, filter.status));\n }\n if (filter.branch) {\n conditions.push(eq(builds.gitBranch, filter.branch));\n }\n if (filter.labelKey && filter.labelValue) {\n const labels = await this.db.list(buildLabels, {\n where: and(\n eq(buildLabels.projectId, projectId),\n eq(buildLabels.typeKey, filter.labelKey),\n eq(buildLabels.value, filter.labelValue),\n ),\n });\n const ids = labels.map((l) => l.buildId);\n if (ids.length === 0) {\n return [];\n }\n conditions.push(inArray(builds.id, ids));\n }\n return this.db.list(builds, { where: and(...conditions), orderBy: desc(builds.createdAt) });\n }\n\n /** Return the most recent build intended for publishing, if any. */\n async latestPublished(project: {\n id: string;\n publicBranchRegex: string | null;\n }): Promise<Build | null> {\n const rows = await this.list(project.id);\n for (const build of rows) {\n if (isPublicBuild(project, build)) {\n return build;\n }\n }\n return null;\n }\n\n async update(\n id: string,\n patch: Partial<Pick<Build, \"status\" | \"public\" | \"message\" | \"authorEmail\" | \"authorName\">>,\n ): Promise<Build> {\n return await this.db.update(builds, id, { ...patch, updatedAt: new Date().toISOString() });\n }\n\n async setStatus(id: string, status: BuildStatus): Promise<Build> {\n return await this.update(id, { status });\n }\n\n async updateCounts(id: string): Promise<Build> {\n const rows = await this.db.list(snapshots, { where: eq(snapshots.buildId, id) });\n const snapshotCount = rows.length;\n const changedCount = rows.filter((s) => s.status === \"changed\" || s.status === \"new\").length;\n const approvedCount = rows.filter(\n (s) => s.status === \"approved\" || s.status === \"unchanged\",\n ).length;\n const rejectedCount = rows.filter((s) => s.status === \"rejected\").length;\n return this.db.update(builds, id, {\n snapshotCount,\n changedCount,\n approvedCount,\n rejectedCount,\n updatedAt: new Date().toISOString(),\n });\n }\n\n async remove(id: string): Promise<void> {\n await this.db.remove(builds, id);\n }\n}\n"],"mappings":"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"}
|
package/dist/models/comment.mjs
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import {
|
|
4
|
+
import { projects } from "../schema/project.mjs";
|
|
5
5
|
import { ulid } from "../utils/ulid.mjs";
|
|
6
|
+
import { comments } from "../schema/comment.mjs";
|
|
6
7
|
import { eq } from "drizzle-orm";
|
|
7
8
|
//#region src/models/comment.ts
|
|
9
|
+
/** Build and snapshot review comments. */
|
|
10
|
+
/** Data operations for review comments. */
|
|
8
11
|
var CommentModel = class {
|
|
9
12
|
db;
|
|
10
13
|
constructor(db) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment.mjs","names":[],"sources":["../../src/models/comment.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"comment.mjs","names":[],"sources":["../../src/models/comment.ts"],"sourcesContent":["/** Build and snapshot review comments. */\nimport { eq } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { comments } from \"../schema/comment.ts\";\nimport type { Comment } from \"../schema/comment.ts\";\nimport { projects } from \"../schema/project.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Input for creating a review comment. */\nexport interface CommentCreateInput {\n body: string;\n snapshotId?: string;\n parentId?: string;\n}\n\n/** Data operations for review comments. */\nexport class CommentModel {\n constructor(private readonly db: DatabaseAdapter) {}\n\n async listByBuild(buildId: string): Promise<Comment[]> {\n return await this.db.list(comments, { where: eq(comments.buildId, buildId) });\n }\n\n async create(\n projectId: string,\n buildId: string,\n userId: string,\n input: CommentCreateInput,\n ): Promise<Comment> {\n const now = new Date().toISOString();\n const project = await this.db.get(projects, projectId);\n if (!project) {\n throw new Error(`Project not found: ${projectId}`);\n }\n return await this.db.insert(comments, {\n id: ulid(),\n projectId,\n buildId,\n snapshotId: input.snapshotId,\n userId,\n body: input.body,\n parentId: input.parentId,\n resolved: false,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n async resolve(id: string): Promise<Comment> {\n return await this.db.update(comments, id, {\n resolved: true,\n updatedAt: new Date().toISOString(),\n });\n }\n}\n"],"mappings":";;;;;;;;;;AAgBA,IAAa,eAAb,MAA0B;CACK;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;CAEnD,MAAM,YAAY,SAAqC;EACrD,OAAO,MAAM,KAAK,GAAG,KAAK,UAAU,EAAE,OAAO,GAAG,SAAS,SAAS,OAAO,EAAE,CAAC;CAC9E;CAEA,MAAM,OACJ,WACA,SACA,QACA,OACkB;EAClB,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EAEnC,IAAI,CAAC,MADiB,KAAK,GAAG,IAAI,UAAU,SAAS,GAEnD,MAAM,IAAI,MAAM,sBAAsB,WAAW;EAEnD,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU;GACpC,IAAI,KAAK;GACT;GACA;GACA,YAAY,MAAM;GAClB;GACA,MAAM,MAAM;GACZ,UAAU,MAAM;GAChB,UAAU;GACV,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAEA,MAAM,QAAQ,IAA8B;EAC1C,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU,IAAI;GACxC,UAAU;GACV,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;AACF"}
|
package/dist/models/label.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import {
|
|
4
|
+
import { builds } from "../schema/build.mjs";
|
|
5
|
+
import { buildLabels, labelTypes } from "../schema/label.mjs";
|
|
5
6
|
import { ulid } from "../utils/ulid.mjs";
|
|
6
7
|
import { PERSISTENT_LABEL_KEY, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS } from "../types.mjs";
|
|
7
8
|
import { and, desc, eq } from "drizzle-orm";
|
|
8
9
|
//#region src/models/label.ts
|
|
10
|
+
/** Build labels and project label types. */
|
|
9
11
|
const SEEDED_NAMES = {
|
|
10
12
|
branch: "Branch",
|
|
11
13
|
persistent: "Persistent",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"label.mjs","names":[],"sources":["../../src/models/label.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"label.mjs","names":[],"sources":["../../src/models/label.ts"],"sourcesContent":["/** Build labels and project label types. */\nimport { and, desc, eq } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { builds } from \"../schema/build.ts\";\nimport { buildLabels, labelTypes } from \"../schema/label.ts\";\nimport type { BuildLabel, LabelType } from \"../schema/label.ts\";\nimport { PERSISTENT_LABEL_KEY, RESERVED_LABEL_KEYS, SEEDED_LABEL_KEYS } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\nconst SEEDED_NAMES: Record<string, string> = {\n branch: \"Branch\",\n persistent: \"Persistent\",\n pr: \"Pull request\",\n mr: \"Merge request\",\n jira: \"Jira issue\",\n linear: \"Linear issue\",\n figma: \"Figma file\",\n custom: \"Custom\",\n};\n\n/** Data operations for label types and build labels. */\nexport class LabelModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Seed the default label types for a project if missing.\n *\n * @param projectId - Project ID.\n */\n async seedFor(projectId: string): Promise<void> {\n const now = new Date().toISOString();\n const existing = await this.listTypes(projectId);\n const existingKeys = new Set(existing.map((t) => t.key));\n const missing = SEEDED_LABEL_KEYS.filter((key) => !existingKeys.has(key));\n await Promise.all(\n missing.map(async (key) => {\n await this.db.insert(labelTypes, {\n id: ulid(),\n projectId,\n key,\n name: SEEDED_NAMES[key] ?? key,\n createdAt: now,\n });\n }),\n );\n }\n\n /** List all label types for a project. */\n async listTypes(projectId: string): Promise<LabelType[]> {\n return await this.db.list(labelTypes, { where: eq(labelTypes.projectId, projectId) });\n }\n\n /** Fetch a label type by key, or null if not found. */\n async getType(projectId: string, key: string): Promise<LabelType | null> {\n const rows = await this.db.list(labelTypes, {\n where: and(eq(labelTypes.projectId, projectId), eq(labelTypes.key, key)),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Create a custom label type for a project. */\n async createType(\n projectId: string,\n input: { key: string; name: string; linkTemplate?: string; color?: string },\n ): Promise<LabelType> {\n return await this.db.insert(labelTypes, {\n id: ulid(),\n projectId,\n key: input.key,\n name: input.name,\n linkTemplate: input.linkTemplate,\n color: input.color,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** Update a custom label type's name, template or color. */\n async updateType(\n projectId: string,\n key: string,\n input: { name?: string; linkTemplate?: string | null; color?: string | null },\n ): Promise<LabelType | null> {\n if (key === PERSISTENT_LABEL_KEY || RESERVED_LABEL_KEYS.includes(key as never)) {\n throw new Error(`Label type '${key}' cannot be updated.`);\n }\n const existing = await this.getType(projectId, key);\n if (!existing) {\n return null;\n }\n return await this.db.update(labelTypes, existing.id, {\n name: input.name ?? existing.name,\n linkTemplate: input.linkTemplate === undefined ? existing.linkTemplate : input.linkTemplate,\n color: input.color === undefined ? existing.color : input.color,\n });\n }\n\n /** Remove a custom label type, rejecting reserved or persistent types. */\n async removeType(projectId: string, key: string): Promise<void> {\n if (key === PERSISTENT_LABEL_KEY || RESERVED_LABEL_KEYS.includes(key as never)) {\n throw new Error(`Label type '${key}' cannot be removed.`);\n }\n const existing = await this.getType(projectId, key);\n if (existing) {\n await this.db.remove(labelTypes, existing.id);\n }\n }\n\n /** Attach a label value to a build. */\n async attach(\n projectId: string,\n buildId: string,\n typeKey: string,\n value: string,\n ): Promise<BuildLabel> {\n return await this.db.insert(buildLabels, {\n id: ulid(),\n projectId,\n buildId,\n typeKey,\n value,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** List all labels attached to a build. */\n async listForBuild(buildId: string): Promise<BuildLabel[]> {\n return await this.db.list(buildLabels, { where: eq(buildLabels.buildId, buildId) });\n }\n\n /** Return the id of the latest build carrying a given label value, if any. */\n async latestBuildId(projectId: string, typeKey: string, value: string): Promise<string | null> {\n const labels = await this.db.list(buildLabels, {\n where: and(\n eq(buildLabels.projectId, projectId),\n eq(buildLabels.typeKey, typeKey),\n eq(buildLabels.value, value),\n ),\n });\n if (labels.length === 0) {\n return null;\n }\n const idSet = new Set(labels.map((l) => l.buildId));\n const rows = await this.db.list(builds, { orderBy: desc(builds.createdAt) });\n return rows.find((b) => idSet.has(b.id))?.id ?? null;\n }\n\n /** Return whether a build is marked as persistent. */\n async hasPersistent(projectId: string, buildId: string): Promise<boolean> {\n const labels = await this.db.list(buildLabels, {\n where: and(\n eq(buildLabels.projectId, projectId),\n eq(buildLabels.buildId, buildId),\n eq(buildLabels.typeKey, PERSISTENT_LABEL_KEY),\n ),\n });\n return labels.length > 0;\n }\n}\n"],"mappings":"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"}
|
package/dist/models/member.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { projectMembers } from "../schema.mjs";
|
|
5
4
|
import { ulid } from "../utils/ulid.mjs";
|
|
5
|
+
import { projectMembers } from "../schema/member.mjs";
|
|
6
6
|
import { and, eq } from "drizzle-orm";
|
|
7
7
|
//#region src/models/member.ts
|
|
8
|
+
/** Project membership and role resolution. */
|
|
8
9
|
/** Data operations for project membership and roles. */
|
|
9
10
|
var MemberModel = class {
|
|
10
11
|
db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member.mjs","names":[],"sources":["../../src/models/member.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"member.mjs","names":[],"sources":["../../src/models/member.ts"],"sourcesContent":["/** Project membership and role resolution. */\nimport { and, eq } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projectMembers } from \"../schema/member.ts\";\nimport type { ProjectMember } from \"../schema/member.ts\";\nimport type { ProjectRole, SiteRole } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Data operations for project membership and roles. */\nexport class MemberModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /** List all members of a project. */\n async list(projectId: string): Promise<ProjectMember[]> {\n return await this.db.list(projectMembers, { where: eq(projectMembers.projectId, projectId) });\n }\n\n /** Fetch a project member by project and user id, or null if not found. */\n async get(projectId: string, userId: string): Promise<ProjectMember | null> {\n const rows = await this.db.list(projectMembers, {\n where: and(eq(projectMembers.projectId, projectId), eq(projectMembers.userId, userId)),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Set a user's role in a project, creating the membership if needed. */\n async set(projectId: string, userId: string, role: ProjectRole): Promise<ProjectMember> {\n const existing = await this.get(projectId, userId);\n if (existing) {\n return this.db.update(projectMembers, existing.id, { role });\n }\n return this.db.insert(projectMembers, {\n id: ulid(),\n projectId,\n userId,\n role,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** Remove a user from a project if they are a member. */\n async remove(projectId: string, userId: string): Promise<void> {\n const existing = await this.get(projectId, userId);\n if (existing) {\n await this.db.remove(projectMembers, existing.id);\n }\n }\n\n /** Resolve a user's effective project role, honoring site-wide admins. */\n async effectiveRole(\n siteRole: SiteRole,\n projectId: string,\n userId: string,\n ): Promise<ProjectRole | null> {\n if (siteRole === \"admin\") {\n return \"admin\";\n }\n const member = await this.get(projectId, userId);\n return member?.role ?? null;\n }\n}\n"],"mappings":";;;;;;;;;AASA,IAAa,cAAb,MAAyB;CAIM;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;CAGnD,MAAM,KAAK,WAA6C;EACtD,OAAO,MAAM,KAAK,GAAG,KAAK,gBAAgB,EAAE,OAAO,GAAG,eAAe,WAAW,SAAS,EAAE,CAAC;CAC9F;;CAGA,MAAM,IAAI,WAAmB,QAA+C;EAK1E,QAAO,MAJY,KAAK,GAAG,KAAK,gBAAgB;GAC9C,OAAO,IAAI,GAAG,eAAe,WAAW,SAAS,GAAG,GAAG,eAAe,QAAQ,MAAM,CAAC;GACrF,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;;CAGA,MAAM,IAAI,WAAmB,QAAgB,MAA2C;EACtF,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,MAAM;EACjD,IAAI,UACF,OAAO,KAAK,GAAG,OAAO,gBAAgB,SAAS,IAAI,EAAE,KAAK,CAAC;EAE7D,OAAO,KAAK,GAAG,OAAO,gBAAgB;GACpC,IAAI,KAAK;GACT;GACA;GACA;GACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,OAAO,WAAmB,QAA+B;EAC7D,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,MAAM;EACjD,IAAI,UACF,MAAM,KAAK,GAAG,OAAO,gBAAgB,SAAS,EAAE;CAEpD;;CAGA,MAAM,cACJ,UACA,WACA,QAC6B;EAC7B,IAAI,aAAa,SACf,OAAO;EAGT,QAAO,MADc,KAAK,IAAI,WAAW,MAAM,EAAA,EAChC,QAAQ;CACzB;AACF"}
|
package/dist/models/project.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { projects } from "../schema.mjs";
|
|
4
|
+
import { projects } from "../schema/project.mjs";
|
|
5
5
|
import { slugify, ulid } from "../utils/ulid.mjs";
|
|
6
6
|
import { eq } from "drizzle-orm";
|
|
7
7
|
//#region src/models/project.ts
|
|
8
|
+
/** Project records and slug management. */
|
|
8
9
|
/** Data operations for project records. */
|
|
9
10
|
var ProjectModel = class {
|
|
10
11
|
db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project.mjs","names":[],"sources":["../../src/models/project.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"project.mjs","names":[],"sources":["../../src/models/project.ts"],"sourcesContent":["/** Project records and slug management. */\nimport { eq } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projects } from \"../schema/project.ts\";\nimport type { Project } from \"../schema/project.ts\";\nimport { slugify, ulid } from \"../utils/ulid.ts\";\n\n/** Input for creating a project. */\nexport interface ProjectCreateInput {\n name: string;\n gitRepository?: string;\n gitDefaultBranch?: string;\n storybookMeta?: unknown;\n}\n\n/** Data operations for project records. */\nexport class ProjectModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a project with a unique slug.\n *\n * @param input - Project creation input.\n * @returns The created project.\n */\n async create(input: ProjectCreateInput): Promise<Project> {\n const now = new Date().toISOString();\n const slug = await this.uniqueSlug(input.name);\n return this.db.insert(projects, {\n id: ulid(),\n name: input.name,\n slug,\n gitRepository: input.gitRepository,\n gitDefaultBranch: input.gitDefaultBranch ?? \"main\",\n storybookMeta: input.storybookMeta ? JSON.stringify(input.storybookMeta) : null,\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** Fetch a project by id, or null if not found. */\n async get(id: string): Promise<Project | null> {\n return await this.db.get(projects, id);\n }\n\n /** Fetch a project by its slug, or null if not found. */\n async getBySlug(slug: string): Promise<Project | null> {\n const rows = await this.db.list(projects, { where: eq(projects.slug, slug), limit: 1 });\n return rows[0] ?? null;\n }\n\n /** List all projects. */\n async list(): Promise<Project[]> {\n return await this.db.list(projects);\n }\n\n /** Update mutable fields of a project. */\n async update(\n id: string,\n patch: Partial<Omit<Project, \"storybookMeta\">> & {\n storybookMeta?: unknown;\n },\n ): Promise<Project> {\n const normalized: Partial<Project> & Record<string, unknown> = {\n ...patch,\n } as unknown as Partial<Project> & Record<string, unknown>;\n if (\n patch.storybookMeta !== undefined &&\n patch.storybookMeta !== null &&\n typeof patch.storybookMeta !== \"string\"\n ) {\n normalized.storybookMeta = JSON.stringify(patch.storybookMeta);\n }\n return await this.db.update(projects, id, {\n ...normalized,\n updatedAt: new Date().toISOString(),\n });\n }\n\n /** Delete a project by id. */\n async remove(id: string): Promise<void> {\n await this.db.remove(projects, id);\n }\n\n private async uniqueSlug(name: string): Promise<string> {\n const base = slugify(name) || \"project\";\n const existing = await this.db.list(projects, { where: eq(projects.slug, base) });\n if (existing.length === 0) {\n return base;\n }\n const taken = new Set(existing.map((p) => p.slug));\n let suffix = 2;\n while (taken.has(`${base}-${suffix}`)) {\n suffix += 1;\n }\n return `${base}-${suffix}`;\n }\n}\n"],"mappings":";;;;;;;;;AAgBA,IAAa,eAAb,MAA0B;CAIK;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;CAQnD,MAAM,OAAO,OAA6C;EACxD,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,MAAM,OAAO,MAAM,KAAK,WAAW,MAAM,IAAI;EAC7C,OAAO,KAAK,GAAG,OAAO,UAAU;GAC9B,IAAI,KAAK;GACT,MAAM,MAAM;GACZ;GACA,eAAe,MAAM;GACrB,kBAAkB,MAAM,oBAAoB;GAC5C,eAAe,MAAM,gBAAgB,KAAK,UAAU,MAAM,aAAa,IAAI;GAC3E,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,IAAI,IAAqC;EAC7C,OAAO,MAAM,KAAK,GAAG,IAAI,UAAU,EAAE;CACvC;;CAGA,MAAM,UAAU,MAAuC;EAErD,QAAO,MADY,KAAK,GAAG,KAAK,UAAU;GAAE,OAAO,GAAG,SAAS,MAAM,IAAI;GAAG,OAAO;EAAE,CAAC,EAAA,CAC1E,MAAM;CACpB;;CAGA,MAAM,OAA2B;EAC/B,OAAO,MAAM,KAAK,GAAG,KAAK,QAAQ;CACpC;;CAGA,MAAM,OACJ,IACA,OAGkB;EAClB,MAAM,aAAyD,EAC7D,GAAG,MACL;EACA,IACE,MAAM,kBAAkB,KAAA,KACxB,MAAM,kBAAkB,QACxB,OAAO,MAAM,kBAAkB,UAE/B,WAAW,gBAAgB,KAAK,UAAU,MAAM,aAAa;EAE/D,OAAO,MAAM,KAAK,GAAG,OAAO,UAAU,IAAI;GACxC,GAAG;GACH,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,GAAG,OAAO,UAAU,EAAE;CACnC;CAEA,MAAc,WAAW,MAA+B;EACtD,MAAM,OAAO,QAAQ,IAAI,KAAK;EAC9B,MAAM,WAAW,MAAM,KAAK,GAAG,KAAK,UAAU,EAAE,OAAO,GAAG,SAAS,MAAM,IAAI,EAAE,CAAC;EAChF,IAAI,SAAS,WAAW,GACtB,OAAO;EAET,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,IAAI,CAAC;EACjD,IAAI,SAAS;EACb,OAAO,MAAM,IAAI,GAAG,KAAK,GAAG,QAAQ,GAClC,UAAU;EAEZ,OAAO,GAAG,KAAK,GAAG;CACpB;AACF"}
|
package/dist/models/snapshot.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { snapshots } from "../schema.mjs";
|
|
4
|
+
import { snapshots } from "../schema/snapshot.mjs";
|
|
5
5
|
import { ulid } from "../utils/ulid.mjs";
|
|
6
6
|
import { eq } from "drizzle-orm";
|
|
7
7
|
//#region src/models/snapshot.ts
|
|
8
|
+
/** Snapshot records for captured stories within a build. */
|
|
8
9
|
/** Data operations for snapshot records. */
|
|
9
10
|
var SnapshotModel = class {
|
|
10
11
|
db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.mjs","names":[],"sources":["../../src/models/snapshot.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"snapshot.mjs","names":[],"sources":["../../src/models/snapshot.ts"],"sourcesContent":["/** Snapshot records for captured stories within a build. */\nimport { eq } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { snapshots } from \"../schema/snapshot.ts\";\nimport type { Snapshot } from \"../schema/snapshot.ts\";\nimport type { SnapshotStatus } from \"../types.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Input for creating a snapshot. */\nexport interface SnapshotCreateInput {\n storyId: string;\n storyName: string;\n storyTitle: string;\n storyImportPath?: string;\n viewportName: string;\n viewportWidth: number;\n viewportHeight: number;\n screenshotPath: string;\n}\n\n/** Data operations for snapshot records. */\nexport class SnapshotModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a snapshot for a story within a build.\n *\n * @param projectId - Project ID.\n * @param buildId - Build ID.\n * @param input - Snapshot creation input.\n * @returns The created snapshot.\n */\n async create(projectId: string, buildId: string, input: SnapshotCreateInput): Promise<Snapshot> {\n const now = new Date().toISOString();\n return await this.db.insert(snapshots, {\n id: ulid(),\n projectId,\n buildId,\n storyId: input.storyId,\n storyName: input.storyName,\n storyTitle: input.storyTitle,\n storyImportPath: input.storyImportPath,\n viewportName: input.viewportName,\n viewportWidth: input.viewportWidth,\n viewportHeight: input.viewportHeight,\n screenshotPath: input.screenshotPath,\n status: \"pending\",\n createdAt: now,\n updatedAt: now,\n });\n }\n\n /** List all snapshots belonging to a build. */\n async listByBuild(buildId: string): Promise<Snapshot[]> {\n return await this.db.list(snapshots, { where: eq(snapshots.buildId, buildId) });\n }\n\n /** Fetch a snapshot by id, or null if not found. */\n async get(id: string): Promise<Snapshot | null> {\n return await this.db.get(snapshots, id);\n }\n\n /** Update mutable fields of a snapshot. */\n async update(id: string, patch: Partial<Snapshot>): Promise<Snapshot> {\n return await this.db.update(snapshots, id, { ...patch, updatedAt: new Date().toISOString() });\n }\n\n /** Set the status of a snapshot. */\n async setStatus(id: string, status: SnapshotStatus): Promise<Snapshot> {\n return await this.update(id, { status });\n }\n\n /** Record a reviewer's decision on a snapshot. */\n async review(id: string, status: SnapshotStatus, userId: string): Promise<Snapshot> {\n return await this.update(id, {\n status,\n reviewedBy: userId,\n reviewedAt: new Date().toISOString(),\n });\n }\n}\n"],"mappings":";;;;;;;;;AAqBA,IAAa,gBAAb,MAA2B;CAII;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;;;CAUnD,MAAM,OAAO,WAAmB,SAAiB,OAA+C;EAC9F,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,WAAW;GACrC,IAAI,KAAK;GACT;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB,YAAY,MAAM;GAClB,iBAAiB,MAAM;GACvB,cAAc,MAAM;GACpB,eAAe,MAAM;GACrB,gBAAgB,MAAM;GACtB,gBAAgB,MAAM;GACtB,QAAQ;GACR,WAAW;GACX,WAAW;EACb,CAAC;CACH;;CAGA,MAAM,YAAY,SAAsC;EACtD,OAAO,MAAM,KAAK,GAAG,KAAK,WAAW,EAAE,OAAO,GAAG,UAAU,SAAS,OAAO,EAAE,CAAC;CAChF;;CAGA,MAAM,IAAI,IAAsC;EAC9C,OAAO,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE;CACxC;;CAGA,MAAM,OAAO,IAAY,OAA6C;EACpE,OAAO,MAAM,KAAK,GAAG,OAAO,WAAW,IAAI;GAAE,GAAG;GAAO,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EAAE,CAAC;CAC9F;;CAGA,MAAM,UAAU,IAAY,QAA2C;EACrE,OAAO,MAAM,KAAK,OAAO,IAAI,EAAE,OAAO,CAAC;CACzC;;CAGA,MAAM,OAAO,IAAY,QAAwB,QAAmC;EAClF,OAAO,MAAM,KAAK,OAAO,IAAI;GAC3B;GACA,YAAY;GACZ,6BAAY,IAAI,KAAK,EAAA,CAAE,YAAY;EACrC,CAAC;CACH;AACF"}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { projectStatusConfigs } from "../schema.mjs";
|
|
5
4
|
import { ulid } from "../utils/ulid.mjs";
|
|
6
5
|
import { decrypt, encrypt } from "../utils/encrypt.mjs";
|
|
6
|
+
import { projectStatusConfigs } from "../schema/status-config.mjs";
|
|
7
7
|
import { eq } from "drizzle-orm";
|
|
8
8
|
//#region src/models/status-config.ts
|
|
9
|
+
/** Per-project git-provider status check configurations. */
|
|
9
10
|
/** Data operations for per-project status provider configs. */
|
|
10
11
|
var StatusConfigModel = class {
|
|
11
12
|
db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"status-config.mjs","names":[],"sources":["../../src/models/status-config.ts"],"sourcesContent":["/* oxlint-disable eslint/no-await-in-loop, typescript/promise-function-async, eslint/require-await */\nimport { eq } from \"drizzle-orm\";\
|
|
1
|
+
{"version":3,"file":"status-config.mjs","names":[],"sources":["../../src/models/status-config.ts"],"sourcesContent":["/* oxlint-disable eslint/no-await-in-loop, typescript/promise-function-async, eslint/require-await */\n/** Per-project git-provider status check configurations. */\nimport { eq } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { projectStatusConfigs } from \"../schema/status-config.ts\";\nimport type { ProjectStatusConfig } from \"../schema/status-config.ts\";\nimport { decrypt, encrypt } from \"../utils/encrypt.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Input for creating a git-provider status config. */\nexport interface StatusConfigCreateInput {\n provider: string;\n config: unknown;\n token: string;\n}\n\n/** Data operations for per-project status provider configs. */\nexport class StatusConfigModel {\n constructor(\n private readonly db: DatabaseAdapter,\n private readonly secret: string | undefined,\n ) {}\n\n /** List all status configs for a project. */\n async list(projectId: string): Promise<ProjectStatusConfig[]> {\n return await this.db.list(projectStatusConfigs, {\n where: eq(projectStatusConfigs.projectId, projectId),\n });\n }\n\n /** Fetch a config by id scoped to a project, or null. */\n async get(projectId: string, id: string): Promise<ProjectStatusConfig | null> {\n const rows = await this.db.list(projectStatusConfigs, {\n where: eq(projectStatusConfigs.id, id),\n limit: 1,\n });\n const found = rows[0] ?? null;\n return found?.projectId === projectId ? found : null;\n }\n\n /** Decrypt the token for a stored row. */\n decryptToken(row: ProjectStatusConfig): string {\n return decrypt(this.secret, row.tokenEncrypted);\n }\n\n /** Parse the JSON config column. */\n static parseConfig(row: ProjectStatusConfig): unknown {\n return JSON.parse(row.config) as unknown;\n }\n\n async create(projectId: string, input: StatusConfigCreateInput): Promise<ProjectStatusConfig> {\n const now = new Date().toISOString();\n return await this.db.insert(projectStatusConfigs, {\n id: ulid(),\n projectId,\n provider: input.provider,\n config: JSON.stringify(input.config),\n tokenEncrypted: encrypt(this.secret, input.token),\n createdAt: now,\n updatedAt: now,\n });\n }\n\n async remove(projectId: string, id: string): Promise<void> {\n const existing = await this.get(projectId, id);\n if (existing) {\n await this.db.remove(projectStatusConfigs, existing.id);\n }\n }\n\n async removeByProject(projectId: string): Promise<void> {\n const rows = await this.list(projectId);\n for (const row of rows) {\n await this.db.remove(projectStatusConfigs, row.id);\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAiBA,IAAa,oBAAb,MAA+B;CAEV;CACA;CAFnB,YACE,IACA,QACA;EAFiB,KAAA,KAAA;EACA,KAAA,SAAA;CAChB;;CAGH,MAAM,KAAK,WAAmD;EAC5D,OAAO,MAAM,KAAK,GAAG,KAAK,sBAAsB,EAC9C,OAAO,GAAG,qBAAqB,WAAW,SAAS,EACrD,CAAC;CACH;;CAGA,MAAM,IAAI,WAAmB,IAAiD;EAK5E,MAAM,SAAQ,MAJK,KAAK,GAAG,KAAK,sBAAsB;GACpD,OAAO,GAAG,qBAAqB,IAAI,EAAE;GACrC,OAAO;EACT,CAAC,EAAA,CACkB,MAAM;EACzB,OAAO,OAAO,cAAc,YAAY,QAAQ;CAClD;;CAGA,aAAa,KAAkC;EAC7C,OAAO,QAAQ,KAAK,QAAQ,IAAI,cAAc;CAChD;;CAGA,OAAO,YAAY,KAAmC;EACpD,OAAO,KAAK,MAAM,IAAI,MAAM;CAC9B;CAEA,MAAM,OAAO,WAAmB,OAA8D;EAC5F,MAAM,uBAAM,IAAI,KAAK,EAAA,CAAE,YAAY;EACnC,OAAO,MAAM,KAAK,GAAG,OAAO,sBAAsB;GAChD,IAAI,KAAK;GACT;GACA,UAAU,MAAM;GAChB,QAAQ,KAAK,UAAU,MAAM,MAAM;GACnC,gBAAgB,QAAQ,KAAK,QAAQ,MAAM,KAAK;GAChD,WAAW;GACX,WAAW;EACb,CAAC;CACH;CAEA,MAAM,OAAO,WAAmB,IAA2B;EACzD,MAAM,WAAW,MAAM,KAAK,IAAI,WAAW,EAAE;EAC7C,IAAI,UACF,MAAM,KAAK,GAAG,OAAO,sBAAsB,SAAS,EAAE;CAE1D;CAEA,MAAM,gBAAgB,WAAkC;EACtD,MAAM,OAAO,MAAM,KAAK,KAAK,SAAS;EACtC,KAAK,MAAM,OAAO,MAChB,MAAM,KAAK,GAAG,OAAO,sBAAsB,IAAI,EAAE;CAErD;AACF"}
|
package/dist/models/token.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { tokens } from "../schema.mjs";
|
|
5
4
|
import { ulid } from "../utils/ulid.mjs";
|
|
5
|
+
import { tokens } from "../schema/token.mjs";
|
|
6
6
|
import { and, eq } from "drizzle-orm";
|
|
7
7
|
//#region src/models/token.ts
|
|
8
|
+
/** CI tokens for project-scoped API access. */
|
|
8
9
|
/** Data operations for CI tokens. */
|
|
9
10
|
var TokenModel = class {
|
|
10
11
|
db;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.mjs","names":[],"sources":["../../src/models/token.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"token.mjs","names":[],"sources":["../../src/models/token.ts"],"sourcesContent":["/** CI tokens for project-scoped API access. */\nimport { and, eq } from \"drizzle-orm\";\nimport type { DatabaseAdapter } from \"../adapters/database.ts\";\nimport { tokens } from \"../schema/token.ts\";\nimport type { Token } from \"../schema/token.ts\";\nimport { ulid } from \"../utils/ulid.ts\";\n\n/** Data operations for CI tokens. */\nexport class TokenModel {\n /**\n * @param db - Database adapter.\n */\n constructor(private readonly db: DatabaseAdapter) {}\n\n /**\n * Create a token record storing its hash.\n *\n * @param projectId - Project ID.\n * @param name - Token name.\n * @param hash - Hashed token value.\n * @returns The created token.\n */\n async create(projectId: string, name: string, hash: string): Promise<Token> {\n return await this.db.insert(tokens, {\n id: ulid(),\n projectId,\n name,\n hash,\n createdAt: new Date().toISOString(),\n });\n }\n\n /** List all tokens for a project. */\n async list(projectId: string): Promise<Token[]> {\n return await this.db.list(tokens, { where: eq(tokens.projectId, projectId) });\n }\n\n /** Fetch a token by id within a project, or null if not found. */\n async get(projectId: string, id: string): Promise<Token | null> {\n const rows = await this.db.list(tokens, {\n where: and(eq(tokens.projectId, projectId), eq(tokens.id, id)),\n limit: 1,\n });\n return rows[0] ?? null;\n }\n\n /** Fetch a token by its hashed value, or null if not found. */\n async findByHash(hash: string): Promise<Token | null> {\n const rows = await this.db.list(tokens, { where: eq(tokens.hash, hash), limit: 1 });\n return rows[0] ?? null;\n }\n\n /** Delete a token by id. */\n async remove(id: string): Promise<void> {\n await this.db.remove(tokens, id);\n }\n}\n"],"mappings":";;;;;;;;;AAQA,IAAa,aAAb,MAAwB;CAIO;;;;CAA7B,YAAY,IAAsC;EAArB,KAAA,KAAA;CAAsB;;;;;;;;;CAUnD,MAAM,OAAO,WAAmB,MAAc,MAA8B;EAC1E,OAAO,MAAM,KAAK,GAAG,OAAO,QAAQ;GAClC,IAAI,KAAK;GACT;GACA;GACA;GACA,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACpC,CAAC;CACH;;CAGA,MAAM,KAAK,WAAqC;EAC9C,OAAO,MAAM,KAAK,GAAG,KAAK,QAAQ,EAAE,OAAO,GAAG,OAAO,WAAW,SAAS,EAAE,CAAC;CAC9E;;CAGA,MAAM,IAAI,WAAmB,IAAmC;EAK9D,QAAO,MAJY,KAAK,GAAG,KAAK,QAAQ;GACtC,OAAO,IAAI,GAAG,OAAO,WAAW,SAAS,GAAG,GAAG,OAAO,IAAI,EAAE,CAAC;GAC7D,OAAO;EACT,CAAC,EAAA,CACW,MAAM;CACpB;;CAGA,MAAM,WAAW,MAAqC;EAEpD,QAAO,MADY,KAAK,GAAG,KAAK,QAAQ;GAAE,OAAO,GAAG,OAAO,MAAM,IAAI;GAAG,OAAO;EAAE,CAAC,EAAA,CACtE,MAAM;CACpB;;CAGA,MAAM,OAAO,IAA2B;EACtC,MAAM,KAAK,GAAG,OAAO,QAAQ,EAAE;CACjC;AACF"}
|