@ttt-productions/media-processing-core 0.8.15 → 0.8.17
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/audio/audio-processor.d.ts +2 -2
- package/dist/audio/audio-processor.d.ts.map +1 -1
- package/dist/audio/audio-processor.js +62 -48
- package/dist/audio/audio-processor.js.map +1 -1
- package/dist/audio/probe.d.ts +1 -1
- package/dist/audio/probe.d.ts.map +1 -1
- package/dist/audio/probe.js +6 -3
- package/dist/audio/probe.js.map +1 -1
- package/dist/duration/media-duration.d.ts +2 -2
- package/dist/duration/media-duration.d.ts.map +1 -1
- package/dist/duration/media-duration.js +23 -21
- package/dist/duration/media-duration.js.map +1 -1
- package/dist/image/formats.d.ts +2 -2
- package/dist/image/formats.js +25 -25
- package/dist/image/formats.js.map +1 -1
- package/dist/image/image-processor.d.ts +2 -2
- package/dist/image/image-processor.d.ts.map +1 -1
- package/dist/image/image-processor.js +47 -37
- package/dist/image/image-processor.js.map +1 -1
- package/dist/image/resize.d.ts +2 -2
- package/dist/image/resize.d.ts.map +1 -1
- package/dist/image/resize.js +13 -13
- package/dist/image/resize.js.map +1 -1
- package/dist/index.d.ts +19 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -16
- package/dist/index.js.map +1 -1
- package/dist/inspection/inspect-media.d.ts +13 -13
- package/dist/inspection/inspect-media.d.ts.map +1 -1
- package/dist/inspection/inspect-media.js +196 -140
- package/dist/inspection/inspect-media.js.map +1 -1
- package/dist/io/fs.d.ts +1 -1
- package/dist/io/fs.js +4 -4
- package/dist/io/fs.js.map +1 -1
- package/dist/io/local-input.d.ts +1 -1
- package/dist/io/local-input.d.ts.map +1 -1
- package/dist/io/local-input.js +15 -15
- package/dist/io/local-input.js.map +1 -1
- package/dist/io/types.d.ts.map +1 -1
- package/dist/io/types.js.map +1 -1
- package/dist/moderation/merge.d.ts +1 -1
- package/dist/moderation/merge.d.ts.map +1 -1
- package/dist/moderation/merge.js +1 -1
- package/dist/moderation/merge.js.map +1 -1
- package/dist/moderation/types.d.ts +3 -3
- package/dist/moderation/types.js.map +1 -1
- package/dist/process-media.d.ts +2 -3
- package/dist/process-media.d.ts.map +1 -1
- package/dist/process-media.js +9 -9
- package/dist/process-media.js.map +1 -1
- package/dist/processing/errors.d.ts.map +1 -1
- package/dist/processing/errors.js +1 -1
- package/dist/processing/errors.js.map +1 -1
- package/dist/processing/result.d.ts +2 -2
- package/dist/processing/result.js +1 -1
- package/dist/processing/result.js.map +1 -1
- package/dist/run-pipeline.d.ts +5 -5
- package/dist/run-pipeline.d.ts.map +1 -1
- package/dist/run-pipeline.js +70 -54
- package/dist/run-pipeline.js.map +1 -1
- package/dist/server/download-token.d.ts +14 -0
- package/dist/server/download-token.d.ts.map +1 -0
- package/dist/server/download-token.js +19 -0
- package/dist/server/download-token.js.map +1 -0
- package/dist/server/firebase-media-io.d.ts +2 -2
- package/dist/server/firebase-media-io.js +1 -1
- package/dist/server/firebase-media-io.js.map +1 -1
- package/dist/server/gcs-errors.js +2 -2
- package/dist/server/gcs-errors.js.map +1 -1
- package/dist/server/index.d.ts +7 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/pending-media.d.ts +2 -2
- package/dist/server/pending-media.d.ts.map +1 -1
- package/dist/server/pending-media.js +12 -6
- package/dist/server/pending-media.js.map +1 -1
- package/dist/server/r2-retry.d.ts +4 -4
- package/dist/server/r2-retry.d.ts.map +1 -1
- package/dist/server/r2-retry.js +48 -47
- package/dist/server/r2-retry.js.map +1 -1
- package/dist/server/staged-object.d.ts +2 -2
- package/dist/server/staged-object.d.ts.map +1 -1
- package/dist/server/staged-object.js +4 -4
- package/dist/server/staged-object.js.map +1 -1
- package/dist/server/storage-ops.d.ts +3 -5
- package/dist/server/storage-ops.d.ts.map +1 -1
- package/dist/server/storage-ops.js +101 -97
- package/dist/server/storage-ops.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/dist/utils/log.d.ts.map +1 -1
- package/dist/utils/log.js +3 -3
- package/dist/utils/log.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +1 -4
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/safe-path.js +8 -8
- package/dist/utils/safe-path.js.map +1 -1
- package/dist/validation/validate-duration.d.ts.map +1 -1
- package/dist/validation/validate-duration.js.map +1 -1
- package/dist/validation/validate-mime.d.ts.map +1 -1
- package/dist/validation/validate-mime.js.map +1 -1
- package/dist/validation/validate-size.d.ts.map +1 -1
- package/dist/validation/validate-size.js.map +1 -1
- package/dist/video/ffmpeg.d.ts +1 -1
- package/dist/video/ffmpeg.d.ts.map +1 -1
- package/dist/video/ffmpeg.js +51 -43
- package/dist/video/ffmpeg.js.map +1 -1
- package/dist/video/probe.d.ts +1 -1
- package/dist/video/probe.d.ts.map +1 -1
- package/dist/video/probe.js +10 -7
- package/dist/video/probe.js.map +1 -1
- package/dist/video/video-processor.d.ts +2 -2
- package/dist/video/video-processor.d.ts.map +1 -1
- package/dist/video/video-processor.js +123 -81
- package/dist/video/video-processor.js.map +1 -1
- package/dist/workspace/temp.js +4 -4
- package/dist/workspace/temp.js.map +1 -1
- package/package.json +2 -2
- package/dist/audio/index.d.ts +0 -3
- package/dist/audio/index.d.ts.map +0 -1
- package/dist/audio/index.js +0 -3
- package/dist/audio/index.js.map +0 -1
- package/dist/image/index.d.ts +0 -4
- package/dist/image/index.d.ts.map +0 -1
- package/dist/image/index.js +0 -4
- package/dist/image/index.js.map +0 -1
- package/dist/video/index.d.ts +0 -4
- package/dist/video/index.d.ts.map +0 -1
- package/dist/video/index.js +0 -4
- package/dist/video/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-ops.js","sourceRoot":"","sources":["../../src/server/storage-ops.ts"],"names":[],"mappings":"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","sourcesContent":["// Final-media object-store chokepoint — every final-media write/copy/delete in\n// the system goes through a MediaObjectStore. Inline storage-provider calls for\n// FINAL media are forbidden outside this module. (Upload STAGING stays plain\n// Firebase Storage and is not this module's concern.)\n//\n// Implementations (the consuming app picks one per environment):\n// - R2 (S3-compatible) for deployed environments whose final media lives in\n// R2 and is served by an edge gateway.\n// - Firebase Storage for deployed environments whose final media lives in\n// Firebase Storage. Token-free: it writes and copies no download token, so\n// its objects are readable only through the app's own access path (Storage\n// rules). Firebase can still mint a token later (a client getDownloadURL, the\n// console); the app's read path must never do so.\n// - Firebase Storage emulator for local dev/tests — the Firebase Storage store\n// plus a FIXED caller-supplied download token on every object, so\n// deterministic emulator URLs work. Production NEVER mints download tokens.\n//\n// Writes return `{ key }` — never URLs. Display URLs are built at render time\n// from asset refs (see the consuming app's media-asset-url helper).\n\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { stat } from \"node:fs/promises\";\nimport { Readable } from \"node:stream\";\nimport { pipeline } from \"node:stream/promises\";\nimport { AwsClient } from \"aws4fetch\";\nimport type { getStorage } from \"firebase-admin/storage\";\nimport { NEUTRAL_CONTENT_TYPE } from \"@ttt-productions/media-schemas\";\nimport { isObjectNotFoundError } from \"./gcs-errors.js\";\nimport {\n defaultR2RetrySeams,\n fetchWithR2Retry,\n R2StorageError,\n type R2RetrySeams,\n} from \"./r2-retry.js\";\nimport { requireStorageObjectGeneration, UnreadableStorageObjectMetadataError } from \"./staged-object.js\";\n\n// Re-export the exhaustion error so callers can recognize an exhausted R2\n// operation. The rest of the retry helper stays private to this module.\nexport { R2StorageError };\n\n/**\n * A copy found an object at its destination key that is not this copy's result, and\n * wrote nothing; that object is untouched. The key is a PROPERTY, never message text, so\n * an escaping error never carries an object key to monitoring.\n */\nexport class ObjectAlreadyExistsError extends Error {\n constructor(public readonly key: string) {\n super(\"copy destination already holds another object\");\n this.name = \"ObjectAlreadyExistsError\";\n }\n}\n\n/** Firebase Admin Storage Bucket — inferred to avoid @google-cloud/storage dep. */\nexport type Bucket = ReturnType<ReturnType<typeof getStorage>[\"bucket\"]>;\n\nexport interface ObjectWriteResult {\n key: string;\n sizeBytes: number;\n contentType: string;\n}\n\nexport interface ObjectReadResult {\n sizeBytes: number;\n contentType: string;\n}\n\nexport interface ObjectCopyResult {\n key: string;\n /**\n * `true` when the destination already held this copy's result — the same source version,\n * committed by an earlier attempt whose answer was lost or by a concurrent identical\n * copy; `false` when this call committed it.\n */\n alreadyPresent: boolean;\n}\n\n/**\n * What `deleteCopy` found at the destination: `deleted` — a copy of the source, now removed;\n * `absent` — no object; `notThisCopy` — an object this package did not copy from that\n * source, left untouched.\n */\nexport type DeleteCopyResult = \"deleted\" | \"absent\" | \"notThisCopy\";\n\n// Every copy records on its destination which source version it holds. A copy whose\n// destination already exists compares this record with its own, so it recognizes its\n// own earlier result and never mistakes another writer's object for it. The values are\n// URI-encoded because R2 carries custom metadata in HTTP headers, which must be ASCII.\nconst COPY_SOURCE_METADATA_KEY = \"copy-source\";\nconst COPY_SOURCE_VERSION_METADATA_KEY = \"copy-source-version\";\n\ntype CopyProvenance = Record<typeof COPY_SOURCE_METADATA_KEY | typeof COPY_SOURCE_VERSION_METADATA_KEY, string>;\n\nfunction copyProvenance(fromKey: string, sourceVersion: string): CopyProvenance {\n return {\n [COPY_SOURCE_METADATA_KEY]: encodeURIComponent(fromKey),\n [COPY_SOURCE_VERSION_METADATA_KEY]: encodeURIComponent(sourceVersion),\n };\n}\n\n/** Whether an object's custom metadata, read through `readMetadata`, names exactly this provenance. */\nfunction holdsProvenance(readMetadata: (name: string) => unknown, provenance: CopyProvenance): boolean {\n return Object.entries(provenance).every(([name, value]) => readMetadata(name) === value);\n}\n\n/**\n * Whether an object's custom metadata records a copy of `fromKey`, of whichever source\n * version: `deleteCopy` reads only the destination, so it cannot know the source's version.\n * A record naming the source without a version is not provenance this package wrote.\n */\nfunction recordsCopyOf(readMetadata: (name: string) => unknown, fromKey: string): boolean {\n const version = readMetadata(COPY_SOURCE_VERSION_METADATA_KEY);\n return (\n readMetadata(COPY_SOURCE_METADATA_KEY) === encodeURIComponent(fromKey) &&\n typeof version === \"string\" &&\n version !== \"\"\n );\n}\n\n/** The content headers a copy carries from its source: HTTP header name → Cloud Storage field. */\nconst CARRIED_CONTENT_HEADERS = {\n \"content-type\": \"contentType\",\n \"cache-control\": \"cacheControl\",\n \"content-disposition\": \"contentDisposition\",\n \"content-encoding\": \"contentEncoding\",\n} as const;\n\ntype CarriedContentField = (typeof CARRIED_CONTENT_HEADERS)[keyof typeof CARRIED_CONTENT_HEADERS];\n\nexport interface MediaObjectStore {\n /**\n * Upload a local file to the store at `key`. Objects are written with a\n * one-year `immutable` cache-control, so keys are write-once: a replacement is\n * written under a NEW key and the app repoints its reference to it. Overwriting\n * an existing key would keep serving the old bytes from caches for up to a year.\n */\n putFile(args: { localPath: string; key: string; contentType?: string }): Promise<ObjectWriteResult>;\n /**\n * Server-side copy within the store. It never overwrites and is idempotent: it commits\n * only when `toKey` holds no object, in one call that writes the destination fully\n * formed (the source version's bytes and content headers, its provenance, and this\n * store's token policy). When `toKey` already holds this copy's result — a retry of\n * the same copy of the same source version — it resolves with `alreadyPresent: true`;\n * when it holds anything else, it throws `ObjectAlreadyExistsError` and leaves that\n * object untouched. It never deletes anything.\n */\n copy(args: { fromKey: string; toKey: string }): Promise<ObjectCopyResult>;\n /**\n * Download an object's bytes from the store to a local file (streamed, so a\n * large video never sits fully in memory). Used by safety evidence capture to\n * pull final media — which may live in a different backend (R2) — before\n * writing it into the separate (GCS) evidence vault: capture is inherently\n * read-here-then-write, NOT a server-side copy, because source and vault can\n * be different backends.\n */\n readToFile(args: { key: string; localPath: string }): Promise<ObjectReadResult>;\n /** Delete an object. Missing objects are a no-op, not an error. */\n delete(key: string): Promise<void>;\n /**\n * Delete `toKey` only when the object there is a copy of `fromKey` this package made: its\n * own recorded provenance must name `fromKey` as its source. Only the destination is read,\n * so this works after the source is gone. Any other object — a copy of another source, or\n * one with no provenance — is left untouched and answers `notThisCopy`.\n *\n * Firebase Storage deletes only the generation it checked; an object replaced after the\n * check is checked again, never deleted blind. R2's DeleteObject takes no precondition, so\n * there the delete follows the check unconditioned. That gap is harmless only under the key\n * contract: keys are write-once and a copy never replaces an object, so in the gap the\n * checked object can only be removed; and a copy's key is derived from its source, so the\n * only object that can land there afterwards is another copy of the same source.\n */\n deleteCopy(args: { fromKey: string; toKey: string }): Promise<DeleteCopyResult>;\n}\n\nconst DEFAULT_CACHE_CONTROL = \"public, max-age=31536000, immutable\";\n\n// ---------------------------------------------------------------------------\n// R2 (S3-compatible) store — deployed environments\n// ---------------------------------------------------------------------------\n\nexport interface CreateR2ObjectStoreArgs {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucket: string;\n /** Override for tests; defaults to the real R2 S3 endpoint. */\n endpoint?: string;\n /** Override fetch for tests. */\n fetchImpl?: typeof fetch;\n /** Test seam: override the retry sleep (defaults to real `setTimeout`). */\n sleepImpl?: (ms: number) => Promise<void>;\n /** Test seam: override the backoff jitter source (defaults to `Math.random`). */\n randomImpl?: () => number;\n /** Test seam: override the clock used for HTTP-date `Retry-After` (defaults to `Date.now`). */\n nowImpl?: () => number;\n}\n\nexport function createR2ObjectStore(args: CreateR2ObjectStoreArgs): MediaObjectStore {\n const endpoint = (args.endpoint ?? `https://${args.accountId}.r2.cloudflarestorage.com`).replace(/\\/$/, \"\");\n const client = new AwsClient({\n accessKeyId: args.accessKeyId,\n secretAccessKey: args.secretAccessKey,\n service: \"s3\",\n region: \"auto\",\n });\n const doFetch = args.fetchImpl ?? fetch;\n\n // Retry seams: production defaults are internal; tests inject deterministic\n // sleep/random/now. putFile/copy/readToFile ride the bounded retry helper;\n // delete stays one-shot (never routes through it — see below).\n const defaults = defaultR2RetrySeams();\n const seams: R2RetrySeams = {\n sleep: args.sleepImpl ?? defaults.sleep,\n random: args.randomImpl ?? defaults.random,\n now: args.nowImpl ?? defaults.now,\n };\n\n const objectUrl = (key: string) =>\n `${endpoint}/${args.bucket}/${key.split(\"/\").map(encodeURIComponent).join(\"/\")}`;\n\n async function signedFetch(url: string, init: RequestInit & { headers?: Record<string, string> }) {\n const signed = await client.sign(url, { ...init, aws: { allHeaders: true } } as any);\n return doFetch(signed, { ...(init.body ? { duplex: \"half\" } : {}) } as any);\n }\n\n // A GET whose body is discarded unread, not a HEAD: a HEAD's error response carries no\n // body, so the S3 error code that tells a missing key from a missing bucket would be lost.\n async function readObjectHeaders(key: string, operation: \"copy\" | \"deleteCopy\"): Promise<Headers> {\n return fetchWithR2Retry({\n operation,\n bucket: args.bucket,\n key,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), { method: \"GET\" });\n if (!res.ok) return { kind: \"response\" as const, response: res };\n await res.body?.cancel();\n return { kind: \"success\" as const, value: res.headers };\n },\n });\n }\n\n // R2 exposes no object version id, so a source version is its ETag, last-modified time,\n // and size together: replacing the source changes at least one of them unless the new\n // content has the same MD5-derived ETag and lands within the same second. A multipart\n // upload's ETag is not the content's MD5, but it is the same kind of version token.\n function readSourceVersion(headers: Headers, key: string): { etag: string; version: string } {\n const etag = headers.get(\"etag\");\n if (!etag) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"etag\");\n const lastModified = headers.get(\"last-modified\");\n if (!lastModified) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"lastModified\");\n const size = headers.get(\"content-length\");\n if (!size) throw new UnreadableStorageObjectMetadataError(args.bucket, key, \"size\");\n return { etag, version: `${etag} ${lastModified} ${size}` };\n }\n\n return {\n async putFile({ localPath, key, contentType }) {\n // stat() once, outside the retry loop: a missing input file is a\n // deterministic local error and must fail immediately, never retry.\n const { size } = await stat(localPath);\n const ct = contentType ?? getMimeFromExt(extractExt(localPath));\n return fetchWithR2Retry({\n operation: \"putFile\",\n bucket: args.bucket,\n key,\n seams,\n // Fresh signed request AND a fresh read stream on every attempt — a\n // consumed stream is never reused.\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), {\n method: \"PUT\",\n headers: {\n \"content-type\": ct,\n \"content-length\": String(size),\n \"cache-control\": DEFAULT_CACHE_CONTROL,\n },\n body: createReadStream(localPath) as unknown as BodyInit,\n });\n if (res.ok) {\n return { kind: \"success\" as const, value: { key, sizeBytes: size, contentType: ct } };\n }\n return { kind: \"response\" as const, response: res };\n },\n });\n },\n\n async copy({ fromKey, toKey }) {\n const source = await readObjectHeaders(fromKey, \"copy\");\n const { etag, version } = readSourceVersion(source, fromKey);\n const provenance = copyProvenance(fromKey, version);\n const headers: Record<string, string> = {\n \"x-amz-copy-source\": `/${args.bucket}/${fromKey.split(\"/\").map(encodeURIComponent).join(\"/\")}`,\n // Pins the copy to the source version the provenance names.\n \"x-amz-copy-source-if-match\": etag,\n // R2's destination-conditional CopyObject extension, checked when the copy commits;\n // S3's x-amz-copy-source-if-* headers condition only the source.\n \"cf-copy-destination-if-none-match\": \"*\",\n // The destination takes exactly the headers below rather than the source's, so it\n // commits fully formed in this one call.\n \"x-amz-metadata-directive\": \"REPLACE\",\n };\n for (const name of Object.keys(CARRIED_CONTENT_HEADERS)) {\n const value = source.get(name);\n if (value !== null) headers[name] = value;\n }\n for (const [name, value] of Object.entries(provenance)) headers[`x-amz-meta-${name}`] = value;\n\n try {\n await fetchWithR2Retry({\n operation: \"copy\",\n bucket: args.bucket,\n key: toKey,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(toKey), { method: \"PUT\", headers });\n if (res.ok) return { kind: \"success\" as const, value: undefined };\n return { kind: \"response\" as const, response: res };\n },\n });\n return { key: toKey, alreadyPresent: false };\n } catch (e) {\n if (!(e instanceof R2StorageError && e.status === 412)) throw e;\n // R2 answers 412 both for an existing destination and for a source that changed\n // after it was read; only an existing destination can be this copy's result.\n let destination: Headers;\n try {\n destination = await readObjectHeaders(toKey, \"copy\");\n } catch (readError) {\n if (isObjectNotFoundError(readError)) throw e;\n throw readError;\n }\n if (holdsProvenance((name) => destination.get(`x-amz-meta-${name}`), provenance)) {\n return { key: toKey, alreadyPresent: true };\n }\n throw new ObjectAlreadyExistsError(toKey);\n }\n },\n\n async readToFile({ key, localPath }) {\n return fetchWithR2Retry({\n operation: \"readToFile\",\n bucket: args.bucket,\n key,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), { method: \"GET\" });\n if (!res.ok || !res.body) {\n return { kind: \"response\" as const, response: res };\n }\n const contentType = res.headers.get(\"content-type\") ?? NEUTRAL_CONTENT_TYPE;\n // Pipe ONLY after a successful response, and open the destination in\n // truncating mode ('w') on every attempt so a partial download from a\n // prior attempt can never prefix/corrupt this one. res.body is a web\n // ReadableStream; stream it to disk so large media never sits fully in\n // memory. A transient body/network stream failure here throws and is\n // retried by the helper; a deterministic local fs error is not.\n await pipeline(\n Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]),\n createWriteStream(localPath, { flags: \"w\" }),\n );\n const { size } = await stat(localPath);\n return { kind: \"success\" as const, value: { sizeBytes: size, contentType } };\n },\n });\n },\n\n async delete(key) {\n // Intentionally one-shot: delete is never retried (see r2-retry.ts header).\n const res = await signedFetch(objectUrl(key), { method: \"DELETE\" });\n // S3 DELETE returns 204 even for missing keys; anything else non-ok is real.\n if (!res.ok && res.status !== 404) {\n throw new R2StorageError({\n operation: \"delete\",\n bucket: args.bucket,\n key,\n attempts: 1,\n status: res.status,\n responseText: await safeText(res),\n });\n }\n },\n\n async deleteCopy({ fromKey, toKey }) {\n let destination: Headers;\n try {\n destination = await readObjectHeaders(toKey, \"deleteCopy\");\n } catch (e) {\n if (isObjectNotFoundError(e)) return \"absent\";\n throw e;\n }\n if (!recordsCopyOf((name) => destination.get(`x-amz-meta-${name}`), fromKey)) return \"notThisCopy\";\n // Unconditioned: R2's DeleteObject honors no precondition header, so none is sent — one\n // it silently ignored would only look like protection. `MediaObjectStore.deleteCopy`\n // states why the gap after the check is harmless. One-shot, like `delete`.\n const res = await signedFetch(objectUrl(toKey), { method: \"DELETE\" });\n if (res.ok) return \"deleted\";\n const error = new R2StorageError({\n operation: \"deleteCopy\",\n bucket: args.bucket,\n key: toKey,\n attempts: 1,\n status: res.status,\n responseText: await safeText(res),\n });\n if (isObjectNotFoundError(error)) return \"absent\";\n throw error;\n },\n };\n}\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 500);\n } catch {\n return \"<no body>\";\n }\n}\n\n// ---------------------------------------------------------------------------\n// Firebase Storage stores — deployed (token-free) and emulator (fixed token)\n// ---------------------------------------------------------------------------\n\n/** Custom-metadata key Firebase Storage reads download tokens from. */\nconst DOWNLOAD_TOKENS_METADATA_KEY = \"firebaseStorageDownloadTokens\";\n\n/**\n * How many times `deleteCopy` checks a Firebase Storage destination before giving up. Keys\n * are write-once, so one change between the check and the delete is already rare; an object\n * still changing after that is left alone and the precondition failure is rethrown.\n */\nconst DELETE_COPY_MAX_READS = 2;\n\nexport interface CreateFirebaseStorageObjectStoreArgs {\n bucket: Bucket;\n}\n\n/** Firebase Storage store for deployed environments. Token-free (see header). */\nexport function createFirebaseStorageObjectStore(args: CreateFirebaseStorageObjectStoreArgs): MediaObjectStore {\n return createFirebaseStore(args.bucket, null);\n}\n\nexport interface CreateEmulatorObjectStoreArgs extends CreateFirebaseStorageObjectStoreArgs {\n /** Fixed token attached to every object so deterministic emulator URLs work\n * (token URLs bypass rules in the emulator). Pass the app's constant. */\n downloadToken: string;\n}\n\n/** Local dev / tests only: the Firebase Storage store plus a fixed download token. */\nexport function createFirebaseEmulatorObjectStore(args: CreateEmulatorObjectStoreArgs): MediaObjectStore {\n return createFirebaseStore(args.bucket, args.downloadToken);\n}\n\n/** `downloadToken` is the store's token policy: `null` = token-free, a string = that token on every object. */\nfunction createFirebaseStore(bucket: Bucket, downloadToken: string | null): MediaObjectStore {\n async function deleteObject(key: string): Promise<void> {\n try {\n await bucket.file(key).delete();\n } catch (e) {\n if (isObjectNotFoundError(e)) return;\n throw e;\n }\n }\n\n return {\n async putFile({ localPath, key, contentType }) {\n const { size } = await stat(localPath);\n const ct = contentType ?? getMimeFromExt(extractExt(localPath));\n // An upload replaces the object and all of its metadata, so a token-free\n // put carries no custom metadata at all.\n await bucket.upload(localPath, {\n destination: key,\n metadata: {\n contentType: ct,\n cacheControl: DEFAULT_CACHE_CONTROL,\n ...(downloadToken === null ? {} : { metadata: { [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken } }),\n },\n });\n return { key, sizeBytes: size, contentType: ct };\n },\n\n async copy({ fromKey, toKey }) {\n const [source] = await bucket.file(fromKey).getMetadata();\n const generation = requireStorageObjectGeneration(source, { bucket: bucket.name, key: fromKey });\n const provenance = copyProvenance(fromKey, generation);\n const contentHeaders: Partial<Record<CarriedContentField, string>> = {};\n for (const field of Object.values(CARRIED_CONTENT_HEADERS)) {\n const value = source[field];\n if (typeof value === \"string\" && value !== \"\") contentHeaders[field] = value;\n }\n try {\n // A rewrite's resource REPLACES the destination's metadata rather than merging with\n // the source's, so this one call commits the destination fully formed: the source's\n // content headers, the provenance, and this store's token policy — never a token the\n // source carried. The source is pinned to the generation the provenance names, so a\n // 412 can only mean the destination exists; a replaced source fails as not-found.\n await bucket.file(fromKey, { generation }).copy(bucket.file(toKey), {\n ...contentHeaders,\n metadata: downloadToken === null ? provenance : { ...provenance, [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken },\n preconditionOpts: { ifGenerationMatch: 0 },\n });\n return { key: toKey, alreadyPresent: false };\n } catch (e) {\n if (!isPreconditionFailedError(e)) throw e;\n let destinationMetadata: Record<string, unknown> | undefined;\n try {\n const [destination] = await bucket.file(toKey).getMetadata();\n destinationMetadata = destination.metadata ?? undefined;\n } catch (readError) {\n if (isObjectNotFoundError(readError)) throw e;\n throw readError;\n }\n if (holdsProvenance((name) => destinationMetadata?.[name], provenance)) {\n return { key: toKey, alreadyPresent: true };\n }\n throw new ObjectAlreadyExistsError(toKey);\n }\n },\n\n async readToFile({ key, localPath }) {\n const file = bucket.file(key);\n await file.download({ destination: localPath });\n const [meta] = await file.getMetadata();\n const { size } = await stat(localPath);\n const contentType = (meta.contentType as string | undefined) ?? NEUTRAL_CONTENT_TYPE;\n return { sizeBytes: size, contentType };\n },\n\n delete: deleteObject,\n\n async deleteCopy({ fromKey, toKey }) {\n const file = bucket.file(toKey);\n for (let read = 1; ; read++) {\n let custom: Record<string, unknown> | undefined;\n let generation: string | number | null | undefined;\n try {\n const [destination] = await file.getMetadata();\n custom = destination.metadata ?? undefined;\n generation = destination.generation;\n } catch (e) {\n if (isObjectNotFoundError(e)) return \"absent\";\n throw e;\n }\n if (!recordsCopyOf((name) => custom?.[name], fromKey)) return \"notThisCopy\";\n // The delete names the generation just checked, so it can only remove that object.\n const checked = requireStorageObjectGeneration({ generation }, { bucket: bucket.name, key: toKey });\n try {\n await file.delete({ ifGenerationMatch: checked });\n return \"deleted\";\n } catch (e) {\n if (isObjectNotFoundError(e)) return \"absent\";\n // A 412 means the object changed after it was checked: check what is there now.\n if (!isPreconditionFailedError(e) || read >= DELETE_COPY_MAX_READS) throw e;\n }\n }\n },\n };\n}\n\n/** The Cloud Storage Admin SDK's numeric `412`: a generation precondition did not hold. */\nfunction isPreconditionFailedError(e: unknown): boolean {\n return (e as { code?: unknown } | null | undefined)?.code === 412;\n}\n\n// ---------------------------------------------------------------------------\n\nfunction extractExt(localPath: string): string {\n return localPath.split(\".\").pop() || \"\";\n}\n\nexport function getMimeFromExt(ext: string): string {\n switch (ext.toLowerCase()) {\n case \"jpg\":\n case \"jpeg\":\n return \"image/jpeg\";\n case \"png\":\n return \"image/png\";\n case \"webp\":\n return \"image/webp\";\n case \"mp4\":\n return \"video/mp4\";\n case \"webm\":\n return \"video/webm\";\n case \"m4a\":\n return \"audio/mp4\";\n case \"aac\":\n return \"audio/aac\";\n case \"mp3\":\n return \"audio/mpeg\";\n default:\n return NEUTRAL_CONTENT_TYPE;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"storage-ops.js","sourceRoot":"","sources":["../../src/server/storage-ops.ts"],"names":[],"mappings":"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","sourcesContent":["// Final-media object-store chokepoint — every final-media write/copy/delete in\n// the system goes through a MediaObjectStore. Inline storage-provider calls for\n// FINAL media are forbidden outside this module. (Upload STAGING stays plain\n// Firebase Storage and is not this module's concern.)\n//\n// Implementations (the consuming app picks one per environment):\n// - R2 (S3-compatible) for deployed environments whose final media lives in\n// R2 and is served by an edge gateway.\n// - Firebase Storage for deployed environments whose final media lives in\n// Firebase Storage. Token-free: it writes and copies no download token, so\n// its objects are readable only through the app's own access path (Storage\n// rules). Firebase can still mint a token later (a client getDownloadURL, the\n// console); the app's read path must never do so.\n// - Firebase Storage emulator for local dev/tests — the Firebase Storage store\n// plus a FIXED caller-supplied download token on every object, so\n// deterministic emulator URLs work. Production NEVER mints download tokens.\n//\n// Writes return `{ key }` — never URLs. Display URLs are built at render time\n// from asset refs (see the consuming app's media-asset-url helper).\n\nimport { createReadStream, createWriteStream } from 'node:fs';\nimport { stat } from 'node:fs/promises';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\nimport { AwsClient } from 'aws4fetch';\nimport type { getStorage } from 'firebase-admin/storage';\nimport { NEUTRAL_CONTENT_TYPE } from '@ttt-productions/media-schemas';\nimport { isObjectNotFoundError } from './gcs-errors.js';\nimport {\n defaultR2RetrySeams,\n fetchWithR2Retry,\n R2StorageError,\n type R2RetrySeams,\n} from './r2-retry.js';\nimport {\n requireStorageObjectGeneration,\n UnreadableStorageObjectMetadataError,\n} from './staged-object.js';\n\n/**\n * A copy found an object at its destination key that is not this copy's result, and\n * wrote nothing; that object is untouched. The key is a PROPERTY, never message text, so\n * an escaping error never carries an object key to monitoring.\n */\nexport class ObjectAlreadyExistsError extends Error {\n constructor(public readonly key: string) {\n super('copy destination already holds another object');\n this.name = 'ObjectAlreadyExistsError';\n }\n}\n\n/** Firebase Admin Storage Bucket — inferred to avoid @google-cloud/storage dep. */\nexport type Bucket = ReturnType<ReturnType<typeof getStorage>['bucket']>;\n\nexport interface ObjectWriteResult {\n key: string;\n sizeBytes: number;\n contentType: string;\n}\n\nexport interface ObjectReadResult {\n sizeBytes: number;\n contentType: string;\n}\n\nexport interface ObjectCopyResult {\n key: string;\n /**\n * `true` when the destination already held this copy's result — the same source version,\n * committed by an earlier attempt whose answer was lost or by a concurrent identical\n * copy; `false` when this call committed it.\n */\n alreadyPresent: boolean;\n}\n\n/**\n * What `deleteCopy` found at the destination: `deleted` — a copy of the source, now removed;\n * `absent` — no object; `notThisCopy` — an object this package did not copy from that\n * source, left untouched.\n */\nexport type DeleteCopyResult = 'deleted' | 'absent' | 'notThisCopy';\n\n// Every copy records on its destination which source version it holds. A copy whose\n// destination already exists compares this record with its own, so it recognizes its\n// own earlier result and never mistakes another writer's object for it. The values are\n// URI-encoded because R2 carries custom metadata in HTTP headers, which must be ASCII.\nconst COPY_SOURCE_METADATA_KEY = 'copy-source';\nconst COPY_SOURCE_VERSION_METADATA_KEY = 'copy-source-version';\n\ntype CopyProvenance = Record<\n typeof COPY_SOURCE_METADATA_KEY | typeof COPY_SOURCE_VERSION_METADATA_KEY,\n string\n>;\n\nfunction copyProvenance(fromKey: string, sourceVersion: string): CopyProvenance {\n return {\n [COPY_SOURCE_METADATA_KEY]: encodeURIComponent(fromKey),\n [COPY_SOURCE_VERSION_METADATA_KEY]: encodeURIComponent(sourceVersion),\n };\n}\n\n/** Whether an object's custom metadata, read through `readMetadata`, names exactly this provenance. */\nfunction holdsProvenance(\n readMetadata: (name: string) => unknown,\n provenance: CopyProvenance,\n): boolean {\n return Object.entries(provenance).every(([name, value]) => readMetadata(name) === value);\n}\n\n/**\n * Whether an object's custom metadata records a copy of `fromKey`, of whichever source\n * version: `deleteCopy` reads only the destination, so it cannot know the source's version.\n * A record naming the source without a version is not provenance this package wrote.\n */\nfunction recordsCopyOf(readMetadata: (name: string) => unknown, fromKey: string): boolean {\n const version = readMetadata(COPY_SOURCE_VERSION_METADATA_KEY);\n return (\n readMetadata(COPY_SOURCE_METADATA_KEY) === encodeURIComponent(fromKey) &&\n typeof version === 'string' &&\n version !== ''\n );\n}\n\n/** The content headers a copy carries from its source: HTTP header name → Cloud Storage field. */\nconst CARRIED_CONTENT_HEADERS = {\n 'content-type': 'contentType',\n 'cache-control': 'cacheControl',\n 'content-disposition': 'contentDisposition',\n 'content-encoding': 'contentEncoding',\n} as const;\n\ntype CarriedContentField = (typeof CARRIED_CONTENT_HEADERS)[keyof typeof CARRIED_CONTENT_HEADERS];\n\nexport interface MediaObjectStore {\n /**\n * Upload a local file to the store at `key`. Objects are written with a\n * one-year `immutable` cache-control, so keys are write-once: a replacement is\n * written under a NEW key and the app repoints its reference to it. Overwriting\n * an existing key would keep serving the old bytes from caches for up to a year.\n */\n putFile(args: {\n localPath: string;\n key: string;\n contentType?: string;\n }): Promise<ObjectWriteResult>;\n /**\n * Server-side copy within the store. It never overwrites and is idempotent: it commits\n * only when `toKey` holds no object, in one call that writes the destination fully\n * formed (the source version's bytes and content headers, its provenance, and this\n * store's token policy). When `toKey` already holds this copy's result — a retry of\n * the same copy of the same source version — it resolves with `alreadyPresent: true`;\n * when it holds anything else, it throws `ObjectAlreadyExistsError` and leaves that\n * object untouched. It never deletes anything.\n */\n copy(args: { fromKey: string; toKey: string }): Promise<ObjectCopyResult>;\n /**\n * Download an object's bytes from the store to a local file (streamed, so a\n * large video never sits fully in memory). Used by safety evidence capture to\n * pull final media — which may live in a different backend (R2) — before\n * writing it into the separate (GCS) evidence vault: capture is inherently\n * read-here-then-write, NOT a server-side copy, because source and vault can\n * be different backends.\n */\n readToFile(args: { key: string; localPath: string }): Promise<ObjectReadResult>;\n /** Delete an object. Missing objects are a no-op, not an error. */\n delete(key: string): Promise<void>;\n /**\n * Delete `toKey` only when the object there is a copy of `fromKey` this package made: its\n * own recorded provenance must name `fromKey` as its source. Only the destination is read,\n * so this works after the source is gone. Any other object — a copy of another source, or\n * one with no provenance — is left untouched and answers `notThisCopy`.\n *\n * Firebase Storage deletes only the generation it checked; an object replaced after the\n * check is checked again, never deleted blind. R2's DeleteObject takes no precondition, so\n * there the delete follows the check unconditioned. That gap is harmless only under the key\n * contract: keys are write-once and a copy never replaces an object, so in the gap the\n * checked object can only be removed; and a copy's key is derived from its source, so the\n * only object that can land there afterwards is another copy of the same source.\n */\n deleteCopy(args: { fromKey: string; toKey: string }): Promise<DeleteCopyResult>;\n}\n\nconst DEFAULT_CACHE_CONTROL = 'public, max-age=31536000, immutable';\n\n// ---------------------------------------------------------------------------\n// R2 (S3-compatible) store — deployed environments\n// ---------------------------------------------------------------------------\n\nexport interface CreateR2ObjectStoreArgs {\n accountId: string;\n accessKeyId: string;\n secretAccessKey: string;\n bucket: string;\n /** Override for tests; defaults to the real R2 S3 endpoint. */\n endpoint?: string;\n /** Override fetch for tests. */\n fetchImpl?: typeof fetch;\n /** Test seam: override the retry sleep (defaults to real `setTimeout`). */\n sleepImpl?: (ms: number) => Promise<void>;\n /** Test seam: override the backoff jitter source (defaults to `Math.random`). */\n randomImpl?: () => number;\n /** Test seam: override the clock used for HTTP-date `Retry-After` (defaults to `Date.now`). */\n nowImpl?: () => number;\n}\n\nexport function createR2ObjectStore(args: CreateR2ObjectStoreArgs): MediaObjectStore {\n const endpoint = (args.endpoint ?? `https://${args.accountId}.r2.cloudflarestorage.com`).replace(\n /\\/$/,\n '',\n );\n const client = new AwsClient({\n accessKeyId: args.accessKeyId,\n secretAccessKey: args.secretAccessKey,\n service: 's3',\n region: 'auto',\n });\n const doFetch = args.fetchImpl ?? fetch;\n\n // Retry seams: production defaults are internal; tests inject deterministic\n // sleep/random/now. putFile/copy/readToFile ride the bounded retry helper;\n // delete stays one-shot (never routes through it — see below).\n const defaults = defaultR2RetrySeams();\n const seams: R2RetrySeams = {\n sleep: args.sleepImpl ?? defaults.sleep,\n random: args.randomImpl ?? defaults.random,\n now: args.nowImpl ?? defaults.now,\n };\n\n const objectUrl = (key: string) =>\n `${endpoint}/${args.bucket}/${key.split('/').map(encodeURIComponent).join('/')}`;\n\n async function signedFetch(\n url: string,\n init: RequestInit & { headers?: Record<string, string> },\n ) {\n const signed = await client.sign(url, { ...init, aws: { allHeaders: true } } as any);\n return doFetch(signed, { ...(init.body ? { duplex: 'half' } : {}) } as any);\n }\n\n // A GET whose body is discarded unread, not a HEAD: a HEAD's error response carries no\n // body, so the S3 error code that tells a missing key from a missing bucket would be lost.\n async function readObjectHeaders(\n key: string,\n operation: 'copy' | 'deleteCopy',\n ): Promise<Headers> {\n return fetchWithR2Retry({\n operation,\n bucket: args.bucket,\n key,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), { method: 'GET' });\n if (!res.ok) return { kind: 'response' as const, response: res };\n await res.body?.cancel();\n return { kind: 'success' as const, value: res.headers };\n },\n });\n }\n\n // R2 exposes no object version id, so a source version is its ETag, last-modified time,\n // and size together: replacing the source changes at least one of them unless the new\n // content has the same MD5-derived ETag and lands within the same second. A multipart\n // upload's ETag is not the content's MD5, but it is the same kind of version token.\n function readSourceVersion(headers: Headers, key: string): { etag: string; version: string } {\n const etag = headers.get('etag');\n if (!etag) throw new UnreadableStorageObjectMetadataError(args.bucket, key, 'etag');\n const lastModified = headers.get('last-modified');\n if (!lastModified)\n throw new UnreadableStorageObjectMetadataError(args.bucket, key, 'lastModified');\n const size = headers.get('content-length');\n if (!size) throw new UnreadableStorageObjectMetadataError(args.bucket, key, 'size');\n return { etag, version: `${etag} ${lastModified} ${size}` };\n }\n\n return {\n async putFile({ localPath, key, contentType }) {\n // stat() once, outside the retry loop: a missing input file is a\n // deterministic local error and must fail immediately, never retry.\n const { size } = await stat(localPath);\n const ct = contentType ?? getMimeFromExt(extractExt(localPath));\n return fetchWithR2Retry({\n operation: 'putFile',\n bucket: args.bucket,\n key,\n seams,\n // Fresh signed request AND a fresh read stream on every attempt — a\n // consumed stream is never reused.\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), {\n method: 'PUT',\n headers: {\n 'content-type': ct,\n 'content-length': String(size),\n 'cache-control': DEFAULT_CACHE_CONTROL,\n },\n body: createReadStream(localPath) as unknown as BodyInit,\n });\n if (res.ok) {\n return { kind: 'success' as const, value: { key, sizeBytes: size, contentType: ct } };\n }\n return { kind: 'response' as const, response: res };\n },\n });\n },\n\n async copy({ fromKey, toKey }) {\n const source = await readObjectHeaders(fromKey, 'copy');\n const { etag, version } = readSourceVersion(source, fromKey);\n const provenance = copyProvenance(fromKey, version);\n const headers: Record<string, string> = {\n 'x-amz-copy-source': `/${args.bucket}/${fromKey.split('/').map(encodeURIComponent).join('/')}`,\n // Pins the copy to the source version the provenance names.\n 'x-amz-copy-source-if-match': etag,\n // R2's destination-conditional CopyObject extension, checked when the copy commits;\n // S3's x-amz-copy-source-if-* headers condition only the source.\n 'cf-copy-destination-if-none-match': '*',\n // The destination takes exactly the headers below rather than the source's, so it\n // commits fully formed in this one call.\n 'x-amz-metadata-directive': 'REPLACE',\n };\n for (const name of Object.keys(CARRIED_CONTENT_HEADERS)) {\n const value = source.get(name);\n if (value !== null) headers[name] = value;\n }\n for (const [name, value] of Object.entries(provenance)) headers[`x-amz-meta-${name}`] = value;\n\n try {\n await fetchWithR2Retry({\n operation: 'copy',\n bucket: args.bucket,\n key: toKey,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(toKey), { method: 'PUT', headers });\n if (res.ok) return { kind: 'success' as const, value: undefined };\n return { kind: 'response' as const, response: res };\n },\n });\n return { key: toKey, alreadyPresent: false };\n } catch (e) {\n if (!(e instanceof R2StorageError && e.status === 412)) throw e;\n // R2 answers 412 both for an existing destination and for a source that changed\n // after it was read; only an existing destination can be this copy's result.\n let destination: Headers;\n try {\n destination = await readObjectHeaders(toKey, 'copy');\n } catch (readError) {\n if (isObjectNotFoundError(readError)) throw e;\n throw readError;\n }\n if (holdsProvenance((name) => destination.get(`x-amz-meta-${name}`), provenance)) {\n return { key: toKey, alreadyPresent: true };\n }\n throw new ObjectAlreadyExistsError(toKey);\n }\n },\n\n async readToFile({ key, localPath }) {\n return fetchWithR2Retry({\n operation: 'readToFile',\n bucket: args.bucket,\n key,\n seams,\n runAttempt: async () => {\n const res = await signedFetch(objectUrl(key), { method: 'GET' });\n if (!res.ok || !res.body) {\n return { kind: 'response' as const, response: res };\n }\n const contentType = res.headers.get('content-type') ?? NEUTRAL_CONTENT_TYPE;\n // Pipe ONLY after a successful response, and open the destination in\n // truncating mode ('w') on every attempt so a partial download from a\n // prior attempt can never prefix/corrupt this one. res.body is a web\n // ReadableStream; stream it to disk so large media never sits fully in\n // memory. A transient body/network stream failure here throws and is\n // retried by the helper; a deterministic local fs error is not.\n await pipeline(\n Readable.fromWeb(res.body as Parameters<typeof Readable.fromWeb>[0]),\n createWriteStream(localPath, { flags: 'w' }),\n );\n const { size } = await stat(localPath);\n return { kind: 'success' as const, value: { sizeBytes: size, contentType } };\n },\n });\n },\n\n async delete(key) {\n // Intentionally one-shot: delete is never retried (see r2-retry.ts header).\n const res = await signedFetch(objectUrl(key), { method: 'DELETE' });\n // S3 DELETE returns 204 even for missing keys; anything else non-ok is real.\n if (!res.ok && res.status !== 404) {\n throw new R2StorageError({\n operation: 'delete',\n bucket: args.bucket,\n key,\n attempts: 1,\n status: res.status,\n responseText: await safeText(res),\n });\n }\n },\n\n async deleteCopy({ fromKey, toKey }) {\n let destination: Headers;\n try {\n destination = await readObjectHeaders(toKey, 'deleteCopy');\n } catch (e) {\n if (isObjectNotFoundError(e)) return 'absent';\n throw e;\n }\n if (!recordsCopyOf((name) => destination.get(`x-amz-meta-${name}`), fromKey))\n return 'notThisCopy';\n // Unconditioned: R2's DeleteObject honors no precondition header, so none is sent — one\n // it silently ignored would only look like protection. `MediaObjectStore.deleteCopy`\n // states why the gap after the check is harmless. One-shot, like `delete`.\n const res = await signedFetch(objectUrl(toKey), { method: 'DELETE' });\n if (res.ok) return 'deleted';\n const error = new R2StorageError({\n operation: 'deleteCopy',\n bucket: args.bucket,\n key: toKey,\n attempts: 1,\n status: res.status,\n responseText: await safeText(res),\n });\n if (isObjectNotFoundError(error)) return 'absent';\n throw error;\n },\n };\n}\n\nasync function safeText(res: Response): Promise<string> {\n try {\n return (await res.text()).slice(0, 500);\n } catch {\n return '<no body>';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Firebase Storage stores — deployed (token-free) and emulator (fixed token)\n// ---------------------------------------------------------------------------\n\n/** Custom-metadata key Firebase Storage reads download tokens from. */\nconst DOWNLOAD_TOKENS_METADATA_KEY = 'firebaseStorageDownloadTokens';\n\n/**\n * How many times `deleteCopy` checks a Firebase Storage destination before giving up. Keys\n * are write-once, so one change between the check and the delete is already rare; an object\n * still changing after that is left alone and the precondition failure is rethrown.\n */\nconst DELETE_COPY_MAX_READS = 2;\n\nexport interface CreateFirebaseStorageObjectStoreArgs {\n bucket: Bucket;\n}\n\n/** Firebase Storage store for deployed environments. Token-free (see header). */\nexport function createFirebaseStorageObjectStore(\n args: CreateFirebaseStorageObjectStoreArgs,\n): MediaObjectStore {\n return createFirebaseStore(args.bucket, null);\n}\n\nexport interface CreateEmulatorObjectStoreArgs extends CreateFirebaseStorageObjectStoreArgs {\n /** Fixed token attached to every object so deterministic emulator URLs work\n * (token URLs bypass rules in the emulator). Pass the app's constant. */\n downloadToken: string;\n}\n\n/** Local dev / tests only: the Firebase Storage store plus a fixed download token. */\nexport function createFirebaseEmulatorObjectStore(\n args: CreateEmulatorObjectStoreArgs,\n): MediaObjectStore {\n return createFirebaseStore(args.bucket, args.downloadToken);\n}\n\n/** `downloadToken` is the store's token policy: `null` = token-free, a string = that token on every object. */\nfunction createFirebaseStore(bucket: Bucket, downloadToken: string | null): MediaObjectStore {\n async function deleteObject(key: string): Promise<void> {\n try {\n await bucket.file(key).delete();\n } catch (e) {\n if (isObjectNotFoundError(e)) return;\n throw e;\n }\n }\n\n return {\n async putFile({ localPath, key, contentType }) {\n const { size } = await stat(localPath);\n const ct = contentType ?? getMimeFromExt(extractExt(localPath));\n // An upload replaces the object and all of its metadata, so a token-free\n // put carries no custom metadata at all.\n await bucket.upload(localPath, {\n destination: key,\n metadata: {\n contentType: ct,\n cacheControl: DEFAULT_CACHE_CONTROL,\n ...(downloadToken === null\n ? {}\n : { metadata: { [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken } }),\n },\n });\n return { key, sizeBytes: size, contentType: ct };\n },\n\n async copy({ fromKey, toKey }) {\n const [source] = await bucket.file(fromKey).getMetadata();\n const generation = requireStorageObjectGeneration(source, {\n bucket: bucket.name,\n key: fromKey,\n });\n const provenance = copyProvenance(fromKey, generation);\n const contentHeaders: Partial<Record<CarriedContentField, string>> = {};\n for (const field of Object.values(CARRIED_CONTENT_HEADERS)) {\n const value = source[field];\n if (typeof value === 'string' && value !== '') contentHeaders[field] = value;\n }\n try {\n // A rewrite's resource REPLACES the destination's metadata rather than merging with\n // the source's, so this one call commits the destination fully formed: the source's\n // content headers, the provenance, and this store's token policy — never a token the\n // source carried. The source is pinned to the generation the provenance names, so a\n // 412 can only mean the destination exists; a replaced source fails as not-found.\n await bucket.file(fromKey, { generation }).copy(bucket.file(toKey), {\n ...contentHeaders,\n metadata:\n downloadToken === null\n ? provenance\n : { ...provenance, [DOWNLOAD_TOKENS_METADATA_KEY]: downloadToken },\n preconditionOpts: { ifGenerationMatch: 0 },\n });\n return { key: toKey, alreadyPresent: false };\n } catch (e) {\n if (!isPreconditionFailedError(e)) throw e;\n let destinationMetadata: Record<string, unknown> | undefined;\n try {\n const [destination] = await bucket.file(toKey).getMetadata();\n destinationMetadata = destination.metadata ?? undefined;\n } catch (readError) {\n if (isObjectNotFoundError(readError)) throw e;\n throw readError;\n }\n if (holdsProvenance((name) => destinationMetadata?.[name], provenance)) {\n return { key: toKey, alreadyPresent: true };\n }\n throw new ObjectAlreadyExistsError(toKey);\n }\n },\n\n async readToFile({ key, localPath }) {\n const file = bucket.file(key);\n await file.download({ destination: localPath });\n const [meta] = await file.getMetadata();\n const { size } = await stat(localPath);\n const contentType = (meta.contentType as string | undefined) ?? NEUTRAL_CONTENT_TYPE;\n return { sizeBytes: size, contentType };\n },\n\n delete: deleteObject,\n\n async deleteCopy({ fromKey, toKey }) {\n const file = bucket.file(toKey);\n for (let read = 1; ; read++) {\n let custom: Record<string, unknown> | undefined;\n let generation: string | number | null | undefined;\n try {\n const [destination] = await file.getMetadata();\n custom = destination.metadata ?? undefined;\n generation = destination.generation;\n } catch (e) {\n if (isObjectNotFoundError(e)) return 'absent';\n throw e;\n }\n if (!recordsCopyOf((name) => custom?.[name], fromKey)) return 'notThisCopy';\n // The delete names the generation just checked, so it can only remove that object.\n const checked = requireStorageObjectGeneration(\n { generation },\n { bucket: bucket.name, key: toKey },\n );\n try {\n await file.delete({ ifGenerationMatch: checked });\n return 'deleted';\n } catch (e) {\n if (isObjectNotFoundError(e)) return 'absent';\n // A 412 means the object changed after it was checked: check what is there now.\n if (!isPreconditionFailedError(e) || read >= DELETE_COPY_MAX_READS) throw e;\n }\n }\n },\n };\n}\n\n/** The Cloud Storage Admin SDK's numeric `412`: a generation precondition did not hold. */\nfunction isPreconditionFailedError(e: unknown): boolean {\n return (e as { code?: unknown } | null | undefined)?.code === 412;\n}\n\n// ---------------------------------------------------------------------------\n\nfunction extractExt(localPath: string): string {\n return localPath.split('.').pop() || '';\n}\n\nexport function getMimeFromExt(ext: string): string {\n switch (ext.toLowerCase()) {\n case 'jpg':\n case 'jpeg':\n return 'image/jpeg';\n case 'png':\n return 'image/png';\n case 'webp':\n return 'image/webp';\n case 'mp4':\n return 'video/mp4';\n case 'webm':\n return 'video/webm';\n case 'm4a':\n return 'audio/mp4';\n case 'aac':\n return 'audio/aac';\n case 'mp3':\n return 'audio/mpeg';\n default:\n return NEUTRAL_CONTENT_TYPE;\n }\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { MediaProcessingResult, MediaProcessingSpec } from
|
|
1
|
+
import type { MediaProcessingResult, MediaProcessingSpec } from '@ttt-productions/media-schemas';
|
|
2
2
|
export interface MediaPipelineProgress {
|
|
3
|
-
phase:
|
|
3
|
+
phase: 'read_input' | 'moderation_input' | 'process' | 'persist_outputs' | 'moderation_output' | 'done';
|
|
4
4
|
/** 0..1 when known */
|
|
5
5
|
percent?: number;
|
|
6
6
|
detail?: Record<string, unknown>;
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { MediaProcessingResult, MediaProcessingSpec } from
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { MediaProcessingResult, MediaProcessingSpec } from '@ttt-productions/media-schemas';\n\nexport interface MediaPipelineProgress {\n phase:\n | 'read_input'\n | 'moderation_input'\n | 'process'\n | 'persist_outputs'\n | 'moderation_output'\n | 'done';\n /** 0..1 when known */\n percent?: number;\n detail?: Record<string, unknown>;\n}\n\nexport interface ProcessMediaContext {\n inputPath: string;\n outputBasePath: string;\n /** optional mime hint from caller */\n inputMime?: string;\n}\n\nexport interface ProcessMediaOptions {\n signal?: AbortSignal;\n onProgress?: (p: MediaPipelineProgress) => void;\n}\n\nexport type ProcessMediaFn = (\n spec: MediaProcessingSpec,\n ctx: ProcessMediaContext,\n opts?: ProcessMediaOptions,\n) => Promise<MediaProcessingResult>;\n\nexport interface MediaProcessors {\n image: ProcessMediaFn;\n video: ProcessMediaFn;\n audio: ProcessMediaFn;\n}\n"]}
|
package/dist/utils/log.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,wBAAgB,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,QAErC;AAED,wBAAgB,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,QAEtC;AAED,wBAAgB,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,QAEvC"}
|
package/dist/utils/log.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export function log(...args) {
|
|
2
|
-
console.log(
|
|
2
|
+
console.log('[media-processing]', ...args);
|
|
3
3
|
}
|
|
4
4
|
export function warn(...args) {
|
|
5
|
-
console.warn(
|
|
5
|
+
console.warn('[media-processing]', ...args);
|
|
6
6
|
}
|
|
7
7
|
export function error(...args) {
|
|
8
|
-
console.error(
|
|
8
|
+
console.error('[media-processing]', ...args);
|
|
9
9
|
}
|
|
10
10
|
//# sourceMappingURL=log.js.map
|
package/dist/utils/log.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,CAAC,GAAG,IAAe;
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/utils/log.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,CAAC,GAAG,IAAe;IACpC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,GAAG,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,GAAG,IAAe;IACrC,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,GAAG,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,GAAG,IAAe;IACtC,OAAO,CAAC,KAAK,CAAC,oBAAoB,EAAE,GAAG,IAAI,CAAC,CAAC;AAC/C,CAAC","sourcesContent":["export function log(...args: unknown[]) {\n console.log('[media-processing]', ...args);\n}\n\nexport function warn(...args: unknown[]) {\n console.warn('[media-processing]', ...args);\n}\n\nexport function error(...args: unknown[]) {\n console.error('[media-processing]', ...args);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,UAE1C"}
|
package/dist/utils/paths.js
CHANGED
package/dist/utils/paths.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,QAAQ,CAAC,GAAG,KAAe;
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,QAAQ,CAAC,GAAG,KAAe;IACzC,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAC9D,CAAC","sourcesContent":["export function joinPath(...parts: string[]) {\n return parts.filter(Boolean).join('/').replace(/\\/+/g, '/');\n}\n"]}
|
package/dist/utils/safe-path.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import path from
|
|
1
|
+
import path from 'node:path';
|
|
2
2
|
// Very conservative key sanitizer. Prevents path traversal / separators.
|
|
3
3
|
export function sanitizeKey(key) {
|
|
4
|
-
const k = String(key ??
|
|
5
|
-
const stripped = k.replace(/[\\/]+/g,
|
|
6
|
-
const cleaned = stripped.replace(/\.+/g,
|
|
7
|
-
const safe = cleaned.replace(/[^a-zA-Z0-9._-]/g,
|
|
8
|
-
return safe.slice(0, 80) ||
|
|
4
|
+
const k = String(key ?? '');
|
|
5
|
+
const stripped = k.replace(/[\\/]+/g, '_');
|
|
6
|
+
const cleaned = stripped.replace(/\.+/g, '.'); // collapse long dot runs
|
|
7
|
+
const safe = cleaned.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
8
|
+
return safe.slice(0, 80) || 'file';
|
|
9
9
|
}
|
|
10
10
|
export function safeOutputPathFor(base, key, ext) {
|
|
11
11
|
const safeKey = sanitizeKey(key);
|
|
12
|
-
const safeExt = sanitizeKey(ext).replace(/^\./,
|
|
12
|
+
const safeExt = sanitizeKey(ext).replace(/^\./, '');
|
|
13
13
|
const dir = path.dirname(base);
|
|
14
14
|
const baseName = path.basename(base);
|
|
15
15
|
const out = path.join(dir, `${baseName}_${safeKey}.${safeExt}`);
|
|
16
16
|
const resolvedDir = path.resolve(dir);
|
|
17
17
|
const resolvedOut = path.resolve(out);
|
|
18
18
|
if (!resolvedOut.startsWith(resolvedDir + path.sep) && resolvedOut !== resolvedDir) {
|
|
19
|
-
throw new Error(
|
|
19
|
+
throw new Error('Unsafe output path');
|
|
20
20
|
}
|
|
21
21
|
return out;
|
|
22
22
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-path.js","sourceRoot":"","sources":["../../src/utils/safe-path.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,yEAAyE;AACzE,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAC5B,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,yBAAyB;IACxE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,GAAW,EAAE,GAAW;IACtE,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC,CAAC;IAEhE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC;QACnF,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["import path from
|
|
1
|
+
{"version":3,"file":"safe-path.js","sourceRoot":"","sources":["../../src/utils/safe-path.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,yEAAyE;AACzE,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAC5B,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,yBAAyB;IACxE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACtD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,GAAW,EAAE,GAAW;IACtE,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IACjC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC,CAAC;IAEhE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,WAAW,KAAK,WAAW,EAAE,CAAC;QACnF,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["import path from 'node:path';\n\n// Very conservative key sanitizer. Prevents path traversal / separators.\nexport function sanitizeKey(key: string): string {\n const k = String(key ?? '');\n const stripped = k.replace(/[\\\\/]+/g, '_');\n const cleaned = stripped.replace(/\\.+/g, '.'); // collapse long dot runs\n const safe = cleaned.replace(/[^a-zA-Z0-9._-]/g, '_');\n return safe.slice(0, 80) || 'file';\n}\n\nexport function safeOutputPathFor(base: string, key: string, ext: string): string {\n const safeKey = sanitizeKey(key);\n const safeExt = sanitizeKey(ext).replace(/^\\./, '');\n const dir = path.dirname(base);\n const baseName = path.basename(base);\n const out = path.join(dir, `${baseName}_${safeKey}.${safeExt}`);\n\n const resolvedDir = path.resolve(dir);\n const resolvedOut = path.resolve(out);\n if (!resolvedOut.startsWith(resolvedDir + path.sep) && resolvedOut !== resolvedDir) {\n throw new Error('Unsafe output path');\n }\n\n return out;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-duration.d.ts","sourceRoot":"","sources":["../../src/validation/validate-duration.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,
|
|
1
|
+
{"version":3,"file":"validate-duration.d.ts","sourceRoot":"","sources":["../../src/validation/validate-duration.ts"],"names":[],"mappings":"AAAA,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAErF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-duration.js","sourceRoot":"","sources":["../../src/validation/validate-duration.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,
|
|
1
|
+
{"version":3,"file":"validate-duration.js","sourceRoot":"","sources":["../../src/validation/validate-duration.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,cAAsB;IAC1E,OAAO,WAAW,IAAI,cAAc,CAAC;AACvC,CAAC","sourcesContent":["export function validateDuration(durationSec: number, maxDurationSec: number): boolean {\n return durationSec <= maxDurationSec;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-mime.d.ts","sourceRoot":"","sources":["../../src/validation/validate-mime.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"validate-mime.d.ts","sourceRoot":"","sources":["../../src/validation/validate-mime.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAErE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-mime.js","sourceRoot":"","sources":["../../src/validation/validate-mime.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY,
|
|
1
|
+
{"version":3,"file":"validate-mime.js","sourceRoot":"","sources":["../../src/validation/validate-mime.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAiB;IAC1D,OAAO,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAChC,CAAC","sourcesContent":["export function validateMime(mime: string, allowed: string[]): boolean {\n return allowed.includes(mime);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-size.d.ts","sourceRoot":"","sources":["../../src/validation/validate-size.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"validate-size.d.ts","sourceRoot":"","sources":["../../src/validation/validate-size.ts"],"names":[],"mappings":"AAAA,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAErE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-size.js","sourceRoot":"","sources":["../../src/validation/validate-size.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY,
|
|
1
|
+
{"version":3,"file":"validate-size.js","sourceRoot":"","sources":["../../src/validation/validate-size.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,YAAY,CAAC,KAAa,EAAE,QAAgB;IAC1D,OAAO,KAAK,IAAI,QAAQ,CAAC;AAC3B,CAAC","sourcesContent":["export function validateSize(bytes: number, maxBytes: number): boolean {\n return bytes <= maxBytes;\n}\n"]}
|
package/dist/video/ffmpeg.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface RunCmdResult {
|
|
|
12
12
|
* and must not be parsed as complete (e.g. incomplete probe JSON). */
|
|
13
13
|
truncated?: boolean;
|
|
14
14
|
}
|
|
15
|
-
export type FfmpegProgressPhase =
|
|
15
|
+
export type FfmpegProgressPhase = 'transcode' | 'poster';
|
|
16
16
|
export interface RunFfmpegOptions {
|
|
17
17
|
cwd?: string;
|
|
18
18
|
signal?: AbortSignal;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B;iFAC6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;uDAEmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;2EACuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,mBAAmB,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/F,0CAA0C;IAC1C,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAID;wDACwD;AACxD,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAK/C,wBAAgB,MAAM,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;uDAEmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;sEAEkE;IAClE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,GACA,OAAO,CAAC,YAAY,CAAC,
|
|
1
|
+
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B;iFAC6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;uDAEmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;2EACuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,mBAAmB,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/F,0CAA0C;IAC1C,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAID;wDACwD;AACxD,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAK/C,wBAAgB,MAAM,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;uDAEmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;sEAEkE;IAClE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,GACA,OAAO,CAAC,YAAY,CAAC,CA8EvB;AA8CD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuFxF;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CACpD;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAC/D,CAcA"}
|
package/dist/video/ffmpeg.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
const MAX_CAPTURE_BYTES = 1024 * 1024; // 1MB per stream
|
|
3
3
|
/** Wall-clock ceiling for an ffprobe call that opens a file and reads its
|
|
4
4
|
* stream table (metadata only, never every packet). */
|
|
@@ -7,14 +7,14 @@ let ffmpegChecked = null;
|
|
|
7
7
|
let ffmpegVersion = null;
|
|
8
8
|
export function runCmd(cmd, args, opts) {
|
|
9
9
|
return new Promise((resolve, reject) => {
|
|
10
|
-
const p = spawn(cmd, args, { cwd: opts?.cwd, stdio: [
|
|
11
|
-
let stdout =
|
|
12
|
-
let stderr =
|
|
10
|
+
const p = spawn(cmd, args, { cwd: opts?.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
11
|
+
let stdout = '';
|
|
12
|
+
let stderr = '';
|
|
13
13
|
let timedOut = false;
|
|
14
14
|
let truncated = false;
|
|
15
15
|
const kill = () => {
|
|
16
16
|
try {
|
|
17
|
-
p.kill(
|
|
17
|
+
p.kill('SIGKILL');
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
20
|
/* already dead */
|
|
@@ -34,24 +34,24 @@ export function runCmd(cmd, args, opts) {
|
|
|
34
34
|
if (opts.signal.aborted)
|
|
35
35
|
onAbort();
|
|
36
36
|
else
|
|
37
|
-
opts.signal.addEventListener(
|
|
37
|
+
opts.signal.addEventListener('abort', onAbort, { once: true });
|
|
38
38
|
}
|
|
39
39
|
const onLine = opts?.onStdoutLine;
|
|
40
|
-
let lineBuf =
|
|
40
|
+
let lineBuf = '';
|
|
41
41
|
// Decodes across chunk boundaries, so a multi-byte character split
|
|
42
42
|
// between two chunks reaches the line intact.
|
|
43
43
|
if (onLine)
|
|
44
|
-
p.stdout.setEncoding(
|
|
45
|
-
p.stdout.on(
|
|
44
|
+
p.stdout.setEncoding('utf8');
|
|
45
|
+
p.stdout.on('data', (d) => {
|
|
46
46
|
if (onLine) {
|
|
47
47
|
if (truncated)
|
|
48
48
|
return;
|
|
49
49
|
const lines = (lineBuf + String(d)).split(/\r?\n/);
|
|
50
|
-
lineBuf = lines.pop() ??
|
|
50
|
+
lineBuf = lines.pop() ?? '';
|
|
51
51
|
for (const line of [...lines, lineBuf]) {
|
|
52
52
|
if (line.length > MAX_CAPTURE_BYTES) {
|
|
53
53
|
truncated = true;
|
|
54
|
-
lineBuf =
|
|
54
|
+
lineBuf = '';
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -64,32 +64,38 @@ export function runCmd(cmd, args, opts) {
|
|
|
64
64
|
else
|
|
65
65
|
truncated = true;
|
|
66
66
|
});
|
|
67
|
-
p.stderr.on(
|
|
67
|
+
p.stderr.on('data', (d) => {
|
|
68
68
|
if (stderr.length < MAX_CAPTURE_BYTES)
|
|
69
69
|
stderr += String(d);
|
|
70
70
|
else
|
|
71
71
|
truncated = true;
|
|
72
72
|
});
|
|
73
|
-
p.on(
|
|
73
|
+
p.on('error', (e) => {
|
|
74
74
|
if (timer)
|
|
75
75
|
clearTimeout(timer);
|
|
76
76
|
if (opts?.signal)
|
|
77
|
-
opts.signal.removeEventListener(
|
|
77
|
+
opts.signal.removeEventListener('abort', onAbort);
|
|
78
78
|
reject(e);
|
|
79
79
|
});
|
|
80
|
-
p.on(
|
|
80
|
+
p.on('close', (code) => {
|
|
81
81
|
if (timer)
|
|
82
82
|
clearTimeout(timer);
|
|
83
83
|
if (opts?.signal)
|
|
84
|
-
opts.signal.removeEventListener(
|
|
84
|
+
opts.signal.removeEventListener('abort', onAbort);
|
|
85
85
|
if (onLine && !truncated && lineBuf)
|
|
86
86
|
onLine(lineBuf);
|
|
87
|
-
resolve({
|
|
87
|
+
resolve({
|
|
88
|
+
code: code ?? -1,
|
|
89
|
+
stdout,
|
|
90
|
+
stderr,
|
|
91
|
+
...(timedOut ? { timedOut } : {}),
|
|
92
|
+
...(truncated ? { truncated } : {}),
|
|
93
|
+
});
|
|
88
94
|
});
|
|
89
95
|
});
|
|
90
96
|
}
|
|
91
97
|
function abortError() {
|
|
92
|
-
return new DOMException(
|
|
98
|
+
return new DOMException('Aborted', 'AbortError');
|
|
93
99
|
}
|
|
94
100
|
// NOTE: ffmpeg prints "out_time_ms" but it is microseconds in known builds.
|
|
95
101
|
// We harden parsing and sanity-check values.
|
|
@@ -105,7 +111,9 @@ function parseOutTimeMicroseconds(value) {
|
|
|
105
111
|
}
|
|
106
112
|
function parseOutTimeHmsToMs(value) {
|
|
107
113
|
// "HH:MM:SS.micro" (micro may be omitted)
|
|
108
|
-
const m = String(value)
|
|
114
|
+
const m = String(value)
|
|
115
|
+
.trim()
|
|
116
|
+
.match(/^(\d+):([0-5]\d):([0-5]\d)(?:\.(\d{1,6}))?$/);
|
|
109
117
|
if (!m)
|
|
110
118
|
return undefined;
|
|
111
119
|
const hh = Number(m[1]);
|
|
@@ -113,8 +121,8 @@ function parseOutTimeHmsToMs(value) {
|
|
|
113
121
|
const ss = Number(m[3]);
|
|
114
122
|
if (!Number.isFinite(hh) || !Number.isFinite(mm) || !Number.isFinite(ss))
|
|
115
123
|
return undefined;
|
|
116
|
-
const frac = m[4] ??
|
|
117
|
-
const us = frac ? Number((frac +
|
|
124
|
+
const frac = m[4] ?? '';
|
|
125
|
+
const us = frac ? Number((frac + '000000').slice(0, 6)) : 0;
|
|
118
126
|
if (!Number.isFinite(us) || us < 0)
|
|
119
127
|
return undefined;
|
|
120
128
|
const totalMs = (hh * 3600 + mm * 60 + ss) * 1000 + Math.floor(us / 1000);
|
|
@@ -134,26 +142,26 @@ function clamp01(n) {
|
|
|
134
142
|
* If onProgress is provided, ffmpeg is invoked with "-progress pipe:1".
|
|
135
143
|
*/
|
|
136
144
|
export function runFfmpeg(args, opts) {
|
|
137
|
-
const phase = opts?.phase ??
|
|
145
|
+
const phase = opts?.phase ?? 'transcode';
|
|
138
146
|
const withProgress = !!opts?.onProgress;
|
|
139
|
-
const ffArgs = withProgress ? [
|
|
147
|
+
const ffArgs = withProgress ? ['-progress', 'pipe:1', '-nostats', ...args] : args;
|
|
140
148
|
return new Promise((resolve, reject) => {
|
|
141
149
|
if (opts?.signal?.aborted)
|
|
142
150
|
return reject(abortError());
|
|
143
|
-
const p = spawn(
|
|
144
|
-
let stdout =
|
|
145
|
-
let stderr =
|
|
146
|
-
let progressBuf =
|
|
151
|
+
const p = spawn('ffmpeg', ffArgs, { cwd: opts?.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
152
|
+
let stdout = '';
|
|
153
|
+
let stderr = '';
|
|
154
|
+
let progressBuf = '';
|
|
147
155
|
// keep progress monotonic
|
|
148
156
|
let lastOutTimeMs;
|
|
149
157
|
const onAbort = () => {
|
|
150
158
|
try {
|
|
151
|
-
p.kill(
|
|
159
|
+
p.kill('SIGKILL');
|
|
152
160
|
}
|
|
153
161
|
catch { }
|
|
154
162
|
};
|
|
155
|
-
opts?.signal?.addEventListener(
|
|
156
|
-
p.stdout.on(
|
|
163
|
+
opts?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
164
|
+
p.stdout.on('data', (d) => {
|
|
157
165
|
const s = String(d);
|
|
158
166
|
if (stdout.length < MAX_CAPTURE_BYTES)
|
|
159
167
|
stdout += s;
|
|
@@ -161,25 +169,25 @@ export function runFfmpeg(args, opts) {
|
|
|
161
169
|
return;
|
|
162
170
|
progressBuf += s;
|
|
163
171
|
const lines = progressBuf.split(/\r?\n/);
|
|
164
|
-
progressBuf = lines.pop() ??
|
|
172
|
+
progressBuf = lines.pop() ?? '';
|
|
165
173
|
let outTimeMs;
|
|
166
174
|
for (const line of lines) {
|
|
167
|
-
const idx = line.indexOf(
|
|
175
|
+
const idx = line.indexOf('=');
|
|
168
176
|
if (idx <= 0)
|
|
169
177
|
continue;
|
|
170
178
|
const k = line.slice(0, idx).trim();
|
|
171
179
|
const v = line.slice(idx + 1).trim();
|
|
172
|
-
if (k ===
|
|
180
|
+
if (k === 'out_time_ms') {
|
|
173
181
|
const us = parseOutTimeMicroseconds(v);
|
|
174
182
|
if (us !== undefined)
|
|
175
183
|
outTimeMs = Math.floor(us / 1000);
|
|
176
184
|
}
|
|
177
|
-
else if (k ===
|
|
185
|
+
else if (k === 'out_time' && outTimeMs == null) {
|
|
178
186
|
const ms = parseOutTimeHmsToMs(v);
|
|
179
187
|
if (ms !== undefined)
|
|
180
188
|
outTimeMs = ms;
|
|
181
189
|
}
|
|
182
|
-
else if (k ===
|
|
190
|
+
else if (k === 'progress' && v === 'end') {
|
|
183
191
|
opts?.onProgress?.({ phase, percent: 1, outTimeMs: lastOutTimeMs });
|
|
184
192
|
}
|
|
185
193
|
}
|
|
@@ -196,17 +204,17 @@ export function runFfmpeg(args, opts) {
|
|
|
196
204
|
opts?.onProgress?.({ phase, percent, outTimeMs: lastOutTimeMs });
|
|
197
205
|
}
|
|
198
206
|
});
|
|
199
|
-
p.stderr.on(
|
|
207
|
+
p.stderr.on('data', (d) => {
|
|
200
208
|
const s = String(d);
|
|
201
209
|
if (stderr.length < MAX_CAPTURE_BYTES)
|
|
202
210
|
stderr += s;
|
|
203
211
|
});
|
|
204
|
-
p.on(
|
|
205
|
-
opts?.signal?.removeEventListener(
|
|
212
|
+
p.on('error', (e) => {
|
|
213
|
+
opts?.signal?.removeEventListener('abort', onAbort);
|
|
206
214
|
reject(e);
|
|
207
215
|
});
|
|
208
|
-
p.on(
|
|
209
|
-
opts?.signal?.removeEventListener(
|
|
216
|
+
p.on('close', (code) => {
|
|
217
|
+
opts?.signal?.removeEventListener('abort', onAbort);
|
|
210
218
|
if (opts?.signal?.aborted)
|
|
211
219
|
return reject(abortError());
|
|
212
220
|
resolve({ code: code ?? 0, stdout, stderr });
|
|
@@ -217,13 +225,13 @@ export async function ensureFfmpegAvailable() {
|
|
|
217
225
|
if (ffmpegChecked)
|
|
218
226
|
return { ok: true, version: ffmpegVersion ?? undefined };
|
|
219
227
|
try {
|
|
220
|
-
const r = await runCmd(
|
|
228
|
+
const r = await runCmd('ffmpeg', ['-version']);
|
|
221
229
|
if (r.code === 0) {
|
|
222
230
|
ffmpegChecked = true;
|
|
223
|
-
ffmpegVersion = (r.stdout || r.stderr ||
|
|
231
|
+
ffmpegVersion = (r.stdout || r.stderr || '').split(/\r?\n/)[0] || null;
|
|
224
232
|
return { ok: true, version: ffmpegVersion ?? undefined };
|
|
225
233
|
}
|
|
226
|
-
return { ok: false, error: new Error(
|
|
234
|
+
return { ok: false, error: new Error('ffmpeg not available') };
|
|
227
235
|
}
|
|
228
236
|
catch (e) {
|
|
229
237
|
return { ok: false, error: e };
|