@storyshelf/app 0.10.0-next-177e0f6 → 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
|
@@ -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/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":"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"}
|
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",
|