@storyshelf/core 0.1.0 → 0.1.2

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.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -8
  3. package/dist/adapter/capture-runner.d.mts +4 -0
  4. package/dist/adapter/git-host/helpers.mjs +1 -1
  5. package/dist/adapter/git-host/helpers.mjs.map +1 -1
  6. package/dist/adapter/storage.d.mts +1 -0
  7. package/dist/adapters/webhook-events.mjs.map +1 -1
  8. package/dist/capture/adapter.d.mts +9 -1
  9. package/dist/capture/adapter.mjs +16 -1
  10. package/dist/capture/adapter.mjs.map +1 -1
  11. package/dist/capture/orchestrator.mjs +37 -10
  12. package/dist/capture/orchestrator.mjs.map +1 -1
  13. package/dist/capture/pipeline.d.mts +5 -3
  14. package/dist/capture/pipeline.mjs +18 -6
  15. package/dist/capture/pipeline.mjs.map +1 -1
  16. package/dist/capture/queue.mjs +1 -1
  17. package/dist/capture/queue.mjs.map +1 -1
  18. package/dist/capture/storybook.mjs +11 -3
  19. package/dist/capture/storybook.mjs.map +1 -1
  20. package/dist/config.mjs +2 -2
  21. package/dist/config.mjs.map +1 -1
  22. package/dist/ddl.d.mts +1 -1
  23. package/dist/ddl.mjs +2 -0
  24. package/dist/ddl.mjs.map +1 -1
  25. package/dist/diff/engine.mjs +2 -2
  26. package/dist/diff/engine.mjs.map +1 -1
  27. package/dist/index.d.mts +4 -4
  28. package/dist/index.mjs +4 -2
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/logger.d.mts +3 -3
  31. package/dist/logger.mjs.map +1 -1
  32. package/dist/middleware/csrf.mjs +1 -1
  33. package/dist/middleware/csrf.mjs.map +1 -1
  34. package/dist/models/build.d.mts +16 -1
  35. package/dist/models/build.mjs +19 -1
  36. package/dist/models/build.mjs.map +1 -1
  37. package/dist/models/label.d.mts +6 -0
  38. package/dist/models/label.mjs +11 -0
  39. package/dist/models/label.mjs.map +1 -1
  40. package/dist/models/project.mjs +1 -1
  41. package/dist/models/project.mjs.map +1 -1
  42. package/dist/pages/build-detail.mjs +1 -1
  43. package/dist/pages/build-detail.mjs.map +1 -1
  44. package/dist/pages/build-diff.mjs.map +1 -1
  45. package/dist/pages/compute-jobs.mjs.map +1 -1
  46. package/dist/pages/label-detail.mjs +227 -0
  47. package/dist/pages/label-detail.mjs.map +1 -0
  48. package/dist/pages/project-builds.mjs.map +1 -1
  49. package/dist/pages/project-settings.mjs +32 -42
  50. package/dist/pages/project-settings.mjs.map +1 -1
  51. package/dist/pages/projects.mjs.map +1 -1
  52. package/dist/pages/settings-labels.mjs.map +1 -1
  53. package/dist/pages/settings-members.mjs.map +1 -1
  54. package/dist/pages/settings-tests.mjs +148 -0
  55. package/dist/pages/settings-tests.mjs.map +1 -0
  56. package/dist/pages/settings-tokens.mjs.map +1 -1
  57. package/dist/pages/settings-webhooks.mjs.map +1 -1
  58. package/dist/pages/storybook.mjs +59 -0
  59. package/dist/pages/storybook.mjs.map +1 -0
  60. package/dist/routers/builds.handlers.mjs +5 -3
  61. package/dist/routers/builds.handlers.mjs.map +1 -1
  62. package/dist/routers/builds.mjs +4 -2
  63. package/dist/routers/builds.mjs.map +1 -1
  64. package/dist/routers/helpers.mjs.map +1 -1
  65. package/dist/routers/labels.mjs +33 -1
  66. package/dist/routers/labels.mjs.map +1 -1
  67. package/dist/routers/schemas.mjs +15 -4
  68. package/dist/routers/schemas.mjs.map +1 -1
  69. package/dist/routers/settings.mjs +20 -0
  70. package/dist/routers/settings.mjs.map +1 -1
  71. package/dist/routers/storybook.mjs +112 -0
  72. package/dist/routers/storybook.mjs.map +1 -0
  73. package/dist/routers/ui.mjs +24 -2
  74. package/dist/routers/ui.mjs.map +1 -1
  75. package/dist/schema.d.mts +68 -0
  76. package/dist/schema.mjs +2 -0
  77. package/dist/schema.mjs.map +1 -1
  78. package/dist/types.d.mts +2 -1
  79. package/dist/types.mjs.map +1 -1
  80. package/dist/ui/components.mjs.map +1 -1
  81. package/dist/ui/document.mjs +9 -0
  82. package/dist/ui/document.mjs.map +1 -1
  83. package/dist/utils/ulid.mjs +3 -3
  84. package/dist/utils/ulid.mjs.map +1 -1
  85. package/package.json +76 -67
@@ -1 +1 @@
1
- {"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\n\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { type ProjectRole, BUILD_STATUSES } from \"../types.ts\";\nimport { notFound } from \"./helpers.ts\";\n\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\nexport const buildUploadSchema = z.object({\n gitSha: z.string(),\n gitBranch: z.string(),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n zip: z.instanceof(File).openapi({ type: \"string\", format: \"binary\" }).optional(),\n}).openapi(\"BuildUpload\");\n\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n});\n\nexport async function buildForProject(projectId: string, buildId: string): Promise<import(\"../models/build.ts\").Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\nexport async function snapshotForBuild(build: { id: string }, snapshotId: string): Promise<import(\"../models/snapshot.ts\").Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\nexport async function refreshBuild(buildId: string): Promise<void> {\n const db = getStore().db;\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const unresolved = snapshots.some((s) => s.status === \"new\" || s.status === \"changed\");\n let status: \"reviewing\" | \"rejected\" | \"approved\";\n if (unresolved) {\n status = \"reviewing\";\n } else {\n const rejected = snapshots.some((s) => s.status === \"rejected\");\n status = rejected ? \"rejected\" : \"approved\";\n }\n const build = await new BuildModel(db).get(buildId);\n if (build) {\n await new BuildModel(db).setStatus(buildId, status);\n await emitWebhookEvent(db, build.projectId, `build:${status}`, {\n buildId,\n status,\n snapshotCount: snapshots.length,\n });\n }\n}\n\nexport async function approveSnapshot(snapshotId: string, userId: string): Promise<void> {\n const db = getStore().db;\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, getStore().storage);\n await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);\n await refreshBuild(build.id);\n}\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nexport { asString };"],"mappings":";;;;;;;;;;;;;AAWA,MAAa,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;AAC7F,MAAa,kBAA0C;CAAC;CAAa;CAAY;AAAO;AACxF,MAAa,iBAAyC,CAAC,YAAY,OAAO;AAE1E,MAAa,oBAAoB,EAAE,OAAO;CACxC,QAAQ,EAAE,OAAO;CACjB,WAAW,EAAE,OAAO;CACpB,aAAa,EAAE,OAAO,CAAC,CAAC,SAAS;CACjC,YAAY,EAAE,OAAO,CAAC,CAAC,SAAS;CAChC,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,KAAK,EAAE,WAAW,IAAI,CAAC,CAAC,QAAQ;EAAE,MAAM;EAAU,QAAQ;CAAS,CAAC,CAAC,CAAC,SAAS;AACjF,CAAC,CAAC,CAAC,QAAQ,aAAa;AAExB,MAAa,iBAAiB,EAAE,OAAO;CACrC,QAAQ,EAAE,KAAK,cAAc,CAAC,CAAC,SAAS;CACxC,QAAQ,EAAE,OAAO,CAAC,CAAC,SAAS;AAC9B,CAAC;AAED,eAAsB,gBAAgB,WAAmB,SAA8D;CACrH,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO;CAC7D,IAAI,CAAC,SAAS,MAAM,cAAc,WAChC,SAAS,iBAAiB;CAE5B,OAAO;AACT;AAEA,eAAsB,iBAAiB,OAAuB,YAAuE;CACnI,MAAM,WAAW,MAAM,IAAI,cAAc,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,UAAU;CACtE,IAAI,CAAC,YAAY,SAAS,YAAY,MAAM,IAC1C,SAAS,oBAAoB;CAE/B,OAAO;AACT;AAEA,eAAsB,aAAa,SAAgC;CACjE,MAAM,KAAK,SAAS,CAAC,CAAC;CACtB,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,aAAa,OAAO;CAC7C,MAAM,YAAY,MAAM,IAAI,cAAc,EAAE,CAAC,CAAC,YAAY,OAAO;CACjE,MAAM,aAAa,UAAU,MAAM,MAAM,EAAE,WAAW,SAAS,EAAE,WAAW,SAAS;CACrF,IAAI;CACJ,IAAI,YACF,SAAS;MAGT,SADiB,UAAU,MAAM,MAAM,EAAE,WAAW,UACpC,IAAI,aAAa;CAEnC,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,OAAO;CAClD,IAAI,OAAO;EACT,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,UAAU,SAAS,MAAM;EAClD,MAAM,iBAAiB,IAAI,MAAM,WAAW,SAAS,UAAU;GAC7D;GACA;GACA,eAAe,UAAU;EAC3B,CAAC;CACH;AACF;AAEA,eAAsB,gBAAgB,YAAoB,QAA+B;CACvF,MAAM,KAAK,SAAS,CAAC,CAAC;CACtB,MAAM,YAAY,IAAI,cAAc,EAAE;CACtC,MAAM,WAAW,MAAM,UAAU,IAAI,UAAU;CAC/C,IAAI,CAAC,UACH,SAAS,oBAAoB;CAE/B,MAAM,UAAU,MAAM,IAAI,aAAa,EAAE,CAAC,CAAC,IAAI,SAAS,SAAS;CACjE,MAAM,QAAQ,MAAM,IAAI,WAAW,EAAE,CAAC,CAAC,IAAI,SAAS,OAAO;CAC3D,IAAI,CAAC,WAAW,CAAC,OACf,SAAS,4BAA4B;CAEvC,MAAM,UAAU,OAAO,YAAY,YAAY,MAAM;CAErD,MAAM,IADgB,cAAc,IAAI,SAAS,CAAC,CAAC,OACrC,CAAC,CAAC,OAAO,QAAQ,IAAI,SAAS,SAAS,SAAS,cAAc,MAAM,WAAW,SAAS,IAAI,SAAS,cAAc;CACjI,MAAM,aAAa,MAAM,EAAE;AAC7B;AAEA,SAAS,SAAS,OAAsD;CACtE,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAA;AAC7C"}
1
+ {"version":3,"file":"builds.handlers.mjs","names":[],"sources":["../../src/routers/builds.handlers.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\n\nimport { BaselineModel } from \"../models/baseline.ts\";\nimport { BuildModel } from \"../models/build.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { SnapshotModel } from \"../models/snapshot.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { type ProjectRole, BUILD_STATUSES } from \"../types.ts\";\nimport { notFound } from \"./helpers.ts\";\n\nexport const VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nexport const DEVELOPER_ROLES: readonly ProjectRole[] = [\"developer\", \"approver\", \"admin\"];\nexport const APPROVER_ROLES: readonly ProjectRole[] = [\"approver\", \"admin\"];\n\nexport const buildUploadSchema = z.object({\n gitSha: z.string(),\n gitBranch: z.string(),\n authorEmail: z.string().optional(),\n authorName: z.string().optional(),\n message: z.string().optional(),\n zip: z.instanceof(File).openapi({ type: \"string\", format: \"binary\" }).optional(),\n}).openapi(\"BuildUpload\");\n\nexport const buildListQuery = z.object({\n status: z.enum(BUILD_STATUSES).optional(),\n branch: z.string().optional(),\n labelKey: z.string().optional(),\n labelValue: z.string().optional(),\n});\n\nexport async function buildForProject(projectId: string, buildId: string): Promise<import(\"../models/build.ts\").Build> {\n const build = await new BuildModel(getStore().db).get(buildId);\n if (!build || build.projectId !== projectId) {\n notFound(\"Build not found\");\n }\n return build;\n}\n\nexport async function snapshotForBuild(build: { id: string }, snapshotId: string): Promise<import(\"../models/snapshot.ts\").Snapshot> {\n const snapshot = await new SnapshotModel(getStore().db).get(snapshotId);\n if (!snapshot || snapshot.buildId !== build.id) {\n notFound(\"Snapshot not found\");\n }\n return snapshot;\n}\n\nexport async function refreshBuild(buildId: string): Promise<void> {\n const {db} = getStore();\n await new BuildModel(db).updateCounts(buildId);\n const snapshots = await new SnapshotModel(db).listByBuild(buildId);\n const unresolved = snapshots.some((s) => s.status === \"new\" || s.status === \"changed\");\n let status: \"reviewing\" | \"rejected\" | \"approved\";\n if (unresolved) {\n status = \"reviewing\";\n } else {\n const rejected = snapshots.some((s) => s.status === \"rejected\");\n status = rejected ? \"rejected\" : \"approved\";\n }\n const build = await new BuildModel(db).get(buildId);\n if (build) {\n await new BuildModel(db).setStatus(buildId, status);\n await emitWebhookEvent(db, build.projectId, `build:${status}`, {\n buildId,\n status,\n snapshotCount: snapshots.length,\n });\n }\n}\n\nexport async function approveSnapshot(snapshotId: string, userId: string): Promise<void> {\n const {db} = getStore();\n const snapshots = new SnapshotModel(db);\n const snapshot = await snapshots.get(snapshotId);\n if (!snapshot) {\n notFound(\"Snapshot not found\");\n }\n const project = await new ProjectModel(db).get(snapshot.projectId);\n const build = await new BuildModel(db).get(snapshot.buildId);\n if (!project || !build) {\n notFound(\"Project or build not found\");\n }\n await snapshots.review(snapshotId, \"approved\", userId);\n const baselines = new BaselineModel(db, getStore().storage);\n await baselines.upsert(project.id, snapshot.storyId, snapshot.viewportName, build.gitBranch, snapshot.id, snapshot.screenshotPath);\n await refreshBuild(build.id);\n}\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nexport { asString };"],"mappings":"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"}
@@ -92,10 +92,12 @@ const deleteBuildRoute = createRoute({
92
92
  function registerBuilds(app) {
93
93
  app.openapi(listBuildsRoute, async (c) => {
94
94
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
95
- const { status, branch } = c.req.valid("query");
95
+ const { status, branch, labelKey, labelValue } = c.req.valid("query");
96
96
  const builds = new BuildModel(getStore().db).list(project.id, {
97
97
  status,
98
- branch: branch ?? void 0
98
+ branch: branch ?? void 0,
99
+ labelKey: labelKey ?? void 0,
100
+ labelValue: labelValue ?? void 0
99
101
  });
100
102
  return c.json(await builds);
101
103
  });
@@ -1 +1 @@
1
- {"version":3,"file":"builds.mjs","names":["notFoundResponse","forbiddenResponse"],"sources":["../../src/routers/builds.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { HTTPException } from \"hono/http-exception\";\n\nimport { BuildModel } from \"../models/build.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { storybookZipPath } from \"../utils/paths.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { badRequest, buildSchema, forbidden as forbiddenResponse, notFound as notFoundResponse, unauthorized } from \"./schemas.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, APPROVER_ROLES, buildUploadSchema, buildListQuery, buildForProject, asString } from \"./builds.handlers.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\nimport { registerComments } from \"./comments.ts\";\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema.array() } }, description: \"List builds for a project\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: buildUploadSchema } } },\n },\n responses: {\n 202: { content: { \"application/json\": { schema: buildSchema } }, description: \"Build created and capture queued\" },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema } }, description: \"Fetch a build\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst retryBuildRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: { content: { \"application/json\": { schema: buildSchema } }, description: \"Build reset to pending\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 204: { description: \"Build deleted\" },\n ...notFoundResponse,\n },\n});\n\nexport function registerBuilds(app: ShelfApp): void {\n app.openapi(listBuildsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const { status, branch } = c.req.valid(\"query\");\n const builds = new BuildModel(getStore().db).list(project.id, { status, branch: branch ?? undefined });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...DEVELOPER_ROLES);\n const form = await c.req.formData();\n\n const gitSha = asString(form.get(\"gitSha\")) ?? \"\";\n const gitBranch = asString(form.get(\"gitBranch\")) ?? \"\";\n if (!gitSha || !gitBranch) {\n throw new HTTPException(400, { message: \"gitSha and gitBranch are required\" });\n }\n const authorEmail = asString(form.get(\"authorEmail\"));\n const authorName = asString(form.get(\"authorName\"));\n const message = asString(form.get(\"message\"));\n\n const build = await new BuildModel(getStore().db).create(project.id, {\n gitSha,\n gitBranch,\n isDefault: gitBranch === project.gitDefaultBranch,\n authorEmail,\n authorName,\n message,\n });\n\n await emitWebhookEvent(getStore().db, project.id, \"build:created\", {\n buildId: build.id,\n gitSha,\n gitBranch,\n authorEmail,\n authorName,\n message,\n });\n\n const zip = form.get(\"zip\");\n if (zip && typeof zip !== \"string\") {\n const buffer = Buffer.from(await zip.arrayBuffer());\n await getStore().storage.write(storybookZipPath(project.id, build.id), buffer);\n }\n\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(build, 202);\n });\n\n app.openapi(getBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(build);\n });\n\n app.openapi(retryBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const updated = await new BuildModel(getStore().db).setStatus(build.id, \"pending\");\n return c.json(updated, 202);\n });\n\n app.openapi(deleteBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await new BuildModel(getStore().db).remove(build.id);\n return c.body(null, 204);\n });\n\n registerSnapshots(app);\n registerComments(app);\n}"],"mappings":"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"}
1
+ {"version":3,"file":"builds.mjs","names":["notFoundResponse","forbiddenResponse"],"sources":["../../src/routers/builds.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\nimport { HTTPException } from \"hono/http-exception\";\n\nimport { BuildModel } from \"../models/build.ts\";\nimport { emitWebhookEvent } from \"../adapters/webhook-events.ts\";\nimport { getStore } from \"../store.ts\";\nimport { storybookZipPath } from \"../utils/paths.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { badRequest, buildSchema, forbidden as forbiddenResponse, notFound as notFoundResponse, unauthorized } from \"./schemas.ts\";\nimport { VIEW_ROLES, DEVELOPER_ROLES, APPROVER_ROLES, buildUploadSchema, buildListQuery, buildForProject, asString } from \"./builds.handlers.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\nimport { registerComments } from \"./comments.ts\";\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema.array() } }, description: \"List builds for a project\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: buildUploadSchema } } },\n },\n responses: {\n 202: { content: { \"application/json\": { schema: buildSchema } }, description: \"Build created and capture queued\" },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: buildSchema } }, description: \"Fetch a build\" },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst retryBuildRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: { content: { \"application/json\": { schema: buildSchema } }, description: \"Build reset to pending\" },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 204: { description: \"Build deleted\" },\n ...notFoundResponse,\n },\n});\n\nexport function registerBuilds(app: ShelfApp): void {\n app.openapi(listBuildsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n const { status, branch, labelKey, labelValue } = c.req.valid(\"query\");\n const builds = new BuildModel(getStore().db).list(project.id, { status, branch: branch ?? undefined, labelKey: labelKey ?? undefined, labelValue: labelValue ?? undefined });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...DEVELOPER_ROLES);\n const form = await c.req.formData();\n\n const gitSha = asString(form.get(\"gitSha\")) ?? \"\";\n const gitBranch = asString(form.get(\"gitBranch\")) ?? \"\";\n if (!gitSha || !gitBranch) {\n throw new HTTPException(400, { message: \"gitSha and gitBranch are required\" });\n }\n const authorEmail = asString(form.get(\"authorEmail\"));\n const authorName = asString(form.get(\"authorName\"));\n const message = asString(form.get(\"message\"));\n\n const build = await new BuildModel(getStore().db).create(project.id, {\n gitSha,\n gitBranch,\n isDefault: gitBranch === project.gitDefaultBranch,\n authorEmail,\n authorName,\n message,\n });\n\n await emitWebhookEvent(getStore().db, project.id, \"build:created\", {\n buildId: build.id,\n gitSha,\n gitBranch,\n authorEmail,\n authorName,\n message,\n });\n\n const zip = form.get(\"zip\");\n if (zip && typeof zip !== \"string\") {\n const buffer = Buffer.from(await zip.arrayBuffer());\n await getStore().storage.write(storybookZipPath(project.id, build.id), buffer);\n }\n\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(build, 202);\n });\n\n app.openapi(getBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...VIEW_ROLES);\n const build = await buildForProject(project.id, buildId);\n return c.json(build);\n });\n\n app.openapi(retryBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n const build = await buildForProject(project.id, buildId);\n const updated = await new BuildModel(getStore().db).setStatus(build.id, \"pending\");\n return c.json(updated, 202);\n });\n\n app.openapi(deleteBuildRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...APPROVER_ROLES);\n const build = await buildForProject(project.id, buildId);\n await new BuildModel(getStore().db).remove(build.id);\n return c.body(null, 204);\n });\n\n registerSnapshots(app);\n registerComments(app);\n}"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/routers/helpers.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\nimport type { Context, Next } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { ZodType } from \"zod\";\n\nimport { MemberModel } from \"../models/member.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { projects, type Project } from \"../schema.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { sha256 } from \"../utils/hash.ts\";\n\nexport function json(c: Context, data: unknown, status: ContentfulStatusCode = 200): Response {\n return c.json(data, status);\n}\n\nexport async function validJson<T>(c: Context, schema: ZodType<T>): Promise<T> {\n const result = schema.safeParse(await c.req.json());\n if (!result.success) {\n throw new HTTPException(400, { message: result.error.message });\n }\n return result.data;\n}\n\nexport function unauthorized(): never {\n throw new HTTPException(401, { message: \"Unauthorized\" });\n}\n\nexport function forbidden(): never {\n throw new HTTPException(403, { message: \"Forbidden\" });\n}\n\nexport function notFound(message = \"Not found\"): never {\n throw new HTTPException(404, { message });\n}\n\nexport async function currentProjectRole(projectId: string): Promise<ProjectRole | null> {\n const { db, user } = getStore();\n if (!user) {\n return null;\n }\n return await new MemberModel(db).effectiveRole(user.role, projectId, user.id);\n}\n\nexport function requireRole(...roles: ProjectRole[]) {\n return async (c: Context, next: Next): Promise<void> => {\n const projectId = c.req.param(\"projectId\") ?? c.req.param(\"slug\");\n if (!projectId) {\n forbidden();\n }\n const role = await currentProjectRole(projectId);\n if (!role || !roles.includes(role)) {\n forbidden();\n }\n await next();\n };\n}\n\nexport async function findProjectBySlug(slug: string): Promise<Project | null> {\n const { db } = getStore();\n const rows = await db.list(projects, { where: eq(projects.slug, slug), limit: 1 });\n return rows[0] ?? null;\n}\n\nasync function resolveProjectByToken(c: Context, slug: string): Promise<Project | null> {\n const authHeader = c.req.header(\"authorization\");\n if (authHeader?.startsWith(\"Bearer \")) {\n const token = authHeader.slice(\"Bearer \".length);\n const found = await new TokenModel(getStore().db).findByHash(sha256(token));\n if (!found) {\n unauthorized();\n }\n const project = await new ProjectModel(getStore().db).get(found.projectId);\n if (!project || project.slug !== slug) {\n forbidden();\n }\n return project;\n }\n return null;\n}\n\nexport async function resolveProject(c: Context, slug: string): Promise<Project> {\n const project = await resolveProjectByToken(c, slug);\n if (project) return project;\n const found = await findProjectBySlug(slug);\n if (!found) {\n notFound(\"Project not found\");\n }\n return found;\n}\n\n/**\n * Enforce the caller's project role for a given project.\n *\n * Authorization model (ADR 0008):\n * - No auth adapter configured -> all operations permitted (development mode).\n * - Bearer token (CLI) -> grants access to its own project regardless of role.\n * - Session user -> must have an effective role in `minRoles`.\n */\nexport async function assertRole(projectId: string, ...minRoles: ProjectRole[]): Promise<void> {\n if (!getStore().authEnabled) {\n return;\n }\n const role = await currentProjectRole(projectId);\n if (!role || !minRoles.includes(role)) {\n forbidden();\n }\n}\n\nexport async function resolveAuthorizedProject(c: Context, slug: string, ...minRoles: ProjectRole[]): Promise<Project> {\n const project = await resolveProjectByToken(c, slug);\n if (project) return project;\n const found = await findProjectBySlug(slug);\n if (!found) {\n notFound(\"Project not found\");\n }\n await assertRole(found.id, ...minRoles);\n return found;\n}\n\n/** Require a site-level admin (or permit when auth is disabled). */\nexport function requireSiteAdmin(): void {\n if (!getStore().authEnabled) {\n return;\n }\n const { user } = getStore();\n if (user?.role !== \"admin\") {\n forbidden();\n }\n}\n\n/** Require the current session user to hold one of the given project roles. */\nexport async function requireProjectRole(projectId: string, ...minRoles: ProjectRole[]): Promise<void> {\n await assertRole(projectId, ...minRoles);\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,eAAsB;CACpC,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,eAAe,CAAC;AAC1D;AAEA,SAAgB,YAAmB;CACjC,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,YAAY,CAAC;AACvD;AAEA,SAAgB,SAAS,UAAU,aAAoB;CACrD,MAAM,IAAI,cAAc,KAAK,EAAE,QAAQ,CAAC;AAC1C;AAEA,eAAsB,mBAAmB,WAAgD;CACvF,MAAM,EAAE,IAAI,SAAS,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO;CAET,OAAO,MAAM,IAAI,YAAY,EAAE,CAAC,CAAC,cAAc,KAAK,MAAM,WAAW,KAAK,EAAE;AAC9E;AAgBA,eAAsB,kBAAkB,MAAuC;CAC7E,MAAM,EAAE,OAAO,SAAS;CAExB,QAAO,MADY,GAAG,KAAK,UAAU;EAAE,OAAO,GAAG,SAAS,MAAM,IAAI;EAAG,OAAO;CAAE,CAAC,EAAA,CACrE,MAAM;AACpB;AAEA,eAAe,sBAAsB,GAAY,MAAuC;CACtF,MAAM,aAAa,EAAE,IAAI,OAAO,eAAe;CAC/C,IAAI,YAAY,WAAW,SAAS,GAAG;EACrC,MAAM,QAAQ,WAAW,MAAM,CAAgB;EAC/C,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,OAAO,KAAK,CAAC;EAC1E,IAAI,CAAC,OACH,aAAa;EAEf,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,SAAS;EACzE,IAAI,CAAC,WAAW,QAAQ,SAAS,MAC/B,UAAU;EAEZ,OAAO;CACT;CACA,OAAO;AACT;;;;;;;;;AAoBA,eAAsB,WAAW,WAAmB,GAAG,UAAwC;CAC7F,IAAI,CAAC,SAAS,CAAC,CAAC,aACd;CAEF,MAAM,OAAO,MAAM,mBAAmB,SAAS;CAC/C,IAAI,CAAC,QAAQ,CAAC,SAAS,SAAS,IAAI,GAClC,UAAU;AAEd;AAEA,eAAsB,yBAAyB,GAAY,MAAc,GAAG,UAA2C;CACrH,MAAM,UAAU,MAAM,sBAAsB,GAAG,IAAI;CACnD,IAAI,SAAS,OAAO;CACpB,MAAM,QAAQ,MAAM,kBAAkB,IAAI;CAC1C,IAAI,CAAC,OACH,SAAS,mBAAmB;CAE9B,MAAM,WAAW,MAAM,IAAI,GAAG,QAAQ;CACtC,OAAO;AACT;;AAGA,SAAgB,mBAAyB;CACvC,IAAI,CAAC,SAAS,CAAC,CAAC,aACd;CAEF,MAAM,EAAE,SAAS,SAAS;CAC1B,IAAI,MAAM,SAAS,SACjB,UAAU;AAEd"}
1
+ {"version":3,"file":"helpers.mjs","names":[],"sources":["../../src/routers/helpers.ts"],"sourcesContent":["import { eq } from \"drizzle-orm\";\nimport type { Context, Next } from \"hono\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ContentfulStatusCode } from \"hono/utils/http-status\";\nimport type { ZodType } from \"zod\";\n\nimport { MemberModel } from \"../models/member.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { projects, type Project } from \"../schema.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { sha256 } from \"../utils/hash.ts\";\n\nexport function json(c: Context, data: unknown, status: ContentfulStatusCode = 200): Response {\n return c.json(data, status);\n}\n\nexport async function validJson<T>(c: Context, schema: ZodType<T>): Promise<T> {\n const result = schema.safeParse(await c.req.json());\n if (!result.success) {\n throw new HTTPException(400, { message: result.error.message });\n }\n return result.data;\n}\n\nexport function unauthorized(): never {\n throw new HTTPException(401, { message: \"Unauthorized\" });\n}\n\nexport function forbidden(): never {\n throw new HTTPException(403, { message: \"Forbidden\" });\n}\n\nexport function notFound(message = \"Not found\"): never {\n throw new HTTPException(404, { message });\n}\n\nexport async function currentProjectRole(projectId: string): Promise<ProjectRole | null> {\n const { db, user } = getStore();\n if (!user) {\n return null;\n }\n return await new MemberModel(db).effectiveRole(user.role, projectId, user.id);\n}\n\nexport function requireRole(...roles: ProjectRole[]) {\n return async (c: Context, next: Next): Promise<void> => {\n const projectId = c.req.param(\"projectId\") ?? c.req.param(\"slug\");\n if (!projectId) {\n forbidden();\n }\n const role = await currentProjectRole(projectId);\n if (!role || !roles.includes(role)) {\n forbidden();\n }\n await next();\n };\n}\n\nexport async function findProjectBySlug(slug: string): Promise<Project | null> {\n const { db } = getStore();\n const rows = await db.list(projects, { where: eq(projects.slug, slug), limit: 1 });\n return rows[0] ?? null;\n}\n\nasync function resolveProjectByToken(c: Context, slug: string): Promise<Project | null> {\n const authHeader = c.req.header(\"authorization\");\n if (authHeader?.startsWith(\"Bearer \")) {\n const token = authHeader.slice(\"Bearer \".length);\n const found = await new TokenModel(getStore().db).findByHash(sha256(token));\n if (!found) {\n unauthorized();\n }\n const project = await new ProjectModel(getStore().db).get(found.projectId);\n if (!project || project.slug !== slug) {\n forbidden();\n }\n return project;\n }\n return null;\n}\n\nexport async function resolveProject(c: Context, slug: string): Promise<Project> {\n const project = await resolveProjectByToken(c, slug);\n if (project) {return project;}\n const found = await findProjectBySlug(slug);\n if (!found) {\n notFound(\"Project not found\");\n }\n return found;\n}\n\n/**\n * Enforce the caller's project role for a given project.\n *\n * Authorization model (ADR 0008):\n * - No auth adapter configured -> all operations permitted (development mode).\n * - Bearer token (CLI) -> grants access to its own project regardless of role.\n * - Session user -> must have an effective role in `minRoles`.\n */\nexport async function assertRole(projectId: string, ...minRoles: ProjectRole[]): Promise<void> {\n if (!getStore().authEnabled) {\n return;\n }\n const role = await currentProjectRole(projectId);\n if (!role || !minRoles.includes(role)) {\n forbidden();\n }\n}\n\nexport async function resolveAuthorizedProject(c: Context, slug: string, ...minRoles: ProjectRole[]): Promise<Project> {\n const project = await resolveProjectByToken(c, slug);\n if (project) {return project;}\n const found = await findProjectBySlug(slug);\n if (!found) {\n notFound(\"Project not found\");\n }\n await assertRole(found.id, ...minRoles);\n return found;\n}\n\n/** Require a site-level admin (or permit when auth is disabled). */\nexport function requireSiteAdmin(): void {\n if (!getStore().authEnabled) {\n return;\n }\n const { user } = getStore();\n if (user?.role !== \"admin\") {\n forbidden();\n }\n}\n\n/** Require the current session user to hold one of the given project roles. */\nexport async function requireProjectRole(projectId: string, ...minRoles: ProjectRole[]): Promise<void> {\n await assertRole(projectId, ...minRoles);\n}\n"],"mappings":";;;;;;;;;;;;AA0BA,SAAgB,eAAsB;CACpC,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,eAAe,CAAC;AAC1D;AAEA,SAAgB,YAAmB;CACjC,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,YAAY,CAAC;AACvD;AAEA,SAAgB,SAAS,UAAU,aAAoB;CACrD,MAAM,IAAI,cAAc,KAAK,EAAE,QAAQ,CAAC;AAC1C;AAEA,eAAsB,mBAAmB,WAAgD;CACvF,MAAM,EAAE,IAAI,SAAS,SAAS;CAC9B,IAAI,CAAC,MACH,OAAO;CAET,OAAO,MAAM,IAAI,YAAY,EAAE,CAAC,CAAC,cAAc,KAAK,MAAM,WAAW,KAAK,EAAE;AAC9E;AAgBA,eAAsB,kBAAkB,MAAuC;CAC7E,MAAM,EAAE,OAAO,SAAS;CAExB,QAAO,MADY,GAAG,KAAK,UAAU;EAAE,OAAO,GAAG,SAAS,MAAM,IAAI;EAAG,OAAO;CAAE,CAAC,EAAA,CACrE,MAAM;AACpB;AAEA,eAAe,sBAAsB,GAAY,MAAuC;CACtF,MAAM,aAAa,EAAE,IAAI,OAAO,eAAe;CAC/C,IAAI,YAAY,WAAW,SAAS,GAAG;EACrC,MAAM,QAAQ,WAAW,MAAM,CAAgB;EAC/C,MAAM,QAAQ,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,OAAO,KAAK,CAAC;EAC1E,IAAI,CAAC,OACH,aAAa;EAEf,MAAM,UAAU,MAAM,IAAI,aAAa,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,SAAS;EACzE,IAAI,CAAC,WAAW,QAAQ,SAAS,MAC/B,UAAU;EAEZ,OAAO;CACT;CACA,OAAO;AACT;;;;;;;;;AAoBA,eAAsB,WAAW,WAAmB,GAAG,UAAwC;CAC7F,IAAI,CAAC,SAAS,CAAC,CAAC,aACd;CAEF,MAAM,OAAO,MAAM,mBAAmB,SAAS;CAC/C,IAAI,CAAC,QAAQ,CAAC,SAAS,SAAS,IAAI,GAClC,UAAU;AAEd;AAEA,eAAsB,yBAAyB,GAAY,MAAc,GAAG,UAA2C;CACrH,MAAM,UAAU,MAAM,sBAAsB,GAAG,IAAI;CACnD,IAAI,SAAU,OAAO;CACrB,MAAM,QAAQ,MAAM,kBAAkB,IAAI;CAC1C,IAAI,CAAC,OACH,SAAS,mBAAmB;CAE9B,MAAM,WAAW,MAAM,IAAI,GAAG,QAAQ;CACtC,OAAO;AACT;;AAGA,SAAgB,mBAAyB;CACvC,IAAI,CAAC,SAAS,CAAC,CAAC,aACd;CAEF,MAAM,EAAE,SAAS,SAAS;CAC1B,IAAI,MAAM,SAAS,SACjB,UAAU;AAEd"}
@@ -4,8 +4,9 @@ import.meta.url;
4
4
  import { LabelModel } from "../models/label.mjs";
5
5
  import { getStore } from "../store.mjs";
6
6
  import { resolveAuthorizedProject } from "./helpers.mjs";
7
- import { labelTypeCreateSchema, labelTypeSchema, notFound, unauthorized } from "./schemas.mjs";
7
+ import { labelTypeCreateSchema, labelTypeSchema, labelTypeUpdateSchema, notFound, unauthorized } from "./schemas.mjs";
8
8
  import { createRoute, z } from "@hono/zod-openapi";
9
+ import { HTTPException } from "hono/http-exception";
9
10
  //#region src/routers/labels.ts
10
11
  const VIEW_ROLES = [
11
12
  "viewer",
@@ -54,6 +55,24 @@ const deleteLabelRoute = createRoute({
54
55
  ...notFound
55
56
  }
56
57
  });
58
+ const updateLabelRoute = createRoute({
59
+ method: "patch",
60
+ path: "/api/v1/projects/{slug}/label-types/{key}",
61
+ request: {
62
+ params: z.object({
63
+ slug: z.string(),
64
+ key: z.string()
65
+ }),
66
+ body: { content: { "application/json": { schema: labelTypeUpdateSchema } } }
67
+ },
68
+ responses: {
69
+ 200: {
70
+ content: { "application/json": { schema: labelTypeSchema } },
71
+ description: "Updated label type"
72
+ },
73
+ ...notFound
74
+ }
75
+ });
57
76
  function registerLabels(app) {
58
77
  app.openapi(listLabelsRoute, async (c) => {
59
78
  const project = await resolveAuthorizedProject(c, c.req.valid("param").slug, ...VIEW_ROLES);
@@ -70,6 +89,19 @@ function registerLabels(app) {
70
89
  await new LabelModel(getStore().db).removeType(project.id, key);
71
90
  return c.body(null, 204);
72
91
  });
92
+ app.openapi(updateLabelRoute, async (c) => {
93
+ const { slug, key } = c.req.valid("param");
94
+ const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);
95
+ const body = c.req.valid("json");
96
+ try {
97
+ const updated = await new LabelModel(getStore().db).updateType(project.id, key, body);
98
+ if (!updated) throw new HTTPException(404, { message: "Label type not found" });
99
+ return c.json(updated);
100
+ } catch (error) {
101
+ if (error instanceof HTTPException) throw error;
102
+ throw new HTTPException(400, { message: error instanceof Error ? error.message : "Cannot update label type" });
103
+ }
104
+ });
73
105
  }
74
106
  //#endregion
75
107
  export { registerLabels };
@@ -1 +1 @@
1
- {"version":3,"file":"labels.mjs","names":[],"sources":["../../src/routers/labels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { LabelModel } from \"../models/label.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { labelTypeCreateSchema, labelTypeSchema, notFound, unauthorized } from \"./schemas.ts\";\n\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listLabelsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/label-types\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: labelTypeSchema.array() } }, description: \"List label types\" },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createLabelRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/label-types\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: labelTypeCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: labelTypeSchema } }, description: \"Created label type\" },\n ...notFound,\n },\n});\n\nconst deleteLabelRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/label-types/{key}\",\n request: { params: z.object({ slug: z.string(), key: z.string() }) },\n responses: {\n 204: { description: \"Deleted label type\" },\n ...notFound,\n },\n});\n\nexport function registerLabels(app: ShelfApp): void {\n app.openapi(listLabelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new LabelModel(getStore().db).listTypes(project.id));\n });\n\n app.openapi(createLabelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new LabelModel(getStore().db).createType(project.id, body), 201);\n });\n\n app.openapi(deleteLabelRoute, async (c) => {\n const { slug, key } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new LabelModel(getStore().db).removeType(project.id, key);\n return c.body(null, 204);\n });\n}"],"mappings":";;;;;;;;;AASA,MAAM,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;AACtF,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gBAAgB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAmB;EAC7G,GAAG;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,sBAAsB,EAAE,EAAE;CAC7E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gBAAgB,EAAE;GAAG,aAAa;EAAqB;EACvG,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,KAAK,EAAE,OAAO;CAAE,CAAC,EAAE;CACnE,WAAW;EACT,KAAK,EAAE,aAAa,qBAAqB;EACzC,GAAG;CACL;AACF,CAAC;AAED,SAAgB,eAAe,KAAqB;CAClD,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,UAAU;EAC1F,OAAO,EAAE,KAAK,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,QAAQ,EAAE,CAAC;CACzE,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,OAAO,EAAE,KAAK,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,IAAI,GAAG,GAAG;CACrF,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,GAAG;EAC9D,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;AACH"}
1
+ {"version":3,"file":"labels.mjs","names":[],"sources":["../../src/routers/labels.ts"],"sourcesContent":["import { createRoute, z } from \"@hono/zod-openapi\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { LabelModel } from \"../models/label.ts\";\nimport { getStore } from \"../store.ts\";\nimport type { ProjectRole } from \"../types.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { labelTypeCreateSchema, labelTypeSchema, labelTypeUpdateSchema, notFound, unauthorized } from \"./schemas.ts\";\n\nconst VIEW_ROLES: readonly ProjectRole[] = [\"viewer\", \"developer\", \"approver\", \"admin\"];\nconst ADMIN_ROLES: readonly ProjectRole[] = [\"admin\"];\n\nconst listLabelsRoute = createRoute({\n method: \"get\",\n path: \"/api/v1/projects/{slug}/label-types\",\n request: { params: z.object({ slug: z.string() }) },\n responses: {\n 200: { content: { \"application/json\": { schema: labelTypeSchema.array() } }, description: \"List label types\" },\n ...notFound,\n ...unauthorized,\n },\n});\n\nconst createLabelRoute = createRoute({\n method: \"post\",\n path: \"/api/v1/projects/{slug}/label-types\",\n request: {\n params: z.object({ slug: z.string() }),\n body: { content: { \"application/json\": { schema: labelTypeCreateSchema } } },\n },\n responses: {\n 201: { content: { \"application/json\": { schema: labelTypeSchema } }, description: \"Created label type\" },\n ...notFound,\n },\n});\n\nconst deleteLabelRoute = createRoute({\n method: \"delete\",\n path: \"/api/v1/projects/{slug}/label-types/{key}\",\n request: { params: z.object({ slug: z.string(), key: z.string() }) },\n responses: {\n 204: { description: \"Deleted label type\" },\n ...notFound,\n },\n});\n\nconst updateLabelRoute = createRoute({\n method: \"patch\",\n path: \"/api/v1/projects/{slug}/label-types/{key}\",\n request: {\n params: z.object({ slug: z.string(), key: z.string() }),\n body: { content: { \"application/json\": { schema: labelTypeUpdateSchema } } },\n },\n responses: {\n 200: { content: { \"application/json\": { schema: labelTypeSchema } }, description: \"Updated label type\" },\n ...notFound,\n },\n});\n\nexport function registerLabels(app: ShelfApp): void {\n app.openapi(listLabelsRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...VIEW_ROLES);\n return c.json(await new LabelModel(getStore().db).listTypes(project.id));\n });\n\n app.openapi(createLabelRoute, async (c) => {\n const project = await resolveAuthorizedProject(c, c.req.valid(\"param\").slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n return c.json(await new LabelModel(getStore().db).createType(project.id, body), 201);\n });\n\n app.openapi(deleteLabelRoute, async (c) => {\n const { slug, key } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n await new LabelModel(getStore().db).removeType(project.id, key);\n return c.body(null, 204);\n });\n\n app.openapi(updateLabelRoute, async (c) => {\n const { slug, key } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...ADMIN_ROLES);\n const body = c.req.valid(\"json\");\n try {\n const updated = await new LabelModel(getStore().db).updateType(project.id, key, body);\n if (!updated) {\n throw new HTTPException(404, { message: \"Label type not found\" });\n }\n return c.json(updated);\n } catch (error) {\n if (error instanceof HTTPException) {\n throw error;\n }\n throw new HTTPException(400, { message: error instanceof Error ? error.message : \"Cannot update label type\" });\n }\n });\n}"],"mappings":";;;;;;;;;;AAUA,MAAM,aAAqC;CAAC;CAAU;CAAa;CAAY;AAAO;AACtF,MAAM,cAAsC,CAAC,OAAO;AAEpD,MAAM,kBAAkB,YAAY;CAClC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,EAAE;CAClD,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gBAAgB,MAAM,EAAE,EAAE;GAAG,aAAa;EAAmB;EAC7G,GAAG;EACH,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;EACrC,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,sBAAsB,EAAE,EAAE;CAC7E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gBAAgB,EAAE;GAAG,aAAa;EAAqB;EACvG,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS,EAAE,QAAQ,EAAE,OAAO;EAAE,MAAM,EAAE,OAAO;EAAG,KAAK,EAAE,OAAO;CAAE,CAAC,EAAE;CACnE,WAAW;EACT,KAAK,EAAE,aAAa,qBAAqB;EACzC,GAAG;CACL;AACF,CAAC;AAED,MAAM,mBAAmB,YAAY;CACnC,QAAQ;CACR,MAAM;CACN,SAAS;EACP,QAAQ,EAAE,OAAO;GAAE,MAAM,EAAE,OAAO;GAAG,KAAK,EAAE,OAAO;EAAE,CAAC;EACtD,MAAM,EAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,sBAAsB,EAAE,EAAE;CAC7E;CACA,WAAW;EACT,KAAK;GAAE,SAAS,EAAE,oBAAoB,EAAE,QAAQ,gBAAgB,EAAE;GAAG,aAAa;EAAqB;EACvG,GAAG;CACL;AACF,CAAC;AAED,SAAgB,eAAe,KAAqB;CAClD,IAAI,QAAQ,iBAAiB,OAAO,MAAM;EACxC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,UAAU;EAC1F,OAAO,EAAE,KAAK,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,QAAQ,EAAE,CAAC;CACzE,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,EAAE,IAAI,MAAM,OAAO,CAAC,CAAC,MAAM,GAAG,WAAW;EAC3F,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,OAAO,EAAE,KAAK,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,IAAI,GAAG,GAAG;CACrF,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,GAAG;EAC9D,OAAO,EAAE,KAAK,MAAM,GAAG;CACzB,CAAC;CAED,IAAI,QAAQ,kBAAkB,OAAO,MAAM;EACzC,MAAM,EAAE,MAAM,QAAQ,EAAE,IAAI,MAAM,OAAO;EACzC,MAAM,UAAU,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW;EACtE,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;EAC/B,IAAI;GACF,MAAM,UAAU,MAAM,IAAI,WAAW,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,QAAQ,IAAI,KAAK,IAAI;GACpF,IAAI,CAAC,SACH,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,uBAAuB,CAAC;GAElE,OAAO,EAAE,KAAK,OAAO;EACvB,SAAS,OAAO;GACd,IAAI,iBAAiB,eACnB,MAAM;GAER,MAAM,IAAI,cAAc,KAAK,EAAE,SAAS,iBAAiB,QAAQ,MAAM,UAAU,2BAA2B,CAAC;EAC/G;CACF,CAAC;AACH"}
@@ -15,6 +15,8 @@ const projectSchema = z.object({
15
15
  maxDiffRatio: z.number(),
16
16
  publicBranchRegex: z.string().nullable(),
17
17
  storybookMeta: z.string().nullable().optional(),
18
+ executePlay: z.boolean(),
19
+ playTimeoutMs: z.number(),
18
20
  createdAt: z.string(),
19
21
  updatedAt: z.string()
20
22
  }).openapi("Project");
@@ -22,7 +24,9 @@ const projectCreateSchema = z.object({
22
24
  name: z.string().min(1),
23
25
  gitRepository: z.string().optional(),
24
26
  gitDefaultBranch: z.string().optional(),
25
- storybookMeta: storybookMetaSchema
27
+ storybookMeta: storybookMetaSchema,
28
+ executePlay: z.boolean().optional(),
29
+ playTimeoutMs: z.number().int().min(1e3).max(3e4).optional()
26
30
  }).openapi("ProjectCreateInput");
27
31
  const projectUpdateSchema = z.object({
28
32
  name: z.string().optional(),
@@ -31,7 +35,9 @@ const projectUpdateSchema = z.object({
31
35
  pixelThreshold: z.number().optional(),
32
36
  maxDiffRatio: z.number().optional(),
33
37
  publicBranchRegex: z.string().nullable().optional(),
34
- storybookMeta: storybookMetaSchema
38
+ storybookMeta: storybookMetaSchema,
39
+ executePlay: z.boolean().optional(),
40
+ playTimeoutMs: z.number().int().min(1e3).max(3e4).optional()
35
41
  }).openapi("ProjectUpdateInput");
36
42
  const buildSchema = z.object({
37
43
  id: z.string(),
@@ -105,6 +111,11 @@ const labelTypeCreateSchema = z.object({
105
111
  linkTemplate: z.string().optional(),
106
112
  color: z.string().optional()
107
113
  }).openapi("LabelTypeCreateInput");
114
+ const labelTypeUpdateSchema = z.object({
115
+ name: z.string().min(1).optional(),
116
+ linkTemplate: z.string().nullable().optional(),
117
+ color: z.string().nullable().optional()
118
+ }).openapi("LabelTypeUpdateInput");
108
119
  const tokenPublicSchema = z.object({
109
120
  id: z.string(),
110
121
  projectId: z.string(),
@@ -135,7 +146,7 @@ const webhookPublicSchema = z.object({
135
146
  events: z.array(z.string())
136
147
  }).openapi("Webhook");
137
148
  const webhookCreateSchema = z.object({
138
- url: z.url(),
149
+ url: z.string().url(),
139
150
  events: z.array(z.string().min(1)).optional()
140
151
  }).openapi("WebhookCreateInput");
141
152
  const webhookCreatedSchema = z.object({
@@ -183,6 +194,6 @@ const notFound = { 404: {
183
194
  description: "Not found"
184
195
  } };
185
196
  //#endregion
186
- export { badRequest, buildSchema, commentCreateSchema, commentSchema, errorSchema, forbidden, labelTypeCreateSchema, labelTypeSchema, memberRoleSchema, memberSchema, memberSetSchema, notFound, okSchema, projectCreateSchema, projectSchema, projectUpdateSchema, purgeInputSchema, purgeSchema, snapshotSchema, statusConfigCreateSchema, statusConfigSchema, tokenCreateSchema, tokenCreatedSchema, tokenPublicSchema, unauthorized, webhookCreateSchema, webhookCreatedSchema, webhookPublicSchema };
197
+ export { badRequest, buildSchema, commentCreateSchema, commentSchema, errorSchema, forbidden, labelTypeCreateSchema, labelTypeSchema, labelTypeUpdateSchema, memberRoleSchema, memberSchema, memberSetSchema, notFound, okSchema, projectCreateSchema, projectSchema, projectUpdateSchema, purgeInputSchema, purgeSchema, snapshotSchema, statusConfigCreateSchema, statusConfigSchema, tokenCreateSchema, tokenCreatedSchema, tokenPublicSchema, unauthorized, webhookCreateSchema, webhookCreatedSchema, webhookPublicSchema };
187
198
 
188
199
  //# sourceMappingURL=schemas.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"schemas.mjs","names":[],"sources":["../../src/routers/schemas.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\n\nimport { BUILD_STATUSES, PROJECT_ROLES, SNAPSHOT_STATUSES } from \"../types.ts\";\n\nconst storybookMetaSchema = z.record(z.string(), z.unknown()).nullable().optional();\n\nexport const projectSchema = z.object({\n id: z.string(),\n name: z.string(),\n slug: z.string(),\n gitRepository: z.string().nullable(),\n gitDefaultBranch: z.string(),\n pixelThreshold: z.number(),\n maxDiffRatio: z.number(),\n publicBranchRegex: z.string().nullable(),\n storybookMeta: z.string().nullable().optional(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"Project\");\n\nexport const projectCreateSchema = z.object({\n name: z.string().min(1),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n storybookMeta: storybookMetaSchema,\n}).openapi(\"ProjectCreateInput\");\n\nexport const projectUpdateSchema = z.object({\n name: z.string().optional(),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n pixelThreshold: z.number().optional(),\n maxDiffRatio: z.number().optional(),\n publicBranchRegex: z.string().nullable().optional(),\n storybookMeta: storybookMetaSchema,\n}).openapi(\"ProjectUpdateInput\");\n\nexport const buildSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n gitSha: z.string(),\n gitBranch: z.string(),\n isDefault: z.boolean(),\n authorEmail: z.string().nullable(),\n authorName: z.string().nullable(),\n message: z.string().nullable(),\n public: z.boolean(),\n status: z.enum(BUILD_STATUSES),\n snapshotCount: z.number(),\n changedCount: z.number(),\n approvedCount: z.number(),\n rejectedCount: z.number(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"Build\");\n\nexport const snapshotSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n storyId: z.string(),\n storyName: z.string(),\n storyTitle: z.string(),\n storyImportPath: z.string().nullable(),\n viewportName: z.string(),\n viewportWidth: z.number(),\n viewportHeight: z.number(),\n screenshotPath: z.string(),\n diffPath: z.string().nullable(),\n diffPixels: z.number().nullable(),\n diffRatio: z.number().nullable(),\n diffPassed: z.boolean().nullable(),\n status: z.enum(SNAPSHOT_STATUSES),\n reviewedBy: z.string().nullable(),\n reviewedAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"Snapshot\");\n\nexport const commentSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n snapshotId: z.string().nullable(),\n userId: z.string(),\n body: z.string(),\n parentId: z.string().nullable(),\n resolved: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"Comment\");\n\nexport const commentCreateSchema = z.object({\n body: z.string().min(1),\n snapshotId: z.string().optional(),\n parentId: z.string().optional(),\n}).openapi(\"CommentCreateInput\");\n\nexport const labelTypeSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n key: z.string(),\n name: z.string(),\n linkTemplate: z.string().nullable(),\n color: z.string().nullable(),\n createdAt: z.string(),\n}).openapi(\"LabelType\");\n\nexport const labelTypeCreateSchema = z.object({\n key: z.string().min(1),\n name: z.string().min(1),\n linkTemplate: z.string().optional(),\n color: z.string().optional(),\n}).openapi(\"LabelTypeCreateInput\");\n\nexport const tokenPublicSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n name: z.string(),\n lastUsedAt: z.string().nullable(),\n createdAt: z.string(),\n}).openapi(\"Token\");\n\nexport const tokenCreateSchema = z.object({ name: z.string().min(1) }).openapi(\"TokenCreateInput\");\n\nexport const tokenCreatedSchema = z.object({\n name: z.string(),\n token: z.string(),\n}).openapi(\"TokenCreated\");\n\nexport const memberSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n userId: z.string(),\n role: z.enum(PROJECT_ROLES),\n createdAt: z.string(),\n}).openapi(\"ProjectMember\");\n\nexport const memberRoleSchema = z.object({ role: z.enum(PROJECT_ROLES) }).openapi(\"MemberRoleInput\");\n\nexport const memberSetSchema = z.object({\n userId: z.string().min(1),\n role: z.enum(PROJECT_ROLES),\n}).openapi(\"MemberSetInput\");\n\nexport const webhookPublicSchema = z.object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n}).openapi(\"Webhook\");\n\nexport const webhookCreateSchema = z.object({\n url: z.url(),\n events: z.array(z.string().min(1)).optional(),\n}).openapi(\"WebhookCreateInput\");\n\nexport const webhookCreatedSchema = z.object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n secret: z.string(),\n}).openapi(\"WebhookCreated\");\n\nexport const statusConfigSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n provider: z.string(),\n config: z.record(z.string(), z.unknown()),\n hasToken: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"StatusConfig\");\n\nexport const statusConfigCreateSchema = z.object({\n provider: z.string().min(1),\n config: z.record(z.string(), z.unknown()),\n token: z.string().min(1),\n}).openapi(\"StatusConfigCreateInput\");\n\nexport const okSchema = z.object({ ok: z.boolean() }).openapi(\"Ok\");\n\nexport const errorSchema = z.object({ message: z.string() }).openapi(\"Error\");\n\nexport const purgeSchema = z.object({ removedBuilds: z.number() }).openapi(\"PurgeResult\");\n\nexport const purgeInputSchema = z.object({ ttlDays: z.number().optional() }).openapi(\"PurgeInput\");\n\n/** Shared response spec for a 400 Bad Request. */\nexport const badRequest = {\n 400: { content: { \"application/json\": { schema: errorSchema } }, description: \"Bad request\" },\n} as const;\n\n/** Shared response spec for a 401 Unauthorized. */\nexport const unauthorized = {\n 401: { content: { \"application/json\": { schema: errorSchema } }, description: \"Unauthorized\" },\n} as const;\n\n/** Shared response spec for a 403 Forbidden. */\nexport const forbidden = {\n 403: { content: { \"application/json\": { schema: errorSchema } }, description: \"Forbidden\" },\n} as const;\n\n/** Shared response spec for a 404 Not Found. */\nexport const notFound = {\n 404: { content: { \"application/json\": { schema: errorSchema } }, description: \"Not found\" },\n} as const;"],"mappings":"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"}
1
+ {"version":3,"file":"schemas.mjs","names":[],"sources":["../../src/routers/schemas.ts"],"sourcesContent":["import { z } from \"@hono/zod-openapi\";\n\nimport { BUILD_STATUSES, PROJECT_ROLES, SNAPSHOT_STATUSES } from \"../types.ts\";\n\nconst storybookMetaSchema = z.record(z.string(), z.unknown()).nullable().optional();\n\nexport const projectSchema = z.object({\n id: z.string(),\n name: z.string(),\n slug: z.string(),\n gitRepository: z.string().nullable(),\n gitDefaultBranch: z.string(),\n pixelThreshold: z.number(),\n maxDiffRatio: z.number(),\n publicBranchRegex: z.string().nullable(),\n storybookMeta: z.string().nullable().optional(),\n executePlay: z.boolean(),\n playTimeoutMs: z.number(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"Project\");\n\nexport const projectCreateSchema = z.object({\n name: z.string().min(1),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n storybookMeta: storybookMetaSchema,\n executePlay: z.boolean().optional(),\n playTimeoutMs: z.number().int().min(1000).max(30_000).optional(),\n}).openapi(\"ProjectCreateInput\");\n\nexport const projectUpdateSchema = z.object({\n name: z.string().optional(),\n gitRepository: z.string().optional(),\n gitDefaultBranch: z.string().optional(),\n pixelThreshold: z.number().optional(),\n maxDiffRatio: z.number().optional(),\n publicBranchRegex: z.string().nullable().optional(),\n storybookMeta: storybookMetaSchema,\n executePlay: z.boolean().optional(),\n playTimeoutMs: z.number().int().min(1000).max(30_000).optional(),\n}).openapi(\"ProjectUpdateInput\");\n\nexport const buildSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n gitSha: z.string(),\n gitBranch: z.string(),\n isDefault: z.boolean(),\n authorEmail: z.string().nullable(),\n authorName: z.string().nullable(),\n message: z.string().nullable(),\n public: z.boolean(),\n status: z.enum(BUILD_STATUSES),\n snapshotCount: z.number(),\n changedCount: z.number(),\n approvedCount: z.number(),\n rejectedCount: z.number(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"Build\");\n\nexport const snapshotSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n storyId: z.string(),\n storyName: z.string(),\n storyTitle: z.string(),\n storyImportPath: z.string().nullable(),\n viewportName: z.string(),\n viewportWidth: z.number(),\n viewportHeight: z.number(),\n screenshotPath: z.string(),\n diffPath: z.string().nullable(),\n diffPixels: z.number().nullable(),\n diffRatio: z.number().nullable(),\n diffPassed: z.boolean().nullable(),\n status: z.enum(SNAPSHOT_STATUSES),\n reviewedBy: z.string().nullable(),\n reviewedAt: z.string().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"Snapshot\");\n\nexport const commentSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n buildId: z.string(),\n snapshotId: z.string().nullable(),\n userId: z.string(),\n body: z.string(),\n parentId: z.string().nullable(),\n resolved: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"Comment\");\n\nexport const commentCreateSchema = z.object({\n body: z.string().min(1),\n snapshotId: z.string().optional(),\n parentId: z.string().optional(),\n}).openapi(\"CommentCreateInput\");\n\nexport const labelTypeSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n key: z.string(),\n name: z.string(),\n linkTemplate: z.string().nullable(),\n color: z.string().nullable(),\n createdAt: z.string(),\n}).openapi(\"LabelType\");\n\nexport const labelTypeCreateSchema = z.object({\n key: z.string().min(1),\n name: z.string().min(1),\n linkTemplate: z.string().optional(),\n color: z.string().optional(),\n}).openapi(\"LabelTypeCreateInput\");\n\nexport const labelTypeUpdateSchema = z.object({\n name: z.string().min(1).optional(),\n linkTemplate: z.string().nullable().optional(),\n color: z.string().nullable().optional(),\n}).openapi(\"LabelTypeUpdateInput\");\n\nexport const tokenPublicSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n name: z.string(),\n lastUsedAt: z.string().nullable(),\n createdAt: z.string(),\n}).openapi(\"Token\");\n\nexport const tokenCreateSchema = z.object({ name: z.string().min(1) }).openapi(\"TokenCreateInput\");\n\nexport const tokenCreatedSchema = z.object({\n name: z.string(),\n token: z.string(),\n}).openapi(\"TokenCreated\");\n\nexport const memberSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n userId: z.string(),\n role: z.enum(PROJECT_ROLES),\n createdAt: z.string(),\n}).openapi(\"ProjectMember\");\n\nexport const memberRoleSchema = z.object({ role: z.enum(PROJECT_ROLES) }).openapi(\"MemberRoleInput\");\n\nexport const memberSetSchema = z.object({\n userId: z.string().min(1),\n role: z.enum(PROJECT_ROLES),\n}).openapi(\"MemberSetInput\");\n\nexport const webhookPublicSchema = z.object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n}).openapi(\"Webhook\");\n\nexport const webhookCreateSchema = z.object({\n // oxlint-disable-next-line typescript/no-deprecated -- z.string().url() kept for zod v3 API compat\n url: z.string().url(),\n events: z.array(z.string().min(1)).optional(),\n}).openapi(\"WebhookCreateInput\");\n\nexport const webhookCreatedSchema = z.object({\n id: z.string(),\n url: z.string(),\n events: z.array(z.string()),\n secret: z.string(),\n}).openapi(\"WebhookCreated\");\n\nexport const statusConfigSchema = z.object({\n id: z.string(),\n projectId: z.string(),\n provider: z.string(),\n config: z.record(z.string(), z.unknown()),\n hasToken: z.boolean(),\n createdAt: z.string(),\n updatedAt: z.string(),\n}).openapi(\"StatusConfig\");\n\nexport const statusConfigCreateSchema = z.object({\n provider: z.string().min(1),\n config: z.record(z.string(), z.unknown()),\n token: z.string().min(1),\n}).openapi(\"StatusConfigCreateInput\");\n\nexport const okSchema = z.object({ ok: z.boolean() }).openapi(\"Ok\");\n\nexport const errorSchema = z.object({ message: z.string() }).openapi(\"Error\");\n\nexport const purgeSchema = z.object({ removedBuilds: z.number() }).openapi(\"PurgeResult\");\n\nexport const purgeInputSchema = z.object({ ttlDays: z.number().optional() }).openapi(\"PurgeInput\");\n\n/** Shared response spec for a 400 Bad Request. */\nexport const badRequest = {\n 400: { content: { \"application/json\": { schema: errorSchema } }, description: \"Bad request\" },\n} as const;\n\n/** Shared response spec for a 401 Unauthorized. */\nexport const unauthorized = {\n 401: { content: { \"application/json\": { schema: errorSchema } }, description: \"Unauthorized\" },\n} as const;\n\n/** Shared response spec for a 403 Forbidden. */\nexport const forbidden = {\n 403: { content: { \"application/json\": { schema: errorSchema } }, description: \"Forbidden\" },\n} as const;\n\n/** Shared response spec for a 404 Not Found. */\nexport const notFound = {\n 404: { content: { \"application/json\": { schema: errorSchema } }, description: \"Not found\" },\n} as const;"],"mappings":"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"}
@@ -69,6 +69,7 @@ async function findProject(slug) {
69
69
  }
70
70
  function registerSettingsPages(app) {
71
71
  app.get("/projects/:slug/settings", async (c) => c.html(await settingsPage(c, "general")));
72
+ app.get("/projects/:slug/settings/tests", async (c) => c.html(await settingsPage(c, "tests")));
72
73
  app.get("/projects/:slug/settings/labels", async (c) => c.html(await settingsPage(c, "labels")));
73
74
  app.get("/projects/:slug/settings/tokens", async (c) => c.html(await settingsPage(c, "tokens")));
74
75
  app.get("/projects/:slug/settings/webhooks", async (c) => c.html(await settingsPage(c, "webhooks")));
@@ -100,6 +101,25 @@ function registerSettingsPages(app) {
100
101
  return c.html(await renderSettingsPage(c, "general", { globalError: message }) ?? "", 400);
101
102
  }
102
103
  });
104
+ app.post("/projects/:slug/settings/tests", async (c) => {
105
+ const slug = c.req.param("slug");
106
+ const project = await findProject(slug);
107
+ const form = await c.req.formData();
108
+ const executePlay = form.get("executePlay") === "true";
109
+ const playTimeoutMsRaw = asString(form.get("playTimeoutMs"));
110
+ const playTimeoutMs = playTimeoutMsRaw ? Number(playTimeoutMsRaw) : void 0;
111
+ if (playTimeoutMs !== void 0 && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1e3 || playTimeoutMs > 3e4)) return c.html(await renderSettingsPage(c, "tests", { globalError: "Play timeout must be between 1000 and 30000" }) ?? "", 400);
112
+ try {
113
+ await new ProjectModel(getStore().db).update(project.id, {
114
+ executePlay,
115
+ playTimeoutMs: playTimeoutMs ?? void 0
116
+ });
117
+ return hxRedirect(c, `/projects/${slug}/settings/tests`);
118
+ } catch (error) {
119
+ const message = error instanceof Error ? error.message : "Failed to update";
120
+ return c.html(await renderSettingsPage(c, "tests", { globalError: message }) ?? "", 400);
121
+ }
122
+ });
103
123
  app.post("/projects/:slug/delete", async (c) => {
104
124
  const project = await findProject(c.req.param("slug"));
105
125
  await new ProjectModel(getStore().db).remove(project.id);
@@ -1 +1 @@
1
- {"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { LabelModel } from \"../models/label.ts\";\nimport { MemberModel } from \"../models/member.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { renderProjectSettingsPage, type SettingsFormState, type SettingsTab } from \"../pages/project-settings.tsx\";\nimport type { SettingsMember } from \"../pages/settings-members.tsx\";\nimport type { SettingsStatusConfig } from \"../pages/settings-status.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport type { LabelType, Project, Token } from \"../schema.ts\";\nimport { getStore } from \"../store.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\ninterface SettingsData {\n project: Project;\n labelTypes: LabelType[];\n tokens: Array<Omit<Token, \"hash\">>;\n members: SettingsMember[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\nasync function loadSettingsData(slug: string): Promise<SettingsData | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { db, config, user, authEnabled, gitHosts } = getStore();\n const labelTypes = await new LabelModel(db).listTypes(project.id);\n const tokensDb = await new TokenModel(db).list(project.id);\n const tokens = tokensDb.map(({ hash: _hash, ...rest }) => rest);\n const members = await new MemberModel(db).list(project.id);\n const webhooksDb = await new WebhookModel(db).list(project.id);\n const webhooks = webhooksDb.map((webhook) => ({ id: webhook.id, url: webhook.url, events: WebhookModel.eventsOf(webhook) }));\n const statusConfigsDb = await new StatusConfigModel(db, config.secret).list(project.id);\n const statusConfigs = statusConfigsDb.map((row) => ({\n id: row.id,\n provider: row.provider,\n config: JSON.parse(row.config) as Record<string, unknown>,\n hasToken: row.tokenEncrypted.length > 0,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n const isAdmin = !authEnabled || user?.role === \"admin\" || members.some((member) => member.userId === user?.id && member.role === \"admin\");\n return { project, labelTypes, tokens, members, webhooks, statusConfigs, gitHosts, isAdmin };\n}\n\nexport async function renderSettingsPage(c: Context, tab: SettingsTab, formState?: SettingsFormState): Promise<string> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const data = await loadSettingsData(slug);\n if (!data) {\n notFound(\"Project not found\");\n }\n return await renderProjectSettingsPage(\n {\n project: data.project,\n activeTab: tab,\n labelTypes: data.labelTypes,\n tokens: data.tokens,\n members: data.members,\n webhooks: data.webhooks,\n statusConfigs: data.statusConfigs,\n gitHosts: data.gitHosts,\n isAdmin: data.isAdmin,\n },\n formState,\n );\n}\n\nasync function settingsPage(c: Context, tab: SettingsTab): Promise<string> {\n const html = await renderSettingsPage(c, tab);\n if (!html) {\n notFound(\"Project not found\");\n }\n return html;\n}\n\nasync function findProject(slug: string): Promise<Project> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n return project;\n}\n\nexport function registerSettingsPages(app: ShelfApp): void {\n app.get(\"/projects/:slug/settings\", async (c) => c.html(await settingsPage(c, \"general\")));\n app.get(\"/projects/:slug/settings/labels\", async (c) => c.html(await settingsPage(c, \"labels\")));\n app.get(\"/projects/:slug/settings/tokens\", async (c) => c.html(await settingsPage(c, \"tokens\")));\n app.get(\"/projects/:slug/settings/webhooks\", async (c) => c.html(await settingsPage(c, \"webhooks\")));\n app.get(\"/projects/:slug/settings/members\", async (c) => c.html(await settingsPage(c, \"members\")));\n app.get(\"/projects/:slug/settings/status\", async (c) => c.html(await settingsPage(c, \"status\")));\n\n app.post(\"/projects/:slug/settings\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await findProject(slug);\n const form = await c.req.formData();\n const name = form.get(\"name\");\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return c.html((await renderSettingsPage(c, \"general\", { errors: { name: \"Name is required\" } })) ?? \"\", 400);\n }\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n const pixelThreshold = asString(form.get(\"pixelThreshold\"));\n const maxDiffRatio = asString(form.get(\"maxDiffRatio\"));\n const publicBranchRegex = asString(form.get(\"publicBranchRegex\"));\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n name: name.trim(),\n gitRepository: gitRepository ?? undefined,\n gitDefaultBranch: gitDefaultBranch ?? undefined,\n pixelThreshold: pixelThreshold ? Number(pixelThreshold) : undefined,\n maxDiffRatio: maxDiffRatio ? Number(maxDiffRatio) : undefined,\n publicBranchRegex: publicBranchRegex === \"\" ? null : (publicBranchRegex ?? undefined),\n });\n return hxRedirect(c, `/projects/${slug}/settings`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"general\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new ProjectModel(getStore().db).remove(project.id);\n return hxRedirect(c, \"/projects\");\n });\n\n app.post(\"/projects/:slug/settings/labels\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const key = asString(form.get(\"key\"));\n const labelName = asString(form.get(\"labelName\")) ?? asString(form.get(\"name\"));\n const linkTemplate = asString(form.get(\"linkTemplate\"));\n if (!key || !labelName) {\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: \"Key and name are required\" })) ?? \"\", 400);\n }\n try {\n await new LabelModel(getStore().db).createType(project.id, { key, name: labelName, linkTemplate });\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create label\";\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/settings/labels/:key/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n try {\n await new LabelModel(getStore().db).removeType(project.id, c.req.param(\"key\"));\n } catch {\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: \"Cannot delete built-in label\" })) ?? \"\", 400);\n }\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n });\n\n app.post(\"/projects/:slug/settings/tokens\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const tokenName = asString(form.get(\"tokenName\")) ?? asString(form.get(\"name\"));\n if (!tokenName) {\n return c.html((await renderSettingsPage(c, \"tokens\", { globalError: \"Name is required\" })) ?? \"\", 400);\n }\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);\n const html = await renderSettingsPage(c, \"tokens\");\n const inject = `<div class=\"alert alert--success\" role=\"alert\"><strong class=\"alert__title\">Token created</strong><div class=\"alert__body\">Copy now — shown once: <code>${token.value}</code></div></div>`;\n const withToken = html.replace('<nav class=\"tabs\"', `${inject}<nav class=\"tabs\"`);\n return c.html(withToken);\n });\n\n app.post(\"/projects/:slug/settings/tokens/:tokenId/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new TokenModel(getStore().db).remove(c.req.param(\"tokenId\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);\n });\n\n app.post(\"/projects/:slug/settings/webhooks\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const url = asString(form.get(\"url\"));\n if (!url) {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"A valid URL is required\" } })) ?? \"\", 400);\n }\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(url);\n } catch {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"Enter a valid URL\" } })) ?? \"\", 400);\n }\n if (parsedUrl.protocol !== \"https:\" && parsedUrl.protocol !== \"http:\") {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"URL must use http or https\" } })) ?? \"\", 400);\n }\n const eventsRaw = asString(form.get(\"events\"));\n const events = eventsRaw ? eventsRaw.split(\",\").map((event) => event.trim()).filter((event) => event.length > 0) : undefined;\n const webhookModel = new WebhookModel(getStore().db);\n const secret = randomToken(\"whsec_\").value;\n await webhookModel.create(project.id, { url, events, secret });\n return c.html((await renderSettingsPage(c, \"webhooks\", { secret })) ?? \"\", 201);\n });\n\n app.post(\"/projects/:slug/settings/webhooks/:webhookId/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const webhook = await new WebhookModel(getStore().db).get(project.id, c.req.param(\"webhookId\"));\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db).remove(project.id, webhook.id);\n return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);\n });\n\n app.post(\"/projects/:slug/settings/members\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const userId = asString(form.get(\"userId\"));\n const role = asString(form.get(\"role\"));\n if (!userId || !role) {\n return c.html((await renderSettingsPage(c, \"members\", { globalError: \"User and role are required\" })) ?? \"\", 400);\n }\n await new MemberModel(getStore().db).set(project.id, userId, role as never);\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/:userId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new MemberModel(getStore().db).remove(project.id, c.req.param(\"userId\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/status\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const providerKey = asString(form.get(\"provider\"));\n const token = asString(form.get(\"token\"));\n const configRaw = asString(form.get(\"config\")) ?? \"{}\";\n const providers = getStore().gitHosts;\n const provider = providers.find((p) => p.metadata.kind === providerKey);\n if (!provider) {\n return c.html((await renderSettingsPage(c, \"status\", { globalError: \"Unknown git provider\" })) ?? \"\", 400);\n }\n if (!token) {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { token: \"Token is required\" } })) ?? \"\", 400);\n }\n let config: unknown;\n try {\n config = JSON.parse(configRaw);\n } catch {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { config: \"Config must be valid JSON\" } })) ?? \"\", 400);\n }\n const parsed = provider.metadata.schema.safeParse(config);\n if (!parsed.success) {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { config: parsed.error.message } })) ?? \"\", 400);\n }\n const { db, config: shelfConfig } = getStore();\n await new StatusConfigModel(db, shelfConfig.secret).create(project.id, { provider: provider.metadata.kind, config: parsed.data, token });\n return hxRedirect(c, `/projects/${project.slug}/settings/status`);\n });\n\n app.post(\"/projects/:slug/settings/status/:id/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const { db, config: shelfConfig } = getStore();\n await new StatusConfigModel(db, shelfConfig.secret).remove(project.id, c.req.param(\"id\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/status`);\n });\n}\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}"],"mappings":"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"}
1
+ {"version":3,"file":"settings.mjs","names":[],"sources":["../../src/routers/settings.ts"],"sourcesContent":["import type { Context } from \"hono\";\nimport type { GitHostProvider } from \"../adapters/git-host/index.ts\";\nimport type { ShelfApp } from \"../index.tsx\";\n\nimport { LabelModel } from \"../models/label.ts\";\nimport { MemberModel } from \"../models/member.ts\";\nimport { ProjectModel } from \"../models/project.ts\";\nimport { StatusConfigModel } from \"../models/status-config.ts\";\nimport { TokenModel } from \"../models/token.ts\";\nimport { WebhookModel } from \"../models/webhook.ts\";\nimport { renderProjectSettingsPage, type SettingsFormState, type SettingsTab } from \"../pages/project-settings.tsx\";\nimport type { SettingsMember } from \"../pages/settings-members.tsx\";\nimport type { SettingsStatusConfig } from \"../pages/settings-status.tsx\";\nimport type { SettingsWebhook } from \"../pages/settings-webhooks.tsx\";\nimport type { LabelType, Project, Token } from \"../schema.ts\";\nimport { getStore } from \"../store.ts\";\nimport { randomToken } from \"../utils/hash.ts\";\nimport { notFound } from \"./helpers.ts\";\nimport { hxRedirect } from \"./htmx.ts\";\n\ninterface SettingsData {\n project: Project;\n labelTypes: LabelType[];\n tokens: Omit<Token, \"hash\">[];\n members: SettingsMember[];\n webhooks: SettingsWebhook[];\n statusConfigs: SettingsStatusConfig[];\n gitHosts: GitHostProvider[];\n isAdmin: boolean;\n}\n\nasync function loadSettingsData(slug: string): Promise<SettingsData | null> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n return null;\n }\n const { db, config, user, authEnabled, gitHosts } = getStore();\n const labelTypes = await new LabelModel(db).listTypes(project.id);\n const tokensDb = await new TokenModel(db).list(project.id);\n const tokens = tokensDb.map(({ hash: _hash, ...rest }) => rest);\n const members = await new MemberModel(db).list(project.id);\n const webhooksDb = await new WebhookModel(db).list(project.id);\n const webhooks = webhooksDb.map((webhook) => ({ id: webhook.id, url: webhook.url, events: WebhookModel.eventsOf(webhook) }));\n const statusConfigsDb = await new StatusConfigModel(db, config.secret).list(project.id);\n const statusConfigs = statusConfigsDb.map((row) => ({\n id: row.id,\n provider: row.provider,\n config: JSON.parse(row.config) as Record<string, unknown>,\n hasToken: row.tokenEncrypted.length > 0,\n createdAt: row.createdAt,\n updatedAt: row.updatedAt,\n }));\n const isAdmin = !authEnabled || user?.role === \"admin\" || members.some((member) => member.userId === user?.id && member.role === \"admin\");\n return { project, labelTypes, tokens, members, webhooks, statusConfigs, gitHosts, isAdmin };\n}\n\nexport async function renderSettingsPage(c: Context, tab: SettingsTab, formState?: SettingsFormState): Promise<string> {\n const slug = c.req.param(\"slug\") ?? \"\";\n const data = await loadSettingsData(slug);\n if (!data) {\n notFound(\"Project not found\");\n }\n return await renderProjectSettingsPage(\n {\n project: data.project,\n activeTab: tab,\n labelTypes: data.labelTypes,\n tokens: data.tokens,\n members: data.members,\n webhooks: data.webhooks,\n statusConfigs: data.statusConfigs,\n gitHosts: data.gitHosts,\n isAdmin: data.isAdmin,\n },\n formState,\n );\n}\n\nasync function settingsPage(c: Context, tab: SettingsTab): Promise<string> {\n const html = await renderSettingsPage(c, tab);\n if (!html) {\n notFound(\"Project not found\");\n }\n return html;\n}\n\nasync function findProject(slug: string): Promise<Project> {\n const project = await new ProjectModel(getStore().db).getBySlug(slug);\n if (!project) {\n notFound(\"Project not found\");\n }\n return project;\n}\n\nexport function registerSettingsPages(app: ShelfApp): void {\n app.get(\"/projects/:slug/settings\", async (c) => c.html(await settingsPage(c, \"general\")));\n app.get(\"/projects/:slug/settings/tests\", async (c) => c.html(await settingsPage(c, \"tests\")));\n app.get(\"/projects/:slug/settings/labels\", async (c) => c.html(await settingsPage(c, \"labels\")));\n app.get(\"/projects/:slug/settings/tokens\", async (c) => c.html(await settingsPage(c, \"tokens\")));\n app.get(\"/projects/:slug/settings/webhooks\", async (c) => c.html(await settingsPage(c, \"webhooks\")));\n app.get(\"/projects/:slug/settings/members\", async (c) => c.html(await settingsPage(c, \"members\")));\n app.get(\"/projects/:slug/settings/status\", async (c) => c.html(await settingsPage(c, \"status\")));\n\n app.post(\"/projects/:slug/settings\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await findProject(slug);\n const form = await c.req.formData();\n const name = form.get(\"name\");\n if (typeof name !== \"string\" || name.trim() === \"\") {\n return c.html((await renderSettingsPage(c, \"general\", { errors: { name: \"Name is required\" } })) ?? \"\", 400);\n }\n const gitRepository = asString(form.get(\"gitRepository\"));\n const gitDefaultBranch = asString(form.get(\"gitDefaultBranch\"));\n const pixelThreshold = asString(form.get(\"pixelThreshold\"));\n const maxDiffRatio = asString(form.get(\"maxDiffRatio\"));\n const publicBranchRegex = asString(form.get(\"publicBranchRegex\"));\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n name: name.trim(),\n gitRepository: gitRepository ?? undefined,\n gitDefaultBranch: gitDefaultBranch ?? undefined,\n pixelThreshold: pixelThreshold ? Number(pixelThreshold) : undefined,\n maxDiffRatio: maxDiffRatio ? Number(maxDiffRatio) : undefined,\n publicBranchRegex: publicBranchRegex === \"\" ? null : (publicBranchRegex ?? undefined),\n });\n return hxRedirect(c, `/projects/${slug}/settings`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"general\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/settings/tests\", async (c) => {\n const slug = c.req.param(\"slug\");\n const project = await findProject(slug);\n const form = await c.req.formData();\n const executePlay = form.get(\"executePlay\") === \"true\";\n const playTimeoutMsRaw = asString(form.get(\"playTimeoutMs\"));\n const playTimeoutMs = playTimeoutMsRaw ? Number(playTimeoutMsRaw) : undefined;\n if (playTimeoutMs !== undefined && (Number.isNaN(playTimeoutMs) || playTimeoutMs < 1000 || playTimeoutMs > 30_000)) {\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: \"Play timeout must be between 1000 and 30000\" })) ?? \"\", 400);\n }\n try {\n await new ProjectModel(getStore().db).update(project.id, {\n executePlay,\n playTimeoutMs: playTimeoutMs ?? undefined,\n });\n return hxRedirect(c, `/projects/${slug}/settings/tests`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to update\";\n return c.html((await renderSettingsPage(c, \"tests\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new ProjectModel(getStore().db).remove(project.id);\n return hxRedirect(c, \"/projects\");\n });\n\n app.post(\"/projects/:slug/settings/labels\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const key = asString(form.get(\"key\"));\n const labelName = asString(form.get(\"labelName\")) ?? asString(form.get(\"name\"));\n const linkTemplate = asString(form.get(\"linkTemplate\"));\n if (!key || !labelName) {\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: \"Key and name are required\" })) ?? \"\", 400);\n }\n try {\n await new LabelModel(getStore().db).createType(project.id, { key, name: labelName, linkTemplate });\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Failed to create label\";\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: message })) ?? \"\", 400);\n }\n });\n\n app.post(\"/projects/:slug/settings/labels/:key/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n try {\n await new LabelModel(getStore().db).removeType(project.id, c.req.param(\"key\"));\n } catch {\n return c.html((await renderSettingsPage(c, \"labels\", { globalError: \"Cannot delete built-in label\" })) ?? \"\", 400);\n }\n return hxRedirect(c, `/projects/${project.slug}/settings/labels`);\n });\n\n app.post(\"/projects/:slug/settings/tokens\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const tokenName = asString(form.get(\"tokenName\")) ?? asString(form.get(\"name\"));\n if (!tokenName) {\n return c.html((await renderSettingsPage(c, \"tokens\", { globalError: \"Name is required\" })) ?? \"\", 400);\n }\n const token = randomToken(\"shelf_\");\n await new TokenModel(getStore().db).create(project.id, tokenName, token.hash);\n const html = await renderSettingsPage(c, \"tokens\");\n const inject = `<div class=\"alert alert--success\" role=\"alert\"><strong class=\"alert__title\">Token created</strong><div class=\"alert__body\">Copy now — shown once: <code>${token.value}</code></div></div>`;\n const withToken = html.replace('<nav class=\"tabs\"', `${inject}<nav class=\"tabs\"`);\n return c.html(withToken);\n });\n\n app.post(\"/projects/:slug/settings/tokens/:tokenId/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new TokenModel(getStore().db).remove(c.req.param(\"tokenId\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/tokens`);\n });\n\n app.post(\"/projects/:slug/settings/webhooks\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const url = asString(form.get(\"url\"));\n if (!url) {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"A valid URL is required\" } })) ?? \"\", 400);\n }\n let parsedUrl: URL;\n try {\n parsedUrl = new URL(url);\n } catch {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"Enter a valid URL\" } })) ?? \"\", 400);\n }\n if (parsedUrl.protocol !== \"https:\" && parsedUrl.protocol !== \"http:\") {\n return c.html((await renderSettingsPage(c, \"webhooks\", { errors: { url: \"URL must use http or https\" } })) ?? \"\", 400);\n }\n const eventsRaw = asString(form.get(\"events\"));\n const events = eventsRaw ? eventsRaw.split(\",\").map((event) => event.trim()).filter((event) => event.length > 0) : undefined;\n const webhookModel = new WebhookModel(getStore().db);\n const secret = randomToken(\"whsec_\").value;\n await webhookModel.create(project.id, { url, events, secret });\n return c.html((await renderSettingsPage(c, \"webhooks\", { secret })) ?? \"\", 201);\n });\n\n app.post(\"/projects/:slug/settings/webhooks/:webhookId/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const webhook = await new WebhookModel(getStore().db).get(project.id, c.req.param(\"webhookId\"));\n if (!webhook) {\n notFound(\"Webhook not found\");\n }\n await new WebhookModel(getStore().db).remove(project.id, webhook.id);\n return hxRedirect(c, `/projects/${project.slug}/settings/webhooks`);\n });\n\n app.post(\"/projects/:slug/settings/members\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const userId = asString(form.get(\"userId\"));\n const role = asString(form.get(\"role\"));\n if (!userId || !role) {\n return c.html((await renderSettingsPage(c, \"members\", { globalError: \"User and role are required\" })) ?? \"\", 400);\n }\n await new MemberModel(getStore().db).set(project.id, userId, role as never);\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/members/:userId/remove\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n await new MemberModel(getStore().db).remove(project.id, c.req.param(\"userId\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/members`);\n });\n\n app.post(\"/projects/:slug/settings/status\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const form = await c.req.formData();\n const providerKey = asString(form.get(\"provider\"));\n const token = asString(form.get(\"token\"));\n const configRaw = asString(form.get(\"config\")) ?? \"{}\";\n const providers = getStore().gitHosts;\n const provider = providers.find((p) => p.metadata.kind === providerKey);\n if (!provider) {\n return c.html((await renderSettingsPage(c, \"status\", { globalError: \"Unknown git provider\" })) ?? \"\", 400);\n }\n if (!token) {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { token: \"Token is required\" } })) ?? \"\", 400);\n }\n let config: unknown;\n try {\n config = JSON.parse(configRaw);\n } catch {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { config: \"Config must be valid JSON\" } })) ?? \"\", 400);\n }\n const parsed = provider.metadata.schema.safeParse(config);\n if (!parsed.success) {\n return c.html((await renderSettingsPage(c, \"status\", { errors: { config: parsed.error.message } })) ?? \"\", 400);\n }\n const { db, config: shelfConfig } = getStore();\n await new StatusConfigModel(db, shelfConfig.secret).create(project.id, { provider: provider.metadata.kind, config: parsed.data, token });\n return hxRedirect(c, `/projects/${project.slug}/settings/status`);\n });\n\n app.post(\"/projects/:slug/settings/status/:id/delete\", async (c) => {\n const project = await findProject(c.req.param(\"slug\"));\n const { db, config: shelfConfig } = getStore();\n await new StatusConfigModel(db, shelfConfig.secret).remove(project.id, c.req.param(\"id\"));\n return hxRedirect(c, `/projects/${project.slug}/settings/status`);\n });\n}\n\nfunction asString(value: FormDataEntryValue | null): string | undefined {\n return typeof value === \"string\" ? value.trim() : undefined;\n}"],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ import "node:path";
2
+ import "node:url";
3
+ import.meta.url;
4
+ import { BuildModel, isPublicBuild } from "../models/build.mjs";
5
+ import { ProjectModel } from "../models/project.mjs";
6
+ import { LabelModel } from "../models/label.mjs";
7
+ import { getStore } from "../store.mjs";
8
+ import { currentProjectRole, notFound } from "./helpers.mjs";
9
+ import { storybookDir } from "../utils/paths.mjs";
10
+ import { renderStorybookPage } from "../pages/storybook.mjs";
11
+ import { posix } from "node:path";
12
+ //#region src/routers/storybook.ts
13
+ const VIEW_ROLES = /* @__PURE__ */ new Set([
14
+ "viewer",
15
+ "developer",
16
+ "approver",
17
+ "admin"
18
+ ]);
19
+ const MIME = {
20
+ ".html": "text/html; charset=utf-8",
21
+ ".htm": "text/html; charset=utf-8",
22
+ ".js": "application/javascript",
23
+ ".mjs": "application/javascript",
24
+ ".json": "application/json",
25
+ ".css": "text/css; charset=utf-8",
26
+ ".png": "image/png",
27
+ ".jpg": "image/jpeg",
28
+ ".jpeg": "image/jpeg",
29
+ ".gif": "image/gif",
30
+ ".svg": "image/svg+xml",
31
+ ".webp": "image/webp",
32
+ ".ico": "image/x-icon",
33
+ ".woff": "font/woff",
34
+ ".woff2": "font/woff2",
35
+ ".ttf": "font/ttf",
36
+ ".map": "application/json",
37
+ ".wasm": "application/wasm"
38
+ };
39
+ function contentTypeFor(path) {
40
+ const ext = posix.extname(path).toLowerCase();
41
+ return MIME[ext] ?? "application/octet-stream";
42
+ }
43
+ function isSafeSegment(segment) {
44
+ return segment !== ".." && !segment.startsWith("/") && !segment.includes("\\") && !segment.includes("..");
45
+ }
46
+ /**
47
+ * Decide whether the current request may view `build`'s published Storybook.
48
+ *
49
+ * Public builds (ADR 0011) are viewable without auth; every other build requires
50
+ * a logged-in session with at least `viewer` membership on the project.
51
+ */
52
+ async function canViewBuild(build, project) {
53
+ if (isPublicBuild(project, build)) return true;
54
+ const { authEnabled } = getStore();
55
+ if (!authEnabled) return true;
56
+ const role = await currentProjectRole(project.id);
57
+ return Boolean(role && VIEW_ROLES.has(role));
58
+ }
59
+ function registerStorybook(app) {
60
+ app.get("/projects/:slug/storybook", async (c) => {
61
+ const slug = c.req.param("slug");
62
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
63
+ if (!project) notFound("Project not found");
64
+ const build = await new BuildModel(getStore().db).latestPublished(project);
65
+ if (!build) notFound("No published Storybook for this project");
66
+ if (!await canViewBuild(build, project)) return c.redirect("/auth/login", 302);
67
+ return c.redirect(`/projects/${slug}/storybook/build/${build.id}/`, 302);
68
+ });
69
+ app.get("/projects/:slug/storybook/:key/:value", async (c) => {
70
+ const slug = c.req.param("slug");
71
+ const key = c.req.param("key");
72
+ if (key === "build") return c.notFound();
73
+ const value = c.req.param("value");
74
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
75
+ if (!project) notFound("Project not found");
76
+ const buildId = await new LabelModel(getStore().db).latestBuildId(project.id, key, value);
77
+ if (!buildId) notFound("No build carries that label");
78
+ const build = await new BuildModel(getStore().db).get(buildId);
79
+ if (!build || !await canViewBuild(build, project)) return c.redirect("/auth/login", 302);
80
+ return c.redirect(`/projects/${slug}/storybook/build/${buildId}/`, 302);
81
+ });
82
+ app.get("/projects/:slug/storybook/build/:buildId/*", async (c) => {
83
+ const slug = c.req.param("slug");
84
+ const buildId = c.req.param("buildId");
85
+ const base = `/projects/${slug}/storybook/build/${buildId}/`;
86
+ if (!c.req.path.startsWith(base)) return c.notFound();
87
+ let rest;
88
+ try {
89
+ rest = decodeURIComponent(c.req.path.slice(base.length));
90
+ } catch {
91
+ return c.notFound();
92
+ }
93
+ const project = await new ProjectModel(getStore().db).getBySlug(slug);
94
+ if (!project) notFound("Project not found");
95
+ const build = await new BuildModel(getStore().db).get(buildId);
96
+ if (!build || build.projectId !== project.id) notFound("Build not found");
97
+ if (!await canViewBuild(build, project)) return c.redirect("/auth/login", 302);
98
+ if (rest === "") return c.html(renderStorybookPage(project, build, slug), 200);
99
+ if (rest.split("/").some((segment) => !isSafeSegment(segment))) return c.notFound();
100
+ const path = posix.join(storybookDir(project.id, build.id), rest);
101
+ if (!await getStore().storage.exists(path)) return c.notFound();
102
+ const buffer = await getStore().storage.read(path);
103
+ return c.body(new Uint8Array(buffer), 200, {
104
+ "content-type": contentTypeFor(rest),
105
+ "cache-control": "public, max-age=3600"
106
+ });
107
+ });
108
+ }
109
+ //#endregion
110
+ export { registerStorybook };
111
+
112
+ //# sourceMappingURL=storybook.mjs.map