@storyshelf/app 0.10.0-next-2a86736 → 0.10.0-next-e614c0a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs
CHANGED
|
@@ -63,7 +63,7 @@ import { createShelfLogger } from "@storyshelf/core/logger";
|
|
|
63
63
|
*/
|
|
64
64
|
function throwOnInvalidAdapters(options) {
|
|
65
65
|
const invalid = validateBootAssembly(adapterSourcesOf(options));
|
|
66
|
-
if (invalid.length > 0) throw new AdapterLifecycleError("setup", invalid);
|
|
66
|
+
if (invalid.length > 0) throw new AdapterLifecycleError("setup", invalid, true);
|
|
67
67
|
}
|
|
68
68
|
/** Request/capture-path adapters with OTEL wrappers (lifecycle keeps raw ones). */
|
|
69
69
|
function instrumentedOptions(options) {
|
|
@@ -380,7 +380,7 @@ function registerDocs(app) {
|
|
|
380
380
|
* from source (dev server, tests).
|
|
381
381
|
*/
|
|
382
382
|
function packageVersion() {
|
|
383
|
-
return "0.10.0-next-
|
|
383
|
+
return "0.10.0-next-e614c0a";
|
|
384
384
|
}
|
|
385
385
|
export { createAuthSystemHook, createShelfApp, createShelfLogger };
|
|
386
386
|
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["import { swaggerUI } from \"@hono/swagger-ui\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport {\n AdapterLifecycleError,\n adapterSourcesOf,\n validateBootAssembly,\n} from \"@storyshelf/core/adapter/setup\";\nimport { assertServerless, resolveShelfOptions } from \"@storyshelf/core/config\";\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport { configureImages } from \"@storyshelf/core/diff\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { STORYBOOK_TOKEN_PREFIX } from \"@storyshelf/core/utils\";\n/**\n * StoryShelf app: compose database, storage, capture, auth, and notifier\n * adapters into a complete self-hosted visual-testing Hono app.\n */\nimport {\n createHttpMiddleware,\n createInstrumentedDatabase,\n createInstrumentedStorage,\n} from \"@storyshelf/observability\";\nimport { requestId } from \"hono/request-id\";\nimport type { ShelfApp, ShelfContext, ShelfRouter } from \"./app-types.ts\";\nimport { runBootTasks } from \"./boot-tasks.ts\";\nimport { setupJobs, type JobsWiring } from \"./jobs-setup.ts\";\nimport { attachLifecycle, type LifecycleCell } from \"./lifecycle.ts\";\nimport {\n authGate,\n csrf,\n createRateLimiter,\n requireReview,\n requireTesting,\n rateLimitKey,\n requestLogging,\n resolveRequestUser,\n storeScope,\n} from \"./middleware/index.ts\";\nimport { serverTiming } from \"./middleware/server-timing.ts\";\nimport { setupGate, type MiddlewareWiring } from \"./middleware/setup-gate.ts\";\nimport { registerAdminPages } from \"./routers/admin-pages.ts\";\nimport { registerPublicOriginRoutes } from \"./routers/admin-public-origin.ts\";\nimport { registerAdmin } from \"./routers/admin.ts\";\nimport { registerAssets } from \"./routers/assets.ts\";\nimport { registerEngineAuth } from \"./routers/auth.ts\";\nimport { registerBaselines } from \"./routers/baselines.ts\";\nimport { registerBuilds } from \"./routers/builds.ts\";\nimport { registerConnections } from \"./routers/connections.ts\";\nimport { registerHealth, type HealthDeps } from \"./routers/health.ts\";\nimport { registerInsightFragments } from \"./routers/insights-ui.tsx\";\nimport { registerInsights } from \"./routers/insights.ts\";\nimport { registerLabels } from \"./routers/labels.ts\";\nimport { registerLinks } from \"./routers/links.ts\";\nimport { registerMedia } from \"./routers/media.ts\";\nimport { registerMembers } from \"./routers/members.ts\";\nimport {\n registerNotificationChannels,\n registerSystemChannels,\n} from \"./routers/notification-channels.ts\";\nimport { registerNotificationSubscriptions } from \"./routers/notification-subscriptions.ts\";\nimport { registerPreviewToken } from \"./routers/preview-token.ts\";\nimport { registerProfile } from \"./routers/profile.ts\";\nimport { registerProjects } from \"./routers/projects.ts\";\nimport { registerReviewForms } from \"./routers/review-forms.ts\";\nimport { registerSignedStorybook } from \"./routers/signed-storybook.ts\";\nimport { registerStorybook } from \"./routers/storybook.ts\";\nimport { registerTokens } from \"./routers/tokens.ts\";\nimport { registerUiPages } from \"./routers/ui.ts\";\nimport { registerWebhooks } from \"./routers/webhooks.ts\";\nimport { registerWellKnown } from \"./routers/well-known.ts\";\nimport { markRouteClasses } from \"./routes/openapi-classes.ts\";\nimport { resolveRuntime, type ServerRuntime } from \"./runtime.ts\";\n\n/**\n * Fail fast on third-party adapters that cannot satisfy their slot.\n * Throws before any route or lifecycle hook runs.\n */\nfunction throwOnInvalidAdapters(options: ShelfOptions): void {\n const invalid = validateBootAssembly(adapterSourcesOf(options));\n if (invalid.length > 0) {\n throw new AdapterLifecycleError(\"setup\", invalid);\n }\n}\n\n/** Request/capture-path adapters with OTEL wrappers (lifecycle keeps raw ones). */\nfunction instrumentedOptions(options: ShelfOptions): ShelfOptions {\n return {\n ...options,\n database: createInstrumentedDatabase(options.database),\n storage: createInstrumentedStorage(options.storage),\n };\n}\n\n/** Wire the job queue, its handlers and its consumer, and run the boot tasks that need them. */\nfunction wireQueues(scoped: ShelfOptions, runtime: ServerRuntime, cell: LifecycleCell): JobsWiring {\n const jobs = setupJobs(scoped, runtime);\n cell.consumer = jobs.consumer;\n runBootTasks(cell, scoped, runtime, jobs.buildState);\n return jobs;\n}\n\n/**\n * Serverless check (ADR 0006, decision 3): throws on AWS Lambda, warns on Vercel and Netlify. Only\n * an instance with `testing` needs a process that outlives the request.\n */\nfunction warnOnServerless(options: ShelfOptions, logger: Logger): void {\n const warning = assertServerless(options.testing?.runner !== undefined);\n if (warning) {\n logger.warn(warning);\n }\n}\n\n/** Attach lifecycle, queue, middleware, routes, and docs to the app. */\nfunction assembleApp(app: ShelfApp, options: ShelfOptions, scoped: ShelfOptions): void {\n const runtime = resolveRuntime(options);\n warnOnServerless(options, runtime.logger);\n const cell: LifecycleCell = { ready: Promise.resolve({ ok: true, failures: [] }), settled: null };\n attachLifecycle(app, options, runtime, cell);\n if (runtime.config.serverTiming === true) {\n app.use(\"*\", serverTiming(true));\n }\n const queues = wireQueues(scoped, runtime, cell);\n wireMiddleware(app, {\n ...runtime,\n ...queues,\n options: scoped,\n getReady: async () => await cell.ready,\n });\n registerAllRoutes(app, options, {\n sources: adapterSourcesOf(options),\n getSettled: () => cell.settled,\n bootTimeMs: Date.now(),\n version: packageVersion(),\n });\n registerDocs(app);\n}\n\n/**\n * Create the StoryShelf Hono app with all API routes and HTML pages.\n *\n * Adapter `init` hooks kick off eagerly in the background (the constructor\n * stays synchronous). Await `app.lifecycle.setup()` before serving for\n * fail-fast startup, or let the first request gate on settlement.\n */\nexport function createShelfApp(rawOptions: ShelfOptions): ShelfApp {\n const options = resolveShelfOptions(rawOptions);\n configureImages({\n engine: options.testing?.images?.engine,\n threads: options.testing?.compare?.threads,\n });\n throwOnInvalidAdapters(options);\n const app = new OpenAPIHono<{ Variables: ShelfContext }>() as ShelfApp;\n assembleApp(app, options, instrumentedOptions(options));\n\n return app;\n}\n\n/** Cookie-authenticated write paths guarded by CSRF tokens. */\nconst CSRF_PATHS = [\n \"/auth/logout\",\n \"/auth/invites/*\",\n \"/projects/:slug/settings/*\",\n \"/profile/*\",\n \"/connections/*\",\n \"/profile\",\n \"/admin/credentials/*\",\n \"/admin/public-origin\",\n \"/admin/public-origin/*\",\n \"/projects/:slug/builds/:buildId/insights/generate\",\n \"/projects/:slug/builds/:buildId/comments\",\n \"/projects/:slug/builds/:buildId/comments/*\",\n \"/projects/:slug/builds/:buildId/reviews\",\n \"/projects/:slug/insights/health/generate\",\n] as const;\n\n/** Attach global middleware: ids, logging, init gate, limits, store scope, auth gate. */\nfunction wireMiddleware(app: ShelfRouter, wiring: MiddlewareWiring): void {\n const { config, logger, getReady } = wiring;\n app.use(\"*\", requestId());\n // OTEL server spans (W3C extraction, route-template naming). Mounted right\n // after requestId so `storyshelf.req_id` resolves; `enduser.id` resolves\n // once storeScope runs downstream. Noop without an SDK.\n app.use(\n \"*\",\n createHttpMiddleware({ serviceName: \"storyshelf\", serviceVersion: packageVersion() }),\n );\n // Structured request logging. Uses a Hono-native middleware rather than\n // Pino-http, which expects a Node server response (`res.on`) incompatible\n // With Hono's Web `Request`/`Response` model.\n app.use(\"*\", requestLogging(logger));\n app.use(\"*\", setupGate(getReady));\n // Routes classified `testing` answer 404 without `options.testing` (ADR 0006, decision 8).\n app.use(\"*\", requireTesting(wiring.options.testing !== undefined));\n mountGuards(app, config, wiring.options);\n mountStoreScope(app, wiring);\n app.use(\"*\", authGate());\n // Routes classified `review` answer 404 for a project with review off (ADR 0013, decision 10).\n app.use(\"*\", requireReview());\n}\n\nconst MINUTE = 60_000;\n\n/** Per-path limits; `bucket` gives a route family its own counter (default: shared `api:`). */\nconst RATE_LIMITS: readonly { paths: string[]; max: number; bucket?: string }[] = [\n { paths: [\"/api/v1/*\"], max: 100 },\n { paths: [\"/api/v1/tokens/*\"], max: 10 },\n { paths: [\"/api/v1/webhooks/*\"], max: 20 },\n // Auth endpoints ride their own buckets (the store keys by IP, so a prefix\n // keeps login floods from eating the API budget or vice versa).\n { paths: [\"/auth/*\"], max: 100, bucket: \"auth\" },\n { paths: [\"/api/auth/*\"], max: 300, bucket: \"engine\" },\n {\n paths: [\n \"/api/v1/projects/:slug/notification-channels\",\n \"/api/v1/projects/:slug/notification-channels/:channelId\",\n ],\n max: 20,\n bucket: \"notify-channels\",\n },\n {\n paths: [\"/api/v1/projects/:slug/notifications\", \"/api/v1/projects/:slug/notifications/me\"],\n max: 30,\n bucket: \"notify-subs\",\n },\n {\n paths: [\n \"/api/v1/admin/notification-channels\",\n \"/api/v1/admin/notification-channels/:channelId\",\n ],\n max: 20,\n bucket: \"notify-admin\",\n },\n];\n\n/** Requests per minute per client IP on the signed Storybook URL route (`testing.storybookUrlRateLimit`). */\nconst DEFAULT_STORYBOOK_URL_RATE_LIMIT = 6000;\n\n/** Mount rate limits and CSRF guards (abuse floors, not strict protection). */\nfunction mountGuards(app: ShelfRouter, config: ShelfConfig, options: ShelfOptions): void {\n const rateLimit = createRateLimiter();\n // A render fetches thousands of assets from one farm IP, so this floor is generous; it counts\n // before the token is looked at and answers 429 alike for any token.\n const signedUrls = {\n paths: [`${STORYBOOK_TOKEN_PREFIX}*`],\n max: options.testing?.storybookUrlRateLimit ?? DEFAULT_STORYBOOK_URL_RATE_LIMIT,\n bucket: \"signed-storybook\",\n };\n for (const { paths, max, bucket } of [...RATE_LIMITS, signedUrls]) {\n const keyGenerator = bucket ? rateLimitKey(bucket) : undefined;\n for (const path of paths) {\n app.use(path, rateLimit({ windowMs: MINUTE, max, keyGenerator }));\n }\n }\n for (const path of CSRF_PATHS) {\n app.use(path, csrf(config.secret));\n }\n}\n\n/** Mount the request-scoped store (db/storage/config/user/adapters). */\nfunction mountStoreScope(app: ShelfRouter, wiring: MiddlewareWiring): void {\n const {\n options,\n config,\n ui,\n logger,\n authEnabled,\n enqueueCapture,\n captureQueue,\n capturesHere,\n publish,\n cancelBuildJobs,\n buildState,\n design,\n notifiers,\n publicOrigin,\n } = wiring;\n app.use(\n \"*\",\n storeScope({\n db: options.database,\n storage: options.storage,\n config,\n ui,\n logger,\n authEnabled,\n testing: options.testing !== undefined,\n enqueueCapture,\n captureQueue,\n capturesHere,\n publish,\n cancelBuildJobs,\n buildState,\n captureConcurrency: options.testing?.concurrency,\n runner: options.testing?.runner,\n publicOrigin,\n design,\n notifiers,\n notificationsConfig: options.notifications?.config,\n ai: options.ai,\n resolveUser: async (c) => await resolveRequestUser(c, options.auth),\n }),\n );\n}\n\n/** Register every JSON API router. */\nfunction registerApiRoutes(app: ShelfRouter, health: HealthDeps): void {\n registerProjectRoutes(app);\n registerNotificationRoutes(app);\n registerHealth(app, health);\n registerAdmin(app);\n}\n\n/** Register the per-project resource routers. */\nfunction registerProjectRoutes(app: ShelfRouter): void {\n registerProjects(app);\n registerBuilds(app);\n registerLabels(app);\n registerLinks(app);\n registerBaselines(app);\n registerMedia(app);\n registerMembers(app);\n registerTokens(app);\n registerWebhooks(app);\n registerInsights(app);\n}\n\n/** Register the notification channel and subscription routers. */\nfunction registerNotificationRoutes(app: ShelfRouter): void {\n registerNotificationChannels(app);\n registerNotificationSubscriptions(app);\n registerSystemChannels(app);\n}\n\n/** Register the auth flow: engine mount, relying-party documents, profile. */\nfunction registerAuthFlow(app: ShelfRouter, options: ShelfOptions): void {\n // Auth-gated pages mount only with auth: authEnabled=false mounts zero\n // auth routes. Static imports stay — true lazy loading would need an async\n // createShelfApp, which the sync Hono assembly cannot do.\n if (!options.auth) {\n return;\n }\n registerEngineAuth(app, options.auth);\n registerWellKnown(app, options.auth);\n registerProfile(app, options.auth);\n registerConnections(app);\n}\n\n/** Register HTML pages, assets, and the optional auth flow. */\nfunction registerPageRoutes(app: ShelfRouter, options: ShelfOptions, health: HealthDeps): void {\n registerAuthFlow(app, options);\n registerAssets(app);\n registerStorybook(app);\n registerSignedStorybook(app);\n registerPreviewToken(app);\n registerAdminPages(app, health, options.auth);\n registerPublicOriginRoutes(app);\n registerUiPages(app);\n registerReviewForms(app);\n registerInsightFragments(app);\n}\n\n/** Register every API router, page set, and optional auth flow. */\nfunction registerAllRoutes(app: ShelfRouter, options: ShelfOptions, health: HealthDeps): void {\n registerApiRoutes(app, health);\n registerPageRoutes(app, options, health);\n}\n\n/** The OpenAPI document's info and tag catalog. */\nconst OPENAPI_CONFIG = {\n openapi: \"3.0.0\",\n info: {\n title: \"StoryShelf API\",\n description:\n \"REST API for the StoryShelf visual testing platform. JSON endpoints live under /api/v1; HTML pages are served at /.\",\n version: \"1.0.0\",\n },\n tags: [\n {\n name: \"Projects\",\n description: \"Create and manage projects (one project is one Storybook).\",\n },\n {\n name: \"Builds\",\n description: \"Upload Storybook builds, inspect capture attempts, retry, and delete.\",\n },\n {\n name: \"Review\",\n description:\n \"Approve or reject snapshots, bulk actions, re-diff, build verdicts, the build changeset and review comments.\",\n },\n { name: \"Labels\", description: \"Typed build labels and their link templates.\" },\n {\n name: \"Links\",\n description:\n \"Link packages to the projects that publish them and see where stories use them.\",\n },\n { name: \"Baselines\", description: \"Export and import baselines as a portable zip.\" },\n {\n name: \"Members\",\n description: \"Project members, roles, and identity-provider group mappings.\",\n },\n {\n name: \"Insights\",\n description: \"AI build triage and project health (advisory only; needs AI configured).\",\n },\n { name: \"Tokens\", description: \"API tokens used by the CLI and CI.\" },\n { name: \"Webhooks\", description: \"Signed webhook deliveries for build events.\" },\n {\n name: \"Notifications\",\n description: \"Email, chat and git notification channels and subscriptions.\",\n },\n { name: \"Admin\", description: \"Site-wide administration.\" },\n ],\n} as const;\n\n/** Register the OpenAPI document and interactive docs page. */\nfunction registerDocs(app: ShelfRouter): void {\n // Every operation carries the class of its route (`x-storyshelf-class`), so a client can tell\n // what an instance hides: `testing` routes 404 without `testing`, `review` ones with review off.\n app.get(\"/api/v1/openapi.json\", (c) =>\n c.json(\n markRouteClasses(\n app.getOpenAPIDocument({ ...OPENAPI_CONFIG, tags: [...OPENAPI_CONFIG.tags] }),\n ),\n ),\n );\n app.get(\"/api/v1/docs\", swaggerUI({ url: \"/api/v1/openapi.json\" }));\n}\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Package version injected at build via the `__PKG_VERSION__` define. Read as a bare identifier:\n * bundler defines replace identifiers, not `globalThis` property reads. Undefined when running\n * from source (dev server, tests).\n */\nfunction packageVersion(): string {\n return typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\";\n}\n\n/**\n * Public surface: the app and its types only.\n *\n * `createShelfApp` plus the option/config types needed to call it. Adapter\n * interfaces live under `core/adapter/*`; runtime helpers under `core/logger`,\n * `core/capture`, `core/paths`, `core/urls`, `core/diff`; tables and row types\n * under `core/schema`. Importing the barrel must never pull Node-only modules\n * into edge bundles beyond what Hono itself needs.\n */\nexport { createAuthSystemHook } from \"./notify.ts\";\nexport type {\n ShelfOptions,\n ShelfTestingOptions,\n ShelfCompareOptions,\n ShelfNotificationsOptions,\n ShelfConfig,\n UIConfig,\n BrandTheme,\n NotificationsConfig,\n} from \"@storyshelf/core/config\";\nexport {\n createShelfLogger,\n type Logger,\n type LoggerOptions,\n type PinoTransport,\n} from \"@storyshelf/core/logger\";\nexport type { ShelfApp, ShelfContext, ShelfLifecycle, ShelfRouter } from \"./app-types.ts\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,SAAS,uBAAuB,SAA6B;CAC3D,MAAM,UAAU,qBAAqB,iBAAiB,OAAO,CAAC;CAC9D,IAAI,QAAQ,SAAS,GACnB,MAAM,IAAI,sBAAsB,SAAS,OAAO;AAEpD;;AAGA,SAAS,oBAAoB,SAAqC;CAChE,OAAO;EACL,GAAG;EACH,UAAU,2BAA2B,QAAQ,QAAQ;EACrD,SAAS,0BAA0B,QAAQ,OAAO;CACpD;AACF;;AAGA,SAAS,WAAW,QAAsB,SAAwB,MAAiC;CACjG,MAAM,OAAO,UAAU,QAAQ,OAAO;CACtC,KAAK,WAAW,KAAK;CACrB,aAAa,MAAM,QAAQ,SAAS,KAAK,UAAU;CACnD,OAAO;AACT;;;;;AAMA,SAAS,iBAAiB,SAAuB,QAAsB;CACrE,MAAM,UAAU,iBAAiB,QAAQ,SAAS,WAAW,KAAA,CAAS;CACtE,IAAI,SACF,OAAO,KAAK,OAAO;AAEvB;;AAGA,SAAS,YAAY,KAAe,SAAuB,QAA4B;CACrF,MAAM,UAAU,eAAe,OAAO;CACtC,iBAAiB,SAAS,QAAQ,MAAM;CACxC,MAAM,OAAsB;EAAE,OAAO,QAAQ,QAAQ;GAAE,IAAI;GAAM,UAAU,CAAC;EAAE,CAAC;EAAG,SAAS;CAAK;CAChG,gBAAgB,KAAK,SAAS,SAAS,IAAI;CAC3C,IAAI,QAAQ,OAAO,iBAAiB,MAClC,IAAI,IAAI,KAAK,aAAa,IAAI,CAAC;CAEjC,MAAM,SAAS,WAAW,QAAQ,SAAS,IAAI;CAC/C,eAAe,KAAK;EAClB,GAAG;EACH,GAAG;EACH,SAAS;EACT,UAAU,YAAY,MAAM,KAAK;CACnC,CAAC;CACD,kBAAkB,KAAK,SAAS;EAC9B,SAAS,iBAAiB,OAAO;EACjC,kBAAkB,KAAK;EACvB,YAAY,KAAK,IAAI;EACrB,SAAS,eAAe;CAC1B,CAAC;CACD,aAAa,GAAG;AAClB;;;;;;;;AASA,SAAgB,eAAe,YAAoC;CACjE,MAAM,UAAU,oBAAoB,UAAU;CAC9C,gBAAgB;EACd,QAAQ,QAAQ,SAAS,QAAQ;EACjC,SAAS,QAAQ,SAAS,SAAS;CACrC,CAAC;CACD,uBAAuB,OAAO;CAC9B,MAAM,MAAM,IAAI,YAAyC;CACzD,YAAY,KAAK,SAAS,oBAAoB,OAAO,CAAC;CAEtD,OAAO;AACT;;AAGA,MAAM,aAAa;CACjB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;AAGA,SAAS,eAAe,KAAkB,QAAgC;CACxE,MAAM,EAAE,QAAQ,QAAQ,aAAa;CACrC,IAAI,IAAI,KAAK,UAAU,CAAC;CAIxB,IAAI,IACF,KACA,qBAAqB;EAAE,aAAa;EAAc,gBAAgB,eAAe;CAAE,CAAC,CACtF;CAIA,IAAI,IAAI,KAAK,eAAe,MAAM,CAAC;CACnC,IAAI,IAAI,KAAK,UAAU,QAAQ,CAAC;CAEhC,IAAI,IAAI,KAAK,eAAe,OAAO,QAAQ,YAAY,KAAA,CAAS,CAAC;CACjE,YAAY,KAAK,QAAQ,OAAO,OAAO;CACvC,gBAAgB,KAAK,MAAM;CAC3B,IAAI,IAAI,KAAK,SAAS,CAAC;CAEvB,IAAI,IAAI,KAAK,cAAc,CAAC;AAC9B;AAEA,MAAM,SAAS;;AAGf,MAAM,cAA4E;CAChF;EAAE,OAAO,CAAC,WAAW;EAAG,KAAK;CAAI;CACjC;EAAE,OAAO,CAAC,kBAAkB;EAAG,KAAK;CAAG;CACvC;EAAE,OAAO,CAAC,oBAAoB;EAAG,KAAK;CAAG;CAGzC;EAAE,OAAO,CAAC,SAAS;EAAG,KAAK;EAAK,QAAQ;CAAO;CAC/C;EAAE,OAAO,CAAC,aAAa;EAAG,KAAK;EAAK,QAAQ;CAAS;CACrD;EACE,OAAO,CACL,gDACA,yDACF;EACA,KAAK;EACL,QAAQ;CACV;CACA;EACE,OAAO,CAAC,wCAAwC,yCAAyC;EACzF,KAAK;EACL,QAAQ;CACV;CACA;EACE,OAAO,CACL,uCACA,gDACF;EACA,KAAK;EACL,QAAQ;CACV;AACF;;AAGA,MAAM,mCAAmC;;AAGzC,SAAS,YAAY,KAAkB,QAAqB,SAA6B;CACvF,MAAM,YAAY,kBAAkB;CAGpC,MAAM,aAAa;EACjB,OAAO,CAAC,GAAG,uBAAuB,EAAE;EACpC,KAAK,QAAQ,SAAS,yBAAyB;EAC/C,QAAQ;CACV;CACA,KAAK,MAAM,EAAE,OAAO,KAAK,YAAY,CAAC,GAAG,aAAa,UAAU,GAAG;EACjE,MAAM,eAAe,SAAS,aAAa,MAAM,IAAI,KAAA;EACrD,KAAK,MAAM,QAAQ,OACjB,IAAI,IAAI,MAAM,UAAU;GAAE,UAAU;GAAQ;GAAK;EAAa,CAAC,CAAC;CAEpE;CACA,KAAK,MAAM,QAAQ,YACjB,IAAI,IAAI,MAAM,KAAK,OAAO,MAAM,CAAC;AAErC;;AAGA,SAAS,gBAAgB,KAAkB,QAAgC;CACzE,MAAM,EACJ,SACA,QACA,IACA,QACA,aACA,gBACA,cACA,cACA,SACA,iBACA,YACA,QACA,WACA,iBACE;CACJ,IAAI,IACF,KACA,WAAW;EACT,IAAI,QAAQ;EACZ,SAAS,QAAQ;EACjB;EACA;EACA;EACA;EACA,SAAS,QAAQ,YAAY,KAAA;EAC7B;EACA;EACA;EACA;EACA;EACA;EACA,oBAAoB,QAAQ,SAAS;EACrC,QAAQ,QAAQ,SAAS;EACzB;EACA;EACA;EACA,qBAAqB,QAAQ,eAAe;EAC5C,IAAI,QAAQ;EACZ,aAAa,OAAO,MAAM,MAAM,mBAAmB,GAAG,QAAQ,IAAI;CACpE,CAAC,CACH;AACF;;AAGA,SAAS,kBAAkB,KAAkB,QAA0B;CACrE,sBAAsB,GAAG;CACzB,2BAA2B,GAAG;CAC9B,eAAe,KAAK,MAAM;CAC1B,cAAc,GAAG;AACnB;;AAGA,SAAS,sBAAsB,KAAwB;CACrD,iBAAiB,GAAG;CACpB,eAAe,GAAG;CAClB,eAAe,GAAG;CAClB,cAAc,GAAG;CACjB,kBAAkB,GAAG;CACrB,cAAc,GAAG;CACjB,gBAAgB,GAAG;CACnB,eAAe,GAAG;CAClB,iBAAiB,GAAG;CACpB,iBAAiB,GAAG;AACtB;;AAGA,SAAS,2BAA2B,KAAwB;CAC1D,6BAA6B,GAAG;CAChC,kCAAkC,GAAG;CACrC,uBAAuB,GAAG;AAC5B;;AAGA,SAAS,iBAAiB,KAAkB,SAA6B;CAIvE,IAAI,CAAC,QAAQ,MACX;CAEF,mBAAmB,KAAK,QAAQ,IAAI;CACpC,kBAAkB,KAAK,QAAQ,IAAI;CACnC,gBAAgB,KAAK,QAAQ,IAAI;CACjC,oBAAoB,GAAG;AACzB;;AAGA,SAAS,mBAAmB,KAAkB,SAAuB,QAA0B;CAC7F,iBAAiB,KAAK,OAAO;CAC7B,eAAe,GAAG;CAClB,kBAAkB,GAAG;CACrB,wBAAwB,GAAG;CAC3B,qBAAqB,GAAG;CACxB,mBAAmB,KAAK,QAAQ,QAAQ,IAAI;CAC5C,2BAA2B,GAAG;CAC9B,gBAAgB,GAAG;CACnB,oBAAoB,GAAG;CACvB,yBAAyB,GAAG;AAC9B;;AAGA,SAAS,kBAAkB,KAAkB,SAAuB,QAA0B;CAC5F,kBAAkB,KAAK,MAAM;CAC7B,mBAAmB,KAAK,SAAS,MAAM;AACzC;;AAGA,MAAM,iBAAiB;CACrB,SAAS;CACT,MAAM;EACJ,OAAO;EACP,aACE;EACF,SAAS;CACX;CACA,MAAM;EACJ;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aACE;EACJ;EACA;GAAE,MAAM;GAAU,aAAa;EAA+C;EAC9E;GACE,MAAM;GACN,aACE;EACJ;EACA;GAAE,MAAM;GAAa,aAAa;EAAiD;EACnF;GACE,MAAM;GACN,aAAa;EACf;EACA;GACE,MAAM;GACN,aAAa;EACf;EACA;GAAE,MAAM;GAAU,aAAa;EAAqC;EACpE;GAAE,MAAM;GAAY,aAAa;EAA8C;EAC/E;GACE,MAAM;GACN,aAAa;EACf;EACA;GAAE,MAAM;GAAS,aAAa;EAA4B;CAC5D;AACF;;AAGA,SAAS,aAAa,KAAwB;CAG5C,IAAI,IAAI,yBAAyB,MAC/B,EAAE,KACA,iBACE,IAAI,mBAAmB;EAAE,GAAG;EAAgB,MAAM,CAAC,GAAG,eAAe,IAAI;CAAE,CAAC,CAC9E,CACF,CACF;CACA,IAAI,IAAI,gBAAgB,UAAU,EAAE,KAAK,uBAAuB,CAAC,CAAC;AACpE;;;;;;AASA,SAAS,iBAAyB;CAChC,OAAA;AACF"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["import { swaggerUI } from \"@hono/swagger-ui\";\nimport { OpenAPIHono } from \"@hono/zod-openapi\";\nimport {\n AdapterLifecycleError,\n adapterSourcesOf,\n validateBootAssembly,\n} from \"@storyshelf/core/adapter/setup\";\nimport { assertServerless, resolveShelfOptions } from \"@storyshelf/core/config\";\nimport type { ShelfConfig, ShelfOptions } from \"@storyshelf/core/config\";\nimport { configureImages } from \"@storyshelf/core/diff\";\nimport type { Logger } from \"@storyshelf/core/logger\";\nimport { STORYBOOK_TOKEN_PREFIX } from \"@storyshelf/core/utils\";\n/**\n * StoryShelf app: compose database, storage, capture, auth, and notifier\n * adapters into a complete self-hosted visual-testing Hono app.\n */\nimport {\n createHttpMiddleware,\n createInstrumentedDatabase,\n createInstrumentedStorage,\n} from \"@storyshelf/observability\";\nimport { requestId } from \"hono/request-id\";\nimport type { ShelfApp, ShelfContext, ShelfRouter } from \"./app-types.ts\";\nimport { runBootTasks } from \"./boot-tasks.ts\";\nimport { setupJobs, type JobsWiring } from \"./jobs-setup.ts\";\nimport { attachLifecycle, type LifecycleCell } from \"./lifecycle.ts\";\nimport {\n authGate,\n csrf,\n createRateLimiter,\n requireReview,\n requireTesting,\n rateLimitKey,\n requestLogging,\n resolveRequestUser,\n storeScope,\n} from \"./middleware/index.ts\";\nimport { serverTiming } from \"./middleware/server-timing.ts\";\nimport { setupGate, type MiddlewareWiring } from \"./middleware/setup-gate.ts\";\nimport { registerAdminPages } from \"./routers/admin-pages.ts\";\nimport { registerPublicOriginRoutes } from \"./routers/admin-public-origin.ts\";\nimport { registerAdmin } from \"./routers/admin.ts\";\nimport { registerAssets } from \"./routers/assets.ts\";\nimport { registerEngineAuth } from \"./routers/auth.ts\";\nimport { registerBaselines } from \"./routers/baselines.ts\";\nimport { registerBuilds } from \"./routers/builds.ts\";\nimport { registerConnections } from \"./routers/connections.ts\";\nimport { registerHealth, type HealthDeps } from \"./routers/health.ts\";\nimport { registerInsightFragments } from \"./routers/insights-ui.tsx\";\nimport { registerInsights } from \"./routers/insights.ts\";\nimport { registerLabels } from \"./routers/labels.ts\";\nimport { registerLinks } from \"./routers/links.ts\";\nimport { registerMedia } from \"./routers/media.ts\";\nimport { registerMembers } from \"./routers/members.ts\";\nimport {\n registerNotificationChannels,\n registerSystemChannels,\n} from \"./routers/notification-channels.ts\";\nimport { registerNotificationSubscriptions } from \"./routers/notification-subscriptions.ts\";\nimport { registerPreviewToken } from \"./routers/preview-token.ts\";\nimport { registerProfile } from \"./routers/profile.ts\";\nimport { registerProjects } from \"./routers/projects.ts\";\nimport { registerReviewForms } from \"./routers/review-forms.ts\";\nimport { registerSignedStorybook } from \"./routers/signed-storybook.ts\";\nimport { registerStorybook } from \"./routers/storybook.ts\";\nimport { registerTokens } from \"./routers/tokens.ts\";\nimport { registerUiPages } from \"./routers/ui.ts\";\nimport { registerWebhooks } from \"./routers/webhooks.ts\";\nimport { registerWellKnown } from \"./routers/well-known.ts\";\nimport { markRouteClasses } from \"./routes/openapi-classes.ts\";\nimport { resolveRuntime, type ServerRuntime } from \"./runtime.ts\";\n\n/**\n * Fail fast on third-party adapters that cannot satisfy their slot.\n * Throws before any route or lifecycle hook runs.\n */\nfunction throwOnInvalidAdapters(options: ShelfOptions): void {\n const invalid = validateBootAssembly(adapterSourcesOf(options));\n if (invalid.length > 0) {\n throw new AdapterLifecycleError(\"setup\", invalid, true);\n }\n}\n\n/** Request/capture-path adapters with OTEL wrappers (lifecycle keeps raw ones). */\nfunction instrumentedOptions(options: ShelfOptions): ShelfOptions {\n return {\n ...options,\n database: createInstrumentedDatabase(options.database),\n storage: createInstrumentedStorage(options.storage),\n };\n}\n\n/** Wire the job queue, its handlers and its consumer, and run the boot tasks that need them. */\nfunction wireQueues(scoped: ShelfOptions, runtime: ServerRuntime, cell: LifecycleCell): JobsWiring {\n const jobs = setupJobs(scoped, runtime);\n cell.consumer = jobs.consumer;\n runBootTasks(cell, scoped, runtime, jobs.buildState);\n return jobs;\n}\n\n/**\n * Serverless check (ADR 0006, decision 3): throws on AWS Lambda, warns on Vercel and Netlify. Only\n * an instance with `testing` needs a process that outlives the request.\n */\nfunction warnOnServerless(options: ShelfOptions, logger: Logger): void {\n const warning = assertServerless(options.testing?.runner !== undefined);\n if (warning) {\n logger.warn(warning);\n }\n}\n\n/** Attach lifecycle, queue, middleware, routes, and docs to the app. */\nfunction assembleApp(app: ShelfApp, options: ShelfOptions, scoped: ShelfOptions): void {\n const runtime = resolveRuntime(options);\n warnOnServerless(options, runtime.logger);\n const cell: LifecycleCell = { ready: Promise.resolve({ ok: true, failures: [] }), settled: null };\n attachLifecycle(app, options, runtime, cell);\n if (runtime.config.serverTiming === true) {\n app.use(\"*\", serverTiming(true));\n }\n const queues = wireQueues(scoped, runtime, cell);\n wireMiddleware(app, {\n ...runtime,\n ...queues,\n options: scoped,\n getReady: async () => await cell.ready,\n });\n registerAllRoutes(app, options, {\n sources: adapterSourcesOf(options),\n getSettled: () => cell.settled,\n bootTimeMs: Date.now(),\n version: packageVersion(),\n });\n registerDocs(app);\n}\n\n/**\n * Create the StoryShelf Hono app with all API routes and HTML pages.\n *\n * Adapter `init` hooks kick off eagerly in the background (the constructor\n * stays synchronous). Await `app.lifecycle.setup()` before serving for\n * fail-fast startup, or let the first request gate on settlement.\n */\nexport function createShelfApp(rawOptions: ShelfOptions): ShelfApp {\n const options = resolveShelfOptions(rawOptions);\n configureImages({\n engine: options.testing?.images?.engine,\n threads: options.testing?.compare?.threads,\n });\n throwOnInvalidAdapters(options);\n const app = new OpenAPIHono<{ Variables: ShelfContext }>() as ShelfApp;\n assembleApp(app, options, instrumentedOptions(options));\n\n return app;\n}\n\n/** Cookie-authenticated write paths guarded by CSRF tokens. */\nconst CSRF_PATHS = [\n \"/auth/logout\",\n \"/auth/invites/*\",\n \"/projects/:slug/settings/*\",\n \"/profile/*\",\n \"/connections/*\",\n \"/profile\",\n \"/admin/credentials/*\",\n \"/admin/public-origin\",\n \"/admin/public-origin/*\",\n \"/projects/:slug/builds/:buildId/insights/generate\",\n \"/projects/:slug/builds/:buildId/comments\",\n \"/projects/:slug/builds/:buildId/comments/*\",\n \"/projects/:slug/builds/:buildId/reviews\",\n \"/projects/:slug/insights/health/generate\",\n] as const;\n\n/** Attach global middleware: ids, logging, init gate, limits, store scope, auth gate. */\nfunction wireMiddleware(app: ShelfRouter, wiring: MiddlewareWiring): void {\n const { config, logger, getReady } = wiring;\n app.use(\"*\", requestId());\n // OTEL server spans (W3C extraction, route-template naming). Mounted right\n // after requestId so `storyshelf.req_id` resolves; `enduser.id` resolves\n // once storeScope runs downstream. Noop without an SDK.\n app.use(\n \"*\",\n createHttpMiddleware({ serviceName: \"storyshelf\", serviceVersion: packageVersion() }),\n );\n // Structured request logging. Uses a Hono-native middleware rather than\n // Pino-http, which expects a Node server response (`res.on`) incompatible\n // With Hono's Web `Request`/`Response` model.\n app.use(\"*\", requestLogging(logger));\n app.use(\"*\", setupGate(getReady));\n // Routes classified `testing` answer 404 without `options.testing` (ADR 0006, decision 8).\n app.use(\"*\", requireTesting(wiring.options.testing !== undefined));\n mountGuards(app, config, wiring.options);\n mountStoreScope(app, wiring);\n app.use(\"*\", authGate());\n // Routes classified `review` answer 404 for a project with review off (ADR 0013, decision 10).\n app.use(\"*\", requireReview());\n}\n\nconst MINUTE = 60_000;\n\n/** Per-path limits; `bucket` gives a route family its own counter (default: shared `api:`). */\nconst RATE_LIMITS: readonly { paths: string[]; max: number; bucket?: string }[] = [\n { paths: [\"/api/v1/*\"], max: 100 },\n { paths: [\"/api/v1/tokens/*\"], max: 10 },\n { paths: [\"/api/v1/webhooks/*\"], max: 20 },\n // Auth endpoints ride their own buckets (the store keys by IP, so a prefix\n // keeps login floods from eating the API budget or vice versa).\n { paths: [\"/auth/*\"], max: 100, bucket: \"auth\" },\n { paths: [\"/api/auth/*\"], max: 300, bucket: \"engine\" },\n {\n paths: [\n \"/api/v1/projects/:slug/notification-channels\",\n \"/api/v1/projects/:slug/notification-channels/:channelId\",\n ],\n max: 20,\n bucket: \"notify-channels\",\n },\n {\n paths: [\"/api/v1/projects/:slug/notifications\", \"/api/v1/projects/:slug/notifications/me\"],\n max: 30,\n bucket: \"notify-subs\",\n },\n {\n paths: [\n \"/api/v1/admin/notification-channels\",\n \"/api/v1/admin/notification-channels/:channelId\",\n ],\n max: 20,\n bucket: \"notify-admin\",\n },\n];\n\n/** Requests per minute per client IP on the signed Storybook URL route (`testing.storybookUrlRateLimit`). */\nconst DEFAULT_STORYBOOK_URL_RATE_LIMIT = 6000;\n\n/** Mount rate limits and CSRF guards (abuse floors, not strict protection). */\nfunction mountGuards(app: ShelfRouter, config: ShelfConfig, options: ShelfOptions): void {\n const rateLimit = createRateLimiter();\n // A render fetches thousands of assets from one farm IP, so this floor is generous; it counts\n // before the token is looked at and answers 429 alike for any token.\n const signedUrls = {\n paths: [`${STORYBOOK_TOKEN_PREFIX}*`],\n max: options.testing?.storybookUrlRateLimit ?? DEFAULT_STORYBOOK_URL_RATE_LIMIT,\n bucket: \"signed-storybook\",\n };\n for (const { paths, max, bucket } of [...RATE_LIMITS, signedUrls]) {\n const keyGenerator = bucket ? rateLimitKey(bucket) : undefined;\n for (const path of paths) {\n app.use(path, rateLimit({ windowMs: MINUTE, max, keyGenerator }));\n }\n }\n for (const path of CSRF_PATHS) {\n app.use(path, csrf(config.secret));\n }\n}\n\n/** Mount the request-scoped store (db/storage/config/user/adapters). */\nfunction mountStoreScope(app: ShelfRouter, wiring: MiddlewareWiring): void {\n const {\n options,\n config,\n ui,\n logger,\n authEnabled,\n enqueueCapture,\n captureQueue,\n capturesHere,\n publish,\n cancelBuildJobs,\n buildState,\n design,\n notifiers,\n publicOrigin,\n } = wiring;\n app.use(\n \"*\",\n storeScope({\n db: options.database,\n storage: options.storage,\n config,\n ui,\n logger,\n authEnabled,\n testing: options.testing !== undefined,\n enqueueCapture,\n captureQueue,\n capturesHere,\n publish,\n cancelBuildJobs,\n buildState,\n captureConcurrency: options.testing?.concurrency,\n runner: options.testing?.runner,\n publicOrigin,\n design,\n notifiers,\n notificationsConfig: options.notifications?.config,\n ai: options.ai,\n resolveUser: async (c) => await resolveRequestUser(c, options.auth),\n }),\n );\n}\n\n/** Register every JSON API router. */\nfunction registerApiRoutes(app: ShelfRouter, health: HealthDeps): void {\n registerProjectRoutes(app);\n registerNotificationRoutes(app);\n registerHealth(app, health);\n registerAdmin(app);\n}\n\n/** Register the per-project resource routers. */\nfunction registerProjectRoutes(app: ShelfRouter): void {\n registerProjects(app);\n registerBuilds(app);\n registerLabels(app);\n registerLinks(app);\n registerBaselines(app);\n registerMedia(app);\n registerMembers(app);\n registerTokens(app);\n registerWebhooks(app);\n registerInsights(app);\n}\n\n/** Register the notification channel and subscription routers. */\nfunction registerNotificationRoutes(app: ShelfRouter): void {\n registerNotificationChannels(app);\n registerNotificationSubscriptions(app);\n registerSystemChannels(app);\n}\n\n/** Register the auth flow: engine mount, relying-party documents, profile. */\nfunction registerAuthFlow(app: ShelfRouter, options: ShelfOptions): void {\n // Auth-gated pages mount only with auth: authEnabled=false mounts zero\n // auth routes. Static imports stay — true lazy loading would need an async\n // createShelfApp, which the sync Hono assembly cannot do.\n if (!options.auth) {\n return;\n }\n registerEngineAuth(app, options.auth);\n registerWellKnown(app, options.auth);\n registerProfile(app, options.auth);\n registerConnections(app);\n}\n\n/** Register HTML pages, assets, and the optional auth flow. */\nfunction registerPageRoutes(app: ShelfRouter, options: ShelfOptions, health: HealthDeps): void {\n registerAuthFlow(app, options);\n registerAssets(app);\n registerStorybook(app);\n registerSignedStorybook(app);\n registerPreviewToken(app);\n registerAdminPages(app, health, options.auth);\n registerPublicOriginRoutes(app);\n registerUiPages(app);\n registerReviewForms(app);\n registerInsightFragments(app);\n}\n\n/** Register every API router, page set, and optional auth flow. */\nfunction registerAllRoutes(app: ShelfRouter, options: ShelfOptions, health: HealthDeps): void {\n registerApiRoutes(app, health);\n registerPageRoutes(app, options, health);\n}\n\n/** The OpenAPI document's info and tag catalog. */\nconst OPENAPI_CONFIG = {\n openapi: \"3.0.0\",\n info: {\n title: \"StoryShelf API\",\n description:\n \"REST API for the StoryShelf visual testing platform. JSON endpoints live under /api/v1; HTML pages are served at /.\",\n version: \"1.0.0\",\n },\n tags: [\n {\n name: \"Projects\",\n description: \"Create and manage projects (one project is one Storybook).\",\n },\n {\n name: \"Builds\",\n description: \"Upload Storybook builds, inspect capture attempts, retry, and delete.\",\n },\n {\n name: \"Review\",\n description:\n \"Approve or reject snapshots, bulk actions, re-diff, build verdicts, the build changeset and review comments.\",\n },\n { name: \"Labels\", description: \"Typed build labels and their link templates.\" },\n {\n name: \"Links\",\n description:\n \"Link packages to the projects that publish them and see where stories use them.\",\n },\n { name: \"Baselines\", description: \"Export and import baselines as a portable zip.\" },\n {\n name: \"Members\",\n description: \"Project members, roles, and identity-provider group mappings.\",\n },\n {\n name: \"Insights\",\n description: \"AI build triage and project health (advisory only; needs AI configured).\",\n },\n { name: \"Tokens\", description: \"API tokens used by the CLI and CI.\" },\n { name: \"Webhooks\", description: \"Signed webhook deliveries for build events.\" },\n {\n name: \"Notifications\",\n description: \"Email, chat and git notification channels and subscriptions.\",\n },\n { name: \"Admin\", description: \"Site-wide administration.\" },\n ],\n} as const;\n\n/** Register the OpenAPI document and interactive docs page. */\nfunction registerDocs(app: ShelfRouter): void {\n // Every operation carries the class of its route (`x-storyshelf-class`), so a client can tell\n // what an instance hides: `testing` routes 404 without `testing`, `review` ones with review off.\n app.get(\"/api/v1/openapi.json\", (c) =>\n c.json(\n markRouteClasses(\n app.getOpenAPIDocument({ ...OPENAPI_CONFIG, tags: [...OPENAPI_CONFIG.tags] }),\n ),\n ),\n );\n app.get(\"/api/v1/docs\", swaggerUI({ url: \"/api/v1/openapi.json\" }));\n}\n\ndeclare const __PKG_VERSION__: string | undefined;\n\n/**\n * Package version injected at build via the `__PKG_VERSION__` define. Read as a bare identifier:\n * bundler defines replace identifiers, not `globalThis` property reads. Undefined when running\n * from source (dev server, tests).\n */\nfunction packageVersion(): string {\n return typeof __PKG_VERSION__ === \"string\" ? __PKG_VERSION__ : \"0.0.0\";\n}\n\n/**\n * Public surface: the app and its types only.\n *\n * `createShelfApp` plus the option/config types needed to call it. Adapter\n * interfaces live under `core/adapter/*`; runtime helpers under `core/logger`,\n * `core/capture`, `core/paths`, `core/urls`, `core/diff`; tables and row types\n * under `core/schema`. Importing the barrel must never pull Node-only modules\n * into edge bundles beyond what Hono itself needs.\n */\nexport { createAuthSystemHook } from \"./notify.ts\";\nexport type {\n ShelfOptions,\n ShelfTestingOptions,\n ShelfCompareOptions,\n ShelfNotificationsOptions,\n ShelfConfig,\n UIConfig,\n BrandTheme,\n NotificationsConfig,\n} from \"@storyshelf/core/config\";\nexport {\n createShelfLogger,\n type Logger,\n type LoggerOptions,\n type PinoTransport,\n} from \"@storyshelf/core/logger\";\nexport type { ShelfApp, ShelfContext, ShelfLifecycle, ShelfRouter } from \"./app-types.ts\";\n"],"mappings":"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"}
|
package/dist/routers/builds.mjs
CHANGED
|
@@ -16,9 +16,10 @@ import { registerComments } from "./comments.mjs";
|
|
|
16
16
|
import { registerSnapshots } from "./snapshots.mjs";
|
|
17
17
|
import { createRoute, z } from "@hono/zod-openapi";
|
|
18
18
|
import { DEFAULT_MAX_UPLOAD_BYTES } from "@storyshelf/core/config";
|
|
19
|
-
import { storybookZipPath } from "@storyshelf/core/utils";
|
|
19
|
+
import { CONTENT_HASH_PATTERN, contentKey, isContentHash, storybookZipPath } from "@storyshelf/core/utils";
|
|
20
20
|
import { BuildModel, PackageUsageModel } from "@storyshelf/core/models";
|
|
21
21
|
import { HTTPException } from "hono/http-exception";
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
22
23
|
// src/routers/builds.ts
|
|
23
24
|
/** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */
|
|
24
25
|
async function planOrDiscard(zipBytes, projectId, buildId) {
|
|
@@ -108,7 +109,7 @@ function registerBuilds(app) {
|
|
|
108
109
|
await buildForProject(project.id, buildId);
|
|
109
110
|
const { hashes } = c.req.valid("json");
|
|
110
111
|
const needed = [];
|
|
111
|
-
for (const hash of hashes) if (!await getStore().storage.exists(
|
|
112
|
+
for (const hash of hashes) if (!await getStore().storage.exists(contentKey(hash))) needed.push(hash);
|
|
112
113
|
return c.json({ needed });
|
|
113
114
|
});
|
|
114
115
|
app.openapi(contentUploadRoute, async (c) => {
|
|
@@ -116,18 +117,17 @@ function registerBuilds(app) {
|
|
|
116
117
|
const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);
|
|
117
118
|
await buildForProject(project.id, buildId);
|
|
118
119
|
if ((c.req.header("content-type") ?? "").includes("multipart/")) {
|
|
119
|
-
const
|
|
120
|
-
for (const
|
|
121
|
-
|
|
122
|
-
const buffer = Buffer.from(await value.arrayBuffer());
|
|
123
|
-
await getStore().storage.write(`content/${hash}`, buffer);
|
|
120
|
+
const files = await verifiedUploads(await c.req.formData());
|
|
121
|
+
for (const { hash, buffer } of files) {
|
|
122
|
+
await getStore().storage.write(contentKey(hash), buffer);
|
|
124
123
|
await upsertContentRef(hash);
|
|
125
124
|
}
|
|
126
125
|
} else {
|
|
127
126
|
const hash = c.req.header("x-content-hash") ?? "";
|
|
128
|
-
|
|
127
|
+
refuseInvalidHash(hash);
|
|
129
128
|
const buffer = Buffer.from(await c.req.arrayBuffer());
|
|
130
|
-
|
|
129
|
+
refuseMismatch(hash, buffer);
|
|
130
|
+
await getStore().storage.write(contentKey(hash), buffer);
|
|
131
131
|
}
|
|
132
132
|
return c.json({ ok: true });
|
|
133
133
|
});
|
|
@@ -265,7 +265,38 @@ const uploadZipRoute = createRoute({
|
|
|
265
265
|
...notFound
|
|
266
266
|
}
|
|
267
267
|
});
|
|
268
|
-
|
|
268
|
+
/** Answer 400 and log only the reason: the refused value is never logged or echoed. */
|
|
269
|
+
function refuseUpload(reason, message) {
|
|
270
|
+
getStore().logger.warn({ reason }, "content upload refused");
|
|
271
|
+
throw new HTTPException(400, { message });
|
|
272
|
+
}
|
|
273
|
+
/** Answer 400 for a hash that is not a SHA-256 hex digest. */
|
|
274
|
+
function refuseInvalidHash(hash) {
|
|
275
|
+
if (!isContentHash(hash)) refuseUpload("invalid content hash", "Invalid content hash");
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* The shared `content/<hash>` blob belongs to no project, so its bytes must be what its name says:
|
|
279
|
+
* an upload whose SHA-256 differs from its hash is refused, or one project could plant bytes under
|
|
280
|
+
* a hash another project's manifest names.
|
|
281
|
+
*/
|
|
282
|
+
function refuseMismatch(hash, buffer) {
|
|
283
|
+
if (createHash("sha256").update(buffer).digest("hex") !== hash) refuseUpload("content does not match its hash", "Content does not match its hash");
|
|
284
|
+
}
|
|
285
|
+
/** The files of a multipart content upload; each key is a valid hash and names its own bytes. */
|
|
286
|
+
async function verifiedUploads(form) {
|
|
287
|
+
return await Promise.all([...form.entries()].map(async ([hash, value]) => {
|
|
288
|
+
if (!(value instanceof File)) refuseUpload("not a file", "Only files can be uploaded");
|
|
289
|
+
refuseInvalidHash(hash);
|
|
290
|
+
const buffer = Buffer.from(await value.arrayBuffer());
|
|
291
|
+
refuseMismatch(hash, buffer);
|
|
292
|
+
return {
|
|
293
|
+
hash,
|
|
294
|
+
buffer
|
|
295
|
+
};
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
const contentHashSchema = z.string().regex(CONTENT_HASH_PATTERN, "Invalid content hash");
|
|
299
|
+
const dedupHashesSchema = z.object({ hashes: z.array(contentHashSchema) });
|
|
269
300
|
const dedupNeededSchema = z.object({ needed: z.array(z.string()) });
|
|
270
301
|
const dedupRoute = createRoute({
|
|
271
302
|
method: "post",
|
|
@@ -311,7 +342,7 @@ const contentUploadRoute = createRoute({
|
|
|
311
342
|
});
|
|
312
343
|
const manifestFileSchema = z.object({
|
|
313
344
|
rel: z.string(),
|
|
314
|
-
hash:
|
|
345
|
+
hash: contentHashSchema,
|
|
315
346
|
size: z.number()
|
|
316
347
|
});
|
|
317
348
|
const manifestRoute = createRoute({
|
|
@@ -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 { DEFAULT_MAX_UPLOAD_BYTES } from \"@storyshelf/core/config\";\nimport { BuildModel, PackageUsageModel } from \"@storyshelf/core/models\";\nimport { storybookZipPath } from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { registerAttempts } from \"./attempts.ts\";\nimport {\n VIEW_ROLES,\n DEVELOPER_ROLES,\n APPROVER_ROLES,\n buildCreateJsonSchema,\n buildListQuery,\n buildForProject,\n createBuildRecord,\n storeUploadStream,\n} from \"./builds.handlers.ts\";\nimport { discardZip, planUpload, publishUpload } from \"./publish-upload.ts\";\n\n/** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */\nasync function planOrDiscard(\n zipBytes: number,\n projectId: string,\n buildId: string,\n): Promise<\"inline\" | \"job\"> {\n try {\n return planUpload(zipBytes);\n } catch (error) {\n await discardZip(projectId, buildId);\n throw error;\n }\n}\n\nasync function upsertContentRef(hash: string): Promise<void> {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, hash);\n if (existing) {\n await getStore().db.update(contentRefs, hash, {\n refCount: (existing as { refCount: number }).refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await getStore().db.insert(contentRefs, {\n hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist on old DB\n }\n}\nimport { registerBuildCancel } from \"./build-cancel.ts\";\nimport { registerBuildChangeset } from \"./build-changeset.ts\";\nimport { registerBuildReviews } from \"./build-reviews.ts\";\nimport { registerComments } from \"./comments.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport {\n badRequest,\n buildSchema,\n forbidden as forbiddenResponse,\n notFound as notFoundResponse,\n unauthorized,\n} from \"./schemas.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfRouter): 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, {\n status,\n branch: branch ?? undefined,\n labelKey: labelKey ?? undefined,\n labelValue: labelValue ?? undefined,\n });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(\n c,\n c.req.valid(\"param\").slug,\n ...DEVELOPER_ROLES,\n );\n const build = await createBuildRecord(project, c.req.valid(\"json\"));\n const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;\n return c.json(\n { build, uploadUrl, baselineSha: build.baselineSha, testing: getStore().testing },\n 202,\n );\n });\n\n app.openapi(affectedRoute, 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 { baselineSha, changedFiles, affectedImportPaths } = c.req.valid(\"json\");\n const updated = await new BuildModel(getStore().db).setAffected(build.id, {\n baselineSha,\n changedFiles,\n affectedImportPaths,\n });\n return c.json(updated);\n });\n\n app.openapi(usageRoute, 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 { usage } = c.req.valid(\"json\");\n const stored = await new PackageUsageModel(getStore().db).replaceForBuild(\n project.id,\n build.id,\n usage,\n );\n return c.json({ stored });\n });\n\n app.openapi(uploadZipRoute, 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 if (build.status === \"failed\") {\n throw new HTTPException(409, {\n message: `Build ${build.id} failed; run storyshelf upload again to create a new build`,\n });\n }\n const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;\n const zipBytes = await storeUploadStream(\n getStore().storage,\n storybookZipPath(project.id, build.id),\n c.req.raw.body,\n maxBytes,\n );\n const route = await planOrDiscard(zipBytes, project.id, build.id);\n const reqId = c.get(\"requestId\");\n const published = await publishUpload(route, build, reqId);\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(published, 202);\n });\n\n app.openapi(dedupRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const { hashes } = c.req.valid(\"json\");\n const needed: string[] = [];\n for (const hash of hashes) {\n if (!(await getStore().storage.exists(`content/${hash}`))) {\n needed.push(hash);\n }\n }\n return c.json({ needed });\n });\n\n // oxlint-disable eslint(max-depth) -- dedup upload handler is intentionally nested for batch handling\n app.openapi(contentUploadRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const contentType = c.req.header(\"content-type\") ?? \"\";\n if (contentType.includes(\"multipart/\")) {\n const form = await c.req.formData();\n for (const [key, value] of form.entries()) {\n if (value instanceof File) {\n const hash = key;\n const buffer = Buffer.from(await value.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n await upsertContentRef(hash);\n }\n }\n } else {\n // Single content upload via PUT with hash header\n const hash = c.req.header(\"x-content-hash\") ?? \"\";\n if (!hash) throw new HTTPException(400, { message: \"Missing X-Content-Hash\" });\n const buffer = Buffer.from(await c.req.arrayBuffer());\n await getStore().storage.write(`content/${hash}`, buffer);\n }\n return c.json({ ok: true });\n });\n\n app.openapi(manifestRoute, 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 { files } = c.req.valid(\"json\");\n const manifest: Record<string, string> = {};\n for (const f of files) {\n manifest[f.rel] = f.hash;\n }\n await getStore().storage.write(\n `${project.id}/builds/${build.id}/storybook/manifest.json`,\n Buffer.from(JSON.stringify(manifest)),\n );\n // Upsert content_refs for each hash\n for (const f of files) {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, f.hash);\n // oxlint-disable-next-line unicorn/no-if-else -- upsert is clearer as if/else\n if (!existing) {\n await getStore().db.insert(contentRefs, {\n hash: f.hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n } else {\n await getStore().db.update(contentRefs, f.hash, {\n lastSeenAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // The files are stored content-addressed: nothing to extract, so publishing is complete.\n await getStore().publish?.completer.complete(build.id, { ok: true });\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json({ ok: true });\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).resetStatus(build.id, \"pending\");\n await getStore().enqueueCapture?.(build.id, c.get(\"requestId\"));\n hxRefresh(c);\n flashHx(c, \"Capture queued\");\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 registerBuildReviews(app);\n registerBuildChangeset(app);\n registerBuildCancel(app);\n registerAttempts(app);\n}\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"List builds\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema.array() } },\n description: \"List builds for a project\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst buildCreatedSchema = z\n .object({\n build: buildSchema,\n uploadUrl: z.string(),\n baselineSha: z.string().nullable(),\n testing: z.boolean().openapi({\n description:\n \"False when the server has no `testing` option: the build is published but never captured or compared.\",\n }),\n })\n .openapi(\"BuildCreated\");\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Create a build\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: {\n content: {\n \"application/json\": { schema: buildCreateJsonSchema },\n },\n },\n },\n responses: {\n 202: {\n content: {\n \"application/json\": {\n schema: buildCreatedSchema,\n },\n },\n description:\n \"Build created. PUT the Storybook zip to uploadUrl to publish it (and queue capture when testing is on).\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst uploadZipRoute = createRoute({\n method: \"put\",\n tags: [\"Builds\"],\n summary: \"Upload the Storybook zip\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/zip\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/zip\": {\n schema: z.string().openapi({ type: \"string\", format: \"binary\" }),\n },\n },\n },\n },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description:\n \"Zip stored. A zip up to maxInlineUnzipSize (default 50 MB) is extracted inline in this request; a larger one is published by a background job. With testing off, the build becomes approved when publishing finishes on the project's default branch (failed if it cannot be published) and stays pending on other branches; with testing on, capture sets the status.\",\n },\n 422: {\n content: { \"text/plain\": { schema: z.string() } },\n description:\n \"The inline zip is invalid, unsafe, or exceeds the extraction limits. The build ends `failed`; upload again to create a new build.\",\n },\n 409: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"The build already failed (for example a rejected zip); create a new build\",\n },\n 413: {\n content: {\n \"application/json\": {\n schema: z.object({ message: z.string() }).openapi(\"Error\"),\n },\n },\n description:\n \"Upload exceeds the configured size limit, or the zip is over the inline limit and this host (serverless) runs no publish consumer: upload file by file instead (POST /dedup, PUT /content, POST /manifest)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst dedupHashesSchema = z.object({ hashes: z.array(z.string()) });\nconst dedupNeededSchema = z.object({ needed: z.array(z.string()) });\nconst dedupRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Check which files need uploading\",\n description:\n \"File-by-file upload, step 1 (dedup, content, manifest): needs no zip extraction and works on any host, serverless included. Send the content hashes; the response lists those the server does not hold yet.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/dedup\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: dedupHashesSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: dedupNeededSchema } },\n description: \"Hashes needed for upload\",\n },\n ...notFoundResponse,\n },\n});\n\nconst contentUploadRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Upload build content\",\n description:\n \"File-by-file upload, step 2: store the missing files content-addressed (multipart, or one file with an X-Content-Hash header).\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/content\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: z.any() } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Content uploaded\",\n },\n ...notFoundResponse,\n },\n});\n\nconst manifestFileSchema = z.object({ rel: z.string(), hash: z.string(), size: z.number() });\nconst manifestRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Store the build manifest\",\n description:\n \"File-by-file upload, step 3: store the path-to-hash manifest. This completes publishing. With testing off the build becomes approved when it is on the project's default branch and stays pending on other branches; with testing on, a capture is queued and sets the status.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/manifest\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/json\": {\n schema: z.object({ files: z.array(manifestFileSchema) }),\n },\n },\n },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Manifest stored\",\n },\n ...notFoundResponse,\n },\n});\n\nconst affectedPayloadSchema = z.object({\n baselineSha: z.string().nullable(),\n changedFiles: z.array(z.string()).max(20_000),\n affectedImportPaths: z.array(z.string()).max(20_000).nullable(),\n});\nconst affectedRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the affected stories\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/affected\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: affectedPayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Affected set recorded\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst usagePayloadSchema = z.object({\n usage: z\n .array(\n z.object({\n storyImportPath: z.string().min(1).max(1024),\n packageName: z.string().min(1).max(214),\n modulePath: z.string().min(1).max(2048),\n version: z.string().max(128).nullable().optional(),\n }),\n )\n .max(20_000),\n});\nconst usageRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the dependency packages each story uses\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/usage\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: usagePayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ stored: z.number() }) } },\n description: \"Usage recorded (replaces any earlier report for the build)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"Get a build\",\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 tags: [\"Builds\"],\n summary: \"Retry capture\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Build reset to pending and capture re-queued (new attempt)\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n tags: [\"Builds\"],\n summary: \"Delete a build\",\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"],"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 { DEFAULT_MAX_UPLOAD_BYTES } from \"@storyshelf/core/config\";\nimport { BuildModel, PackageUsageModel } from \"@storyshelf/core/models\";\nimport {\n CONTENT_HASH_PATTERN,\n contentKey,\n isContentHash,\n storybookZipPath,\n} from \"@storyshelf/core/utils\";\nimport { HTTPException } from \"hono/http-exception\";\nimport { createHash } from \"node:crypto\";\nimport type { ShelfRouter } from \"../app-types.ts\";\nimport { getStore } from \"../store.ts\";\nimport { registerAttempts } from \"./attempts.ts\";\nimport {\n VIEW_ROLES,\n DEVELOPER_ROLES,\n APPROVER_ROLES,\n buildCreateJsonSchema,\n buildListQuery,\n buildForProject,\n createBuildRecord,\n storeUploadStream,\n} from \"./builds.handlers.ts\";\nimport { discardZip, planUpload, publishUpload } from \"./publish-upload.ts\";\n\n/** Decide how the stored zip is published; a zip this host cannot publish is removed and refused. */\nasync function planOrDiscard(\n zipBytes: number,\n projectId: string,\n buildId: string,\n): Promise<\"inline\" | \"job\"> {\n try {\n return planUpload(zipBytes);\n } catch (error) {\n await discardZip(projectId, buildId);\n throw error;\n }\n}\n\nasync function upsertContentRef(hash: string): Promise<void> {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, hash);\n if (existing) {\n await getStore().db.update(contentRefs, hash, {\n refCount: (existing as { refCount: number }).refCount + 1,\n lastSeenAt: now,\n });\n } else {\n await getStore().db.insert(contentRefs, {\n hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist on old DB\n }\n}\nimport { registerBuildCancel } from \"./build-cancel.ts\";\nimport { registerBuildChangeset } from \"./build-changeset.ts\";\nimport { registerBuildReviews } from \"./build-reviews.ts\";\nimport { registerComments } from \"./comments.ts\";\nimport { flashHx } from \"./flash.ts\";\nimport { resolveAuthorizedProject } from \"./helpers.ts\";\nimport { hxRefresh } from \"./htmx.ts\";\nimport {\n badRequest,\n buildSchema,\n forbidden as forbiddenResponse,\n notFound as notFoundResponse,\n unauthorized,\n} from \"./schemas.ts\";\nimport { registerSnapshots } from \"./snapshots.ts\";\n/** Register the build list, upload, fetch, retry, and delete endpoints. */\nexport function registerBuilds(app: ShelfRouter): 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, {\n status,\n branch: branch ?? undefined,\n labelKey: labelKey ?? undefined,\n labelValue: labelValue ?? undefined,\n });\n return c.json(await builds);\n });\n\n app.openapi(createBuildRoute, async (c) => {\n const project = await resolveAuthorizedProject(\n c,\n c.req.valid(\"param\").slug,\n ...DEVELOPER_ROLES,\n );\n const build = await createBuildRecord(project, c.req.valid(\"json\"));\n const uploadUrl = `/api/v1/projects/${project.slug}/builds/${build.id}/zip`;\n return c.json(\n { build, uploadUrl, baselineSha: build.baselineSha, testing: getStore().testing },\n 202,\n );\n });\n\n app.openapi(affectedRoute, 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 { baselineSha, changedFiles, affectedImportPaths } = c.req.valid(\"json\");\n const updated = await new BuildModel(getStore().db).setAffected(build.id, {\n baselineSha,\n changedFiles,\n affectedImportPaths,\n });\n return c.json(updated);\n });\n\n app.openapi(usageRoute, 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 { usage } = c.req.valid(\"json\");\n const stored = await new PackageUsageModel(getStore().db).replaceForBuild(\n project.id,\n build.id,\n usage,\n );\n return c.json({ stored });\n });\n\n app.openapi(uploadZipRoute, 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 if (build.status === \"failed\") {\n throw new HTTPException(409, {\n message: `Build ${build.id} failed; run storyshelf upload again to create a new build`,\n });\n }\n const maxBytes = getStore().config.maxUploadBytes ?? DEFAULT_MAX_UPLOAD_BYTES;\n const zipBytes = await storeUploadStream(\n getStore().storage,\n storybookZipPath(project.id, build.id),\n c.req.raw.body,\n maxBytes,\n );\n const route = await planOrDiscard(zipBytes, project.id, build.id);\n const reqId = c.get(\"requestId\");\n const published = await publishUpload(route, build, reqId);\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json(published, 202);\n });\n\n app.openapi(dedupRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const { hashes } = c.req.valid(\"json\");\n const needed: string[] = [];\n for (const hash of hashes) {\n if (!(await getStore().storage.exists(contentKey(hash)))) {\n needed.push(hash);\n }\n }\n return c.json({ needed });\n });\n\n // oxlint-disable eslint(max-depth) -- dedup upload handler is intentionally nested for batch handling\n app.openapi(contentUploadRoute, async (c) => {\n const { slug, buildId } = c.req.valid(\"param\");\n const project = await resolveAuthorizedProject(c, slug, ...DEVELOPER_ROLES);\n await buildForProject(project.id, buildId);\n const contentType = c.req.header(\"content-type\") ?? \"\";\n if (contentType.includes(\"multipart/\")) {\n // Every file is checked before anything is written, so a bad one leaves no partial upload.\n const files = await verifiedUploads(await c.req.formData());\n for (const { hash, buffer } of files) {\n // oxlint-disable-next-line no-await-in-loop -- one write at a time keeps the order stable\n await getStore().storage.write(contentKey(hash), buffer);\n // oxlint-disable-next-line no-await-in-loop -- see above\n await upsertContentRef(hash);\n }\n } else {\n // Single content upload via PUT with hash header\n const hash = c.req.header(\"x-content-hash\") ?? \"\";\n refuseInvalidHash(hash);\n const buffer = Buffer.from(await c.req.arrayBuffer());\n refuseMismatch(hash, buffer);\n await getStore().storage.write(contentKey(hash), buffer);\n }\n return c.json({ ok: true });\n });\n\n app.openapi(manifestRoute, 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 { files } = c.req.valid(\"json\");\n const manifest: Record<string, string> = {};\n for (const f of files) {\n manifest[f.rel] = f.hash;\n }\n await getStore().storage.write(\n `${project.id}/builds/${build.id}/storybook/manifest.json`,\n Buffer.from(JSON.stringify(manifest)),\n );\n // Upsert content_refs for each hash\n for (const f of files) {\n try {\n const contentRefs = getStore().db.tables.contentRefs;\n const now = new Date().toISOString();\n const existing = await getStore().db.get(contentRefs, f.hash);\n // oxlint-disable-next-line unicorn/no-if-else -- upsert is clearer as if/else\n if (!existing) {\n await getStore().db.insert(contentRefs, {\n hash: f.hash,\n refCount: 1,\n lastSeenAt: now,\n createdAt: now,\n });\n } else {\n await getStore().db.update(contentRefs, f.hash, {\n lastSeenAt: now,\n });\n }\n } catch {\n // ignore — content_refs may not exist\n }\n }\n // The files are stored content-addressed: nothing to extract, so publishing is complete.\n await getStore().publish?.completer.complete(build.id, { ok: true });\n const reqId = c.get(\"requestId\");\n await getStore().enqueueCapture?.(build.id, reqId);\n return c.json({ ok: true });\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).resetStatus(build.id, \"pending\");\n await getStore().enqueueCapture?.(build.id, c.get(\"requestId\"));\n hxRefresh(c);\n flashHx(c, \"Capture queued\");\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 registerBuildReviews(app);\n registerBuildChangeset(app);\n registerBuildCancel(app);\n registerAttempts(app);\n}\n\nconst listBuildsRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"List builds\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: { params: z.object({ slug: z.string() }), query: buildListQuery },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema.array() } },\n description: \"List builds for a project\",\n },\n ...notFoundResponse,\n ...unauthorized,\n },\n});\n\nconst buildCreatedSchema = z\n .object({\n build: buildSchema,\n uploadUrl: z.string(),\n baselineSha: z.string().nullable(),\n testing: z.boolean().openapi({\n description:\n \"False when the server has no `testing` option: the build is published but never captured or compared.\",\n }),\n })\n .openapi(\"BuildCreated\");\n\nconst createBuildRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Create a build\",\n path: \"/api/v1/projects/{slug}/builds\",\n request: {\n params: z.object({ slug: z.string() }),\n body: {\n content: {\n \"application/json\": { schema: buildCreateJsonSchema },\n },\n },\n },\n responses: {\n 202: {\n content: {\n \"application/json\": {\n schema: buildCreatedSchema,\n },\n },\n description:\n \"Build created. PUT the Storybook zip to uploadUrl to publish it (and queue capture when testing is on).\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst uploadZipRoute = createRoute({\n method: \"put\",\n tags: [\"Builds\"],\n summary: \"Upload the Storybook zip\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/zip\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/zip\": {\n schema: z.string().openapi({ type: \"string\", format: \"binary\" }),\n },\n },\n },\n },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description:\n \"Zip stored. A zip up to maxInlineUnzipSize (default 50 MB) is extracted inline in this request; a larger one is published by a background job. With testing off, the build becomes approved when publishing finishes on the project's default branch (failed if it cannot be published) and stays pending on other branches; with testing on, capture sets the status.\",\n },\n 422: {\n content: { \"text/plain\": { schema: z.string() } },\n description:\n \"The inline zip is invalid, unsafe, or exceeds the extraction limits. The build ends `failed`; upload again to create a new build.\",\n },\n 409: {\n content: { \"application/json\": { schema: z.object({ message: z.string() }) } },\n description: \"The build already failed (for example a rejected zip); create a new build\",\n },\n 413: {\n content: {\n \"application/json\": {\n schema: z.object({ message: z.string() }).openapi(\"Error\"),\n },\n },\n description:\n \"Upload exceeds the configured size limit, or the zip is over the inline limit and this host (serverless) runs no publish consumer: upload file by file instead (POST /dedup, PUT /content, POST /manifest)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\n/** Answer 400 and log only the reason: the refused value is never logged or echoed. */\nfunction refuseUpload(reason: string, message: string): never {\n getStore().logger.warn({ reason }, \"content upload refused\");\n throw new HTTPException(400, { message });\n}\n\n/** Answer 400 for a hash that is not a SHA-256 hex digest. */\nfunction refuseInvalidHash(hash: string): void {\n if (!isContentHash(hash)) refuseUpload(\"invalid content hash\", \"Invalid content hash\");\n}\n\n/**\n * The shared `content/<hash>` blob belongs to no project, so its bytes must be what its name says:\n * an upload whose SHA-256 differs from its hash is refused, or one project could plant bytes under\n * a hash another project's manifest names.\n */\nfunction refuseMismatch(hash: string, buffer: Buffer): void {\n const digest = createHash(\"sha256\").update(buffer).digest(\"hex\");\n if (digest !== hash)\n refuseUpload(\"content does not match its hash\", \"Content does not match its hash\");\n}\n\n/** The files of a multipart content upload; each key is a valid hash and names its own bytes. */\nasync function verifiedUploads(form: FormData): Promise<{ hash: string; buffer: Buffer }[]> {\n return await Promise.all(\n [...form.entries()].map(async ([hash, value]) => {\n if (!(value instanceof File)) refuseUpload(\"not a file\", \"Only files can be uploaded\");\n refuseInvalidHash(hash);\n const buffer = Buffer.from(await value.arrayBuffer());\n refuseMismatch(hash, buffer);\n return { hash, buffer };\n }),\n );\n}\n\nconst contentHashSchema = z.string().regex(CONTENT_HASH_PATTERN, \"Invalid content hash\");\nconst dedupHashesSchema = z.object({ hashes: z.array(contentHashSchema) });\nconst dedupNeededSchema = z.object({ needed: z.array(z.string()) });\nconst dedupRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Check which files need uploading\",\n description:\n \"File-by-file upload, step 1 (dedup, content, manifest): needs no zip extraction and works on any host, serverless included. Send the content hashes; the response lists those the server does not hold yet.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/dedup\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: dedupHashesSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: dedupNeededSchema } },\n description: \"Hashes needed for upload\",\n },\n ...notFoundResponse,\n },\n});\n\nconst contentUploadRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Upload build content\",\n description:\n \"File-by-file upload, step 2: store the missing files content-addressed (multipart, or one file with an X-Content-Hash header).\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/content\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"multipart/form-data\": { schema: z.any() } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Content uploaded\",\n },\n ...notFoundResponse,\n },\n});\n\nconst manifestFileSchema = z.object({ rel: z.string(), hash: contentHashSchema, size: z.number() });\nconst manifestRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Store the build manifest\",\n description:\n \"File-by-file upload, step 3: store the path-to-hash manifest. This completes publishing. With testing off the build becomes approved when it is on the project's default branch and stays pending on other branches; with testing on, a capture is queued and sets the status.\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/manifest\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: {\n content: {\n \"application/json\": {\n schema: z.object({ files: z.array(manifestFileSchema) }),\n },\n },\n },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ ok: z.boolean() }) } },\n description: \"Manifest stored\",\n },\n ...notFoundResponse,\n },\n});\n\nconst affectedPayloadSchema = z.object({\n baselineSha: z.string().nullable(),\n changedFiles: z.array(z.string()).max(20_000),\n affectedImportPaths: z.array(z.string()).max(20_000).nullable(),\n});\nconst affectedRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the affected stories\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/affected\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: affectedPayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Affected set recorded\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst usagePayloadSchema = z.object({\n usage: z\n .array(\n z.object({\n storyImportPath: z.string().min(1).max(1024),\n packageName: z.string().min(1).max(214),\n modulePath: z.string().min(1).max(2048),\n version: z.string().max(128).nullable().optional(),\n }),\n )\n .max(20_000),\n});\nconst usageRoute = createRoute({\n method: \"post\",\n tags: [\"Builds\"],\n summary: \"Record the dependency packages each story uses\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/usage\",\n request: {\n params: z.object({ slug: z.string(), buildId: z.string() }),\n body: { content: { \"application/json\": { schema: usagePayloadSchema } } },\n },\n responses: {\n 200: {\n content: { \"application/json\": { schema: z.object({ stored: z.number() }) } },\n description: \"Usage recorded (replaces any earlier report for the build)\",\n },\n ...badRequest,\n ...forbiddenResponse,\n ...notFoundResponse,\n },\n});\n\nconst getBuildRoute = createRoute({\n method: \"get\",\n tags: [\"Builds\"],\n summary: \"Get a build\",\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 tags: [\"Builds\"],\n summary: \"Retry capture\",\n path: \"/api/v1/projects/{slug}/builds/{buildId}/retry\",\n request: { params: z.object({ slug: z.string(), buildId: z.string() }) },\n responses: {\n 202: {\n content: { \"application/json\": { schema: buildSchema } },\n description: \"Build reset to pending and capture re-queued (new attempt)\",\n },\n ...notFoundResponse,\n },\n});\n\nconst deleteBuildRoute = createRoute({\n method: \"delete\",\n tags: [\"Builds\"],\n summary: \"Delete a build\",\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"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
-
import { publishedGzipPath } from "@storyshelf/core/utils";
|
|
4
|
+
import { contentKey, publishedGzipPath } from "@storyshelf/core/utils";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { gunzip, gzip } from "node:zlib";
|
|
@@ -84,7 +84,7 @@ function schedule(storage, state, hash, original) {
|
|
|
84
84
|
}
|
|
85
85
|
async function generate(storage, hash, original) {
|
|
86
86
|
if (createHash("sha256").update(original).digest("hex") !== hash) return;
|
|
87
|
-
const source =
|
|
87
|
+
const source = contentKey(hash);
|
|
88
88
|
if (!await storage.exists(source)) return;
|
|
89
89
|
const compressed = await compress(original);
|
|
90
90
|
if (compressed.length >= original.length || !await storage.exists(source)) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"published-compression.mjs","names":[],"sources":["../../src/routers/published-compression.ts"],"sourcesContent":["/** Disposable, content-addressed gzip variants of authorized published files. */\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { publishedGzipPath } from \"@storyshelf/core/utils\";\nimport { createHash } from \"node:crypto\";\nimport { promisify } from \"node:util\";\nimport { gunzip, gzip } from \"node:zlib\";\n\nconst compress = promisify(gzip);\nconst decompress = promisify(gunzip);\nconst MAX_FILE_BYTES = 8 * 1024 * 1024;\nconst MAX_JOBS = 2;\nconst MAX_READS = 4;\nconst READ_TIMEOUT_MS = 250;\nconst states = new WeakMap<StorageAdapter, State>();\n\ninterface State {\n jobs: Map<string, Promise<void>>;\n reads: number;\n}\n\n/** Gzip is chosen only if explicitly accepted, or by a nonzero wildcard. */\nexport function acceptsGzip(header?: string): boolean {\n if (!header) return false;\n const entries = header.split(\",\").map((part) => part.trim().split(\";\"));\n const entry =\n entries.find(([name]) => name?.trim().toLowerCase() === \"gzip\") ??\n entries.find(([name]) => name?.trim() === \"*\");\n if (!entry) return false;\n const quality = entry.slice(1).find((part) => /^\\s*q\\s*=/iu.test(part));\n if (quality === undefined) return true;\n const value = Number(quality.split(\"=\")[1]);\n return Number.isFinite(value) && value > 0 && value <= 1;\n}\n\n/** Return a verified existing variant, or the original while a bounded background job runs. */\nexport async function publishedGzip(\n storage: StorageAdapter,\n hash: string | undefined,\n original: Buffer,\n mime: string,\n): Promise<Buffer | undefined> {\n if (typeof hash !== \"string\" || !eligible(hash, original, mime)) return undefined;\n const state = stateOf(storage);\n if (state.reads >= MAX_READS) return undefined;\n state.reads += 1;\n const read = findVariant(storage, hash, original).finally(() => {\n state.reads -= 1;\n });\n const found = await withDeadline(read);\n if (found === \"timeout\") return undefined;\n if (found) return found;\n schedule(storage, state, hash, original);\n return undefined;\n}\n\nfunction eligible(hash: string | undefined, original: Buffer, mime: string): boolean {\n return (\n typeof hash === \"string\" &&\n /^[a-f0-9]{64}$/u.test(hash) &&\n original.length > 0 &&\n original.length <= MAX_FILE_BYTES &&\n /^(?:text\\/|application\\/(?:javascript|json|xml|wasm)|image\\/svg\\+xml)/iu.test(mime)\n );\n}\n\nfunction stateOf(storage: StorageAdapter): State {\n let state = states.get(storage);\n if (!state) {\n state = { jobs: new Map(), reads: 0 };\n states.set(storage, state);\n }\n return state;\n}\n\nasync function findVariant(\n storage: StorageAdapter,\n hash: string,\n original: Buffer,\n): Promise<Buffer | undefined> {\n try {\n const path = publishedGzipPath(hash);\n if (!(await storage.exists(path))) return undefined;\n const bytes = await readBounded(storage, path, original.length);\n if (!bytes || bytes.length === 0) return undefined;\n const decoded = await decompress(bytes, { maxOutputLength: original.length });\n return decoded.equals(original) ? bytes : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Do not allocate an unbounded buffer when a damaged storage object is oversized. */\nasync function readBounded(\n storage: StorageAdapter,\n path: string,\n limit: number,\n): Promise<Buffer | undefined> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of await storage.readStream(path)) {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);\n size += bytes.length;\n if (size >= limit) {\n return undefined;\n }\n chunks.push(bytes);\n }\n return Buffer.concat(chunks, size);\n}\n\n/** No queue: at capacity a future request may try again. */\nfunction schedule(storage: StorageAdapter, state: State, hash: string, original: Buffer): void {\n if (state.jobs.has(hash) || state.jobs.size >= MAX_JOBS) return;\n const job = generate(storage, hash, original)\n .catch(() => {})\n .finally(() => {\n state.jobs.delete(hash);\n });\n state.jobs.set(hash, job);\n}\n\nasync function generate(storage: StorageAdapter, hash: string, original: Buffer): Promise<void> {\n if (createHash(\"sha256\").update(original).digest(\"hex\") !== hash) return;\n const source =
|
|
1
|
+
{"version":3,"file":"published-compression.mjs","names":[],"sources":["../../src/routers/published-compression.ts"],"sourcesContent":["/** Disposable, content-addressed gzip variants of authorized published files. */\nimport type { StorageAdapter } from \"@storyshelf/core/adapter/storage\";\nimport { contentKey, publishedGzipPath } from \"@storyshelf/core/utils\";\nimport { createHash } from \"node:crypto\";\nimport { promisify } from \"node:util\";\nimport { gunzip, gzip } from \"node:zlib\";\n\nconst compress = promisify(gzip);\nconst decompress = promisify(gunzip);\nconst MAX_FILE_BYTES = 8 * 1024 * 1024;\nconst MAX_JOBS = 2;\nconst MAX_READS = 4;\nconst READ_TIMEOUT_MS = 250;\nconst states = new WeakMap<StorageAdapter, State>();\n\ninterface State {\n jobs: Map<string, Promise<void>>;\n reads: number;\n}\n\n/** Gzip is chosen only if explicitly accepted, or by a nonzero wildcard. */\nexport function acceptsGzip(header?: string): boolean {\n if (!header) return false;\n const entries = header.split(\",\").map((part) => part.trim().split(\";\"));\n const entry =\n entries.find(([name]) => name?.trim().toLowerCase() === \"gzip\") ??\n entries.find(([name]) => name?.trim() === \"*\");\n if (!entry) return false;\n const quality = entry.slice(1).find((part) => /^\\s*q\\s*=/iu.test(part));\n if (quality === undefined) return true;\n const value = Number(quality.split(\"=\")[1]);\n return Number.isFinite(value) && value > 0 && value <= 1;\n}\n\n/** Return a verified existing variant, or the original while a bounded background job runs. */\nexport async function publishedGzip(\n storage: StorageAdapter,\n hash: string | undefined,\n original: Buffer,\n mime: string,\n): Promise<Buffer | undefined> {\n if (typeof hash !== \"string\" || !eligible(hash, original, mime)) return undefined;\n const state = stateOf(storage);\n if (state.reads >= MAX_READS) return undefined;\n state.reads += 1;\n const read = findVariant(storage, hash, original).finally(() => {\n state.reads -= 1;\n });\n const found = await withDeadline(read);\n if (found === \"timeout\") return undefined;\n if (found) return found;\n schedule(storage, state, hash, original);\n return undefined;\n}\n\nfunction eligible(hash: string | undefined, original: Buffer, mime: string): boolean {\n return (\n typeof hash === \"string\" &&\n /^[a-f0-9]{64}$/u.test(hash) &&\n original.length > 0 &&\n original.length <= MAX_FILE_BYTES &&\n /^(?:text\\/|application\\/(?:javascript|json|xml|wasm)|image\\/svg\\+xml)/iu.test(mime)\n );\n}\n\nfunction stateOf(storage: StorageAdapter): State {\n let state = states.get(storage);\n if (!state) {\n state = { jobs: new Map(), reads: 0 };\n states.set(storage, state);\n }\n return state;\n}\n\nasync function findVariant(\n storage: StorageAdapter,\n hash: string,\n original: Buffer,\n): Promise<Buffer | undefined> {\n try {\n const path = publishedGzipPath(hash);\n if (!(await storage.exists(path))) return undefined;\n const bytes = await readBounded(storage, path, original.length);\n if (!bytes || bytes.length === 0) return undefined;\n const decoded = await decompress(bytes, { maxOutputLength: original.length });\n return decoded.equals(original) ? bytes : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Do not allocate an unbounded buffer when a damaged storage object is oversized. */\nasync function readBounded(\n storage: StorageAdapter,\n path: string,\n limit: number,\n): Promise<Buffer | undefined> {\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of await storage.readStream(path)) {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);\n size += bytes.length;\n if (size >= limit) {\n return undefined;\n }\n chunks.push(bytes);\n }\n return Buffer.concat(chunks, size);\n}\n\n/** No queue: at capacity a future request may try again. */\nfunction schedule(storage: StorageAdapter, state: State, hash: string, original: Buffer): void {\n if (state.jobs.has(hash) || state.jobs.size >= MAX_JOBS) return;\n const job = generate(storage, hash, original)\n .catch(() => {})\n .finally(() => {\n state.jobs.delete(hash);\n });\n state.jobs.set(hash, job);\n}\n\nasync function generate(storage: StorageAdapter, hash: string, original: Buffer): Promise<void> {\n if (createHash(\"sha256\").update(original).digest(\"hex\") !== hash) return;\n const source = contentKey(hash);\n if (!(await storage.exists(source))) return;\n const compressed = await compress(original);\n if (compressed.length >= original.length || !(await storage.exists(source))) return;\n const path = publishedGzipPath(hash);\n await storage.write(path, compressed);\n // A purge can remove the original while compression is in flight.\n if (!(await storage.exists(source))) await storage.delete(path);\n}\n\nasync function withDeadline<T>(promise: Promise<T>): Promise<T | \"timeout\"> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => {\n resolve(\"timeout\");\n }, READ_TIMEOUT_MS);\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n"],"mappings":";;;;;;;;AAOA,MAAM,WAAW,UAAU,IAAI;AAC/B,MAAM,aAAa,UAAU,MAAM;AACnC,MAAM,iBAAiB;AACvB,MAAM,WAAW;AACjB,MAAM,YAAY;AAClB,MAAM,kBAAkB;AACxB,MAAM,yBAAS,IAAI,QAA+B;;AAQlD,SAAgB,YAAY,QAA0B;CACpD,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,UAAU,OAAO,MAAM,GAAG,CAAC,CAAC,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,MAAM,GAAG,CAAC;CACtE,MAAM,QACJ,QAAQ,MAAM,CAAC,UAAU,MAAM,KAAK,CAAC,CAAC,YAAY,MAAM,MAAM,KAC9D,QAAQ,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,GAAG;CAC/C,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,UAAU,MAAM,MAAM,CAAC,CAAC,CAAC,MAAM,SAAS,cAAc,KAAK,IAAI,CAAC;CACtE,IAAI,YAAY,KAAA,GAAW,OAAO;CAClC,MAAM,QAAQ,OAAO,QAAQ,MAAM,GAAG,CAAC,CAAC,EAAE;CAC1C,OAAO,OAAO,SAAS,KAAK,KAAK,QAAQ,KAAK,SAAS;AACzD;;AAGA,eAAsB,cACpB,SACA,MACA,UACA,MAC6B;CAC7B,IAAI,OAAO,SAAS,YAAY,CAAC,SAAS,MAAM,UAAU,IAAI,GAAG,OAAO,KAAA;CACxE,MAAM,QAAQ,QAAQ,OAAO;CAC7B,IAAI,MAAM,SAAS,WAAW,OAAO,KAAA;CACrC,MAAM,SAAS;CAIf,MAAM,QAAQ,MAAM,aAHP,YAAY,SAAS,MAAM,QAAQ,CAAC,CAAC,cAAc;EAC9D,MAAM,SAAS;CACjB,CACoC,CAAC;CACrC,IAAI,UAAU,WAAW,OAAO,KAAA;CAChC,IAAI,OAAO,OAAO;CAClB,SAAS,SAAS,OAAO,MAAM,QAAQ;AAEzC;AAEA,SAAS,SAAS,MAA0B,UAAkB,MAAuB;CACnF,OACE,OAAO,SAAS,YAChB,kBAAkB,KAAK,IAAI,KAC3B,SAAS,SAAS,KAClB,SAAS,UAAU,kBACnB,0EAA0E,KAAK,IAAI;AAEvF;AAEA,SAAS,QAAQ,SAAgC;CAC/C,IAAI,QAAQ,OAAO,IAAI,OAAO;CAC9B,IAAI,CAAC,OAAO;EACV,QAAQ;GAAE,sBAAM,IAAI,IAAI;GAAG,OAAO;EAAE;EACpC,OAAO,IAAI,SAAS,KAAK;CAC3B;CACA,OAAO;AACT;AAEA,eAAe,YACb,SACA,MACA,UAC6B;CAC7B,IAAI;EACF,MAAM,OAAO,kBAAkB,IAAI;EACnC,IAAI,CAAE,MAAM,QAAQ,OAAO,IAAI,GAAI,OAAO,KAAA;EAC1C,MAAM,QAAQ,MAAM,YAAY,SAAS,MAAM,SAAS,MAAM;EAC9D,IAAI,CAAC,SAAS,MAAM,WAAW,GAAG,OAAO,KAAA;EAEzC,QAAO,MADe,WAAW,OAAO,EAAE,iBAAiB,SAAS,OAAO,CAAC,EAAA,CAC7D,OAAO,QAAQ,IAAI,QAAQ,KAAA;CAC5C,QAAQ;EACN;CACF;AACF;;AAGA,eAAe,YACb,SACA,MACA,OAC6B;CAC7B,MAAM,SAAmB,CAAC;CAC1B,IAAI,OAAO;CACX,WAAW,MAAM,SAAS,MAAM,QAAQ,WAAW,IAAI,GAAG;EACxD,MAAM,QAAQ,OAAO,SAAS,KAAK,IAAI,QAAQ,OAAO,KAAK,KAAmB;EAC9E,QAAQ,MAAM;EACd,IAAI,QAAQ,OACV;EAEF,OAAO,KAAK,KAAK;CACnB;CACA,OAAO,OAAO,OAAO,QAAQ,IAAI;AACnC;;AAGA,SAAS,SAAS,SAAyB,OAAc,MAAc,UAAwB;CAC7F,IAAI,MAAM,KAAK,IAAI,IAAI,KAAK,MAAM,KAAK,QAAQ,UAAU;CACzD,MAAM,MAAM,SAAS,SAAS,MAAM,QAAQ,CAAC,CAC1C,YAAY,CAAC,CAAC,CAAC,CACf,cAAc;EACb,MAAM,KAAK,OAAO,IAAI;CACxB,CAAC;CACH,MAAM,KAAK,IAAI,MAAM,GAAG;AAC1B;AAEA,eAAe,SAAS,SAAyB,MAAc,UAAiC;CAC9F,IAAI,WAAW,QAAQ,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC,OAAO,KAAK,MAAM,MAAM;CAClE,MAAM,SAAS,WAAW,IAAI;CAC9B,IAAI,CAAE,MAAM,QAAQ,OAAO,MAAM,GAAI;CACrC,MAAM,aAAa,MAAM,SAAS,QAAQ;CAC1C,IAAI,WAAW,UAAU,SAAS,UAAU,CAAE,MAAM,QAAQ,OAAO,MAAM,GAAI;CAC7E,MAAM,OAAO,kBAAkB,IAAI;CACnC,MAAM,QAAQ,MAAM,MAAM,UAAU;CAEpC,IAAI,CAAE,MAAM,QAAQ,OAAO,MAAM,GAAI,MAAM,QAAQ,OAAO,IAAI;AAChE;AAEA,eAAe,aAAgB,SAA6C;CAC1E,IAAI;CACJ,IAAI;EACF,OAAO,MAAM,QAAQ,KAAK,CACxB,SACA,IAAI,SAAoB,YAAY;GAClC,QAAQ,iBAAiB;IACvB,QAAQ,SAAS;GACnB,GAAG,eAAe;EACpB,CAAC,CACH,CAAC;CACH,UAAU;EACR,IAAI,OAAO,aAAa,KAAK;CAC/B;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyshelf/app",
|
|
3
|
-
"version": "0.10.0-next-
|
|
3
|
+
"version": "0.10.0-next-e614c0a",
|
|
4
4
|
"description": "StoryShelf app: Hono app, API routes, and server-rendered UI over @storyshelf/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chromatic-alternative",
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"@hono/swagger-ui": "^0.6.1",
|
|
63
63
|
"@hono/zod-openapi": "^1.1.5",
|
|
64
|
-
"@storyshelf/core": "0.10.0-next-
|
|
65
|
-
"@storyshelf/observability": "0.10.0-next-
|
|
64
|
+
"@storyshelf/core": "0.10.0-next-e614c0a",
|
|
65
|
+
"@storyshelf/observability": "0.10.0-next-e614c0a",
|
|
66
66
|
"hono": "^4.11.0",
|
|
67
67
|
"zod": "^4.6.7"
|
|
68
68
|
},
|
|
@@ -72,9 +72,9 @@
|
|
|
72
72
|
"@opentelemetry/core": "^2.11.0",
|
|
73
73
|
"@opentelemetry/sdk-trace-base": "^2.11.0",
|
|
74
74
|
"@storybook/html-vite": "10.6.5",
|
|
75
|
-
"@storyshelf/auth": "0.10.0-next-
|
|
76
|
-
"@storyshelf/db-sqlite": "0.10.0-next-
|
|
77
|
-
"@storyshelf/design-figma": "0.10.0-next-
|
|
75
|
+
"@storyshelf/auth": "0.10.0-next-e614c0a",
|
|
76
|
+
"@storyshelf/db-sqlite": "0.10.0-next-e614c0a",
|
|
77
|
+
"@storyshelf/design-figma": "0.10.0-next-e614c0a",
|
|
78
78
|
"@types/node": "^26.0.0",
|
|
79
79
|
"@vitest/coverage-v8": "^4.0.0",
|
|
80
80
|
"adm-zip": "0.6.1",
|