@ttt-productions/upload-core 0.7.3 → 0.7.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"upload-error.d.ts","sourceRoot":"","sources":["../../src/storage/upload-error.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB,sBAAsB,GACtB,sBAAsB,CAAC;AAE3B;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;gBAEnB,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,MAAM;CAOpD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAKvF"}
1
+ {"version":3,"file":"upload-error.d.ts","sourceRoot":"","sources":["../../src/storage/upload-error.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GACvB,sBAAsB,GACtB,sBAAsB,CAAC;AAE3B;;;;GAIG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;gBAEnB,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,MAAM;CAOpD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAKvF"}
@@ -1,3 +1,4 @@
1
+ import { NEUTRAL_CONTENT_TYPE } from "@ttt-productions/media-schemas";
1
2
  /**
2
3
  * Typed error thrown by upload-core when a client-side guard rejects an upload
3
4
  * before it reaches Firebase Storage. Consumers can catch this and show a
@@ -22,7 +23,7 @@ export function isValidMediaContentType(contentType) {
22
23
  if (!contentType)
23
24
  return false;
24
25
  const ct = contentType.toLowerCase().trim();
25
- if (ct === 'application/octet-stream')
26
+ if (ct === NEUTRAL_CONTENT_TYPE)
26
27
  return false;
27
28
  return /^(image|video|audio)\/[a-z0-9.+-]+$/.test(ct);
28
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"upload-error.js","sourceRoot":"","sources":["../../src/storage/upload-error.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,CAAkB;IAE/B,YAAY,IAAqB,EAAE,OAAgB;QACjD,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,uEAAuE;QACvE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACrD,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,WAAsC;IAC5E,IAAI,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IAC/B,MAAM,EAAE,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,EAAE,KAAK,0BAA0B;QAAE,OAAO,KAAK,CAAC;IACpD,OAAO,qCAAqC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACxD,CAAC","sourcesContent":["export type UploadErrorCode =\n | 'missing_content_type'\n | 'invalid_content_type';\n\n/**\n * Typed error thrown by upload-core when a client-side guard rejects an upload\n * before it reaches Firebase Storage. Consumers can catch this and show a\n * friendly message instead of parsing a storage 403.\n */\nexport class UploadError extends Error {\n readonly code: UploadErrorCode;\n\n constructor(code: UploadErrorCode, message?: string) {\n super(message ?? code);\n this.name = 'UploadError';\n this.code = code;\n // Preserve prototype chain for `instanceof` checks after transpilation\n Object.setPrototypeOf(this, UploadError.prototype);\n }\n}\n\n/**\n * Returns true if the given string looks like a valid media MIME type\n * for an image, video, or audio upload: image/*, video/*, or audio/*.\n * Empty strings, application/octet-stream, and anything else → false.\n */\nexport function isValidMediaContentType(contentType: string | undefined | null): boolean {\n if (!contentType) return false;\n const ct = contentType.toLowerCase().trim();\n if (ct === 'application/octet-stream') return false;\n return /^(image|video|audio)\\/[a-z0-9.+-]+$/.test(ct);\n}\n"]}
1
+ {"version":3,"file":"upload-error.js","sourceRoot":"","sources":["../../src/storage/upload-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAMtE;;;;GAIG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAC3B,IAAI,CAAkB;IAE/B,YAAY,IAAqB,EAAE,OAAgB;QACjD,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,uEAAuE;QACvE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACrD,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,WAAsC;IAC5E,IAAI,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IAC/B,MAAM,EAAE,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,EAAE,KAAK,oBAAoB;QAAE,OAAO,KAAK,CAAC;IAC9C,OAAO,qCAAqC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACxD,CAAC","sourcesContent":["import { NEUTRAL_CONTENT_TYPE } from \"@ttt-productions/media-schemas\";\n\nexport type UploadErrorCode =\n | 'missing_content_type'\n | 'invalid_content_type';\n\n/**\n * Typed error thrown by upload-core when a client-side guard rejects an upload\n * before it reaches Firebase Storage. Consumers can catch this and show a\n * friendly message instead of parsing a storage 403.\n */\nexport class UploadError extends Error {\n readonly code: UploadErrorCode;\n\n constructor(code: UploadErrorCode, message?: string) {\n super(message ?? code);\n this.name = 'UploadError';\n this.code = code;\n // Preserve prototype chain for `instanceof` checks after transpilation\n Object.setPrototypeOf(this, UploadError.prototype);\n }\n}\n\n/**\n * Returns true if the given string looks like a valid media MIME type\n * for an image, video, or audio upload: image/*, video/*, or audio/*.\n * Empty strings, application/octet-stream, and anything else → false.\n */\nexport function isValidMediaContentType(contentType: string | undefined | null): boolean {\n if (!contentType) return false;\n const ct = contentType.toLowerCase().trim();\n if (ct === NEUTRAL_CONTENT_TYPE) return false;\n return /^(image|video|audio)\\/[a-z0-9.+-]+$/.test(ct);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/storage/upload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAKrF,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAC;AAM1B,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,eAAe,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,uBAAuB,CAAC,YAAY,CAAC,CAAC;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,gBAAgB,CA4JnB;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG;IAC7D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;;;;;OASG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC,GACA,OAAO,CAAC,yBAAyB,CAAC,CAwBpC"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../src/storage/upload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAMrF,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,cAAc,EAEpB,MAAM,kBAAkB,CAAC;AAM1B,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,eAAe,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,uBAAuB,CAAC,YAAY,CAAC,CAAC;IACnD,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,gBAAgB,CA4JnB;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG;IAC7D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;;;;;OASG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC,GACA,OAAO,CAAC,yBAAyB,CAAC,CAwBpC"}
@@ -1,5 +1,6 @@
1
1
  import { getFileSize } from "../utils/file-size.js";
2
2
  import { upsertUploadSession } from "../utils/upload-store.js";
3
+ import { NEUTRAL_CONTENT_TYPE } from "@ttt-productions/media-schemas";
3
4
  import { UploadError, isValidMediaContentType } from "./upload-error.js";
4
5
  import { getDownloadURL, ref, uploadBytesResumable, } from "firebase/storage";
5
6
  function isAbortError(e) {
@@ -158,7 +159,7 @@ export async function uploadFileResumable(args) {
158
159
  if (!ct) {
159
160
  throw new UploadError('missing_content_type', 'Upload rejected: contentType metadata is required. Ensure file.type is set before uploading.');
160
161
  }
161
- const neutralAllowed = args.allowNeutralContentType === true && ct.toLowerCase().trim() === 'application/octet-stream';
162
+ const neutralAllowed = args.allowNeutralContentType === true && ct.toLowerCase().trim() === NEUTRAL_CONTENT_TYPE;
162
163
  if (!neutralAllowed && !isValidMediaContentType(ct)) {
163
164
  throw new UploadError('invalid_content_type', `Upload rejected: contentType "${ct}" is not a supported media MIME (expected image/*, video/*, or audio/*).`);
164
165
  }
@@ -1 +1 @@
1
- {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/storage/upload.ts"],"names":[],"mappings":"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","sourcesContent":["import type { UploadFileResumableResult } from \"../types.js\";\nimport type { UploadFileResumableArgs, UploadController } from \"../browser/types.js\";\nimport { getFileSize } from \"../utils/file-size.js\";\nimport { upsertUploadSession } from \"../utils/upload-store.js\";\nimport { UploadError, isValidMediaContentType } from \"./upload-error.js\";\n\nimport {\n getDownloadURL,\n ref,\n uploadBytesResumable,\n type FirebaseStorage,\n type UploadMetadata,\n type UploadTaskSnapshot,\n} from \"firebase/storage\";\n\nfunction isAbortError(e: unknown) {\n return e instanceof DOMException && e.name === \"AbortError\";\n}\n\nexport function startResumableUpload(args: {\n id: string;\n storage: FirebaseStorage;\n path: string;\n file: Blob;\n metadata?: UploadMetadata;\n onProgress?: UploadFileResumableArgs[\"onProgress\"];\n signal?: AbortSignal;\n}): UploadController {\n const { id, storage, path, file, metadata, onProgress, signal } = args;\n\n const r = ref(storage, path);\n const task = uploadBytesResumable(r, file, metadata);\n\n const startedAt = Date.now();\n\n upsertUploadSession({\n id,\n status: \"uploading\",\n path,\n transferred: 0,\n total: getFileSize(file),\n percent: 0,\n startedAt,\n updatedAt: startedAt,\n });\n\n let unsub: (() => void) | null = null;\n\n let resolveDone!: (v: UploadFileResumableResult) => void;\n let rejectDone!: (e: unknown) => void;\n const done = new Promise<UploadFileResumableResult>((res, rej) => {\n resolveDone = res;\n rejectDone = rej;\n });\n\n const onAbort = () => {\n try {\n task.cancel();\n } catch {}\n };\n\n if (signal) {\n if (signal.aborted) onAbort();\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n unsub = task.on(\n \"state_changed\",\n (snap: UploadTaskSnapshot) => {\n const transferred = snap.bytesTransferred ?? 0;\n const total = snap.totalBytes ?? getFileSize(file);\n const percent = total > 0 ? (transferred / total) * 100 : 0;\n\n upsertUploadSession({\n id,\n status:\n snap.state === \"paused\"\n ? \"paused\"\n : snap.state === \"running\"\n ? \"uploading\"\n : \"uploading\",\n path,\n transferred,\n total,\n percent,\n updatedAt: Date.now(),\n });\n\n onProgress?.({ transferred, total, percent, snapshot: snap });\n },\n async (err) => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n if (unsub) unsub();\n\n // normalize aborts as \"canceled\" (not \"error\")\n if (isAbortError(err)) {\n upsertUploadSession({\n id,\n status: \"canceled\",\n path,\n updatedAt: Date.now(),\n error: err,\n });\n return rejectDone(err);\n }\n\n upsertUploadSession({\n id,\n status: \"error\",\n path,\n updatedAt: Date.now(),\n error: err,\n });\n\n rejectDone(err);\n },\n async () => {\n try {\n const downloadURL = await getDownloadURL(task.snapshot.ref);\n const contentType = task.snapshot.metadata?.contentType ?? null;\n const size = task.snapshot.totalBytes ?? getFileSize(file);\n\n const result: UploadFileResumableResult = {\n downloadURL,\n fullPath: task.snapshot.ref.fullPath,\n contentType,\n size,\n };\n\n upsertUploadSession({\n id,\n status: \"success\",\n path,\n transferred: size,\n total: size,\n percent: 100,\n updatedAt: Date.now(),\n result,\n });\n\n resolveDone(result);\n } catch (e) {\n upsertUploadSession({\n id,\n status: \"error\",\n path,\n updatedAt: Date.now(),\n error: e,\n });\n rejectDone(e);\n } finally {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n if (unsub) unsub();\n }\n }\n );\n\n return {\n id,\n task,\n pause: () => {\n try {\n return task.pause();\n } catch {\n return false;\n }\n },\n resume: () => {\n try {\n return task.resume();\n } catch {\n return false;\n }\n },\n cancel: () => {\n try {\n return task.cancel();\n } catch {\n return false;\n }\n },\n done,\n };\n}\n\nexport async function uploadFileResumable(\n args: Omit<Parameters<typeof startResumableUpload>[0], \"id\"> & {\n id?: string;\n /**\n * EXPLICIT opt-in for the NEUTRAL content type (canonical-upload-content-\n * classification, 2026-07-31): a picker file with unknown browser metadata\n * must reach the server AS unknown (`application/octet-stream`) so the\n * server-side byte inspector — the only classification authority — sees it\n * unrewritten. Default false preserves the historical media-only gate for\n * every other consumer. A concrete media MIME is still validated either\n * way; this flag never permits arbitrary declared types, only the one\n * neutral value.\n */\n allowNeutralContentType?: boolean;\n }\n): Promise<UploadFileResumableResult> {\n // Defense-in-depth: reject missing or invalid contentType before any bytes are sent.\n // file-input should already guarantee a valid or explicitly-neutral MIME, but this\n // is the last gate before Firebase Storage.\n const ct = args.metadata?.contentType;\n if (!ct) {\n throw new UploadError(\n 'missing_content_type',\n 'Upload rejected: contentType metadata is required. Ensure file.type is set before uploading.'\n );\n }\n const neutralAllowed =\n args.allowNeutralContentType === true && ct.toLowerCase().trim() === 'application/octet-stream';\n if (!neutralAllowed && !isValidMediaContentType(ct)) {\n throw new UploadError(\n 'invalid_content_type',\n `Upload rejected: contentType \"${ct}\" is not a supported media MIME (expected image/*, video/*, or audio/*).`\n );\n }\n\n const { allowNeutralContentType: _allowNeutral, ...rest } = args;\n const id = args.id ?? `upl_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n const c = startResumableUpload({ ...rest, id });\n return c.done;\n}\n"]}
1
+ {"version":3,"file":"upload.js","sourceRoot":"","sources":["../../src/storage/upload.ts"],"names":[],"mappings":"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","sourcesContent":["import type { UploadFileResumableResult } from \"../types.js\";\nimport type { UploadFileResumableArgs, UploadController } from \"../browser/types.js\";\nimport { getFileSize } from \"../utils/file-size.js\";\nimport { upsertUploadSession } from \"../utils/upload-store.js\";\nimport { NEUTRAL_CONTENT_TYPE } from \"@ttt-productions/media-schemas\";\nimport { UploadError, isValidMediaContentType } from \"./upload-error.js\";\n\nimport {\n getDownloadURL,\n ref,\n uploadBytesResumable,\n type FirebaseStorage,\n type UploadMetadata,\n type UploadTaskSnapshot,\n} from \"firebase/storage\";\n\nfunction isAbortError(e: unknown) {\n return e instanceof DOMException && e.name === \"AbortError\";\n}\n\nexport function startResumableUpload(args: {\n id: string;\n storage: FirebaseStorage;\n path: string;\n file: Blob;\n metadata?: UploadMetadata;\n onProgress?: UploadFileResumableArgs[\"onProgress\"];\n signal?: AbortSignal;\n}): UploadController {\n const { id, storage, path, file, metadata, onProgress, signal } = args;\n\n const r = ref(storage, path);\n const task = uploadBytesResumable(r, file, metadata);\n\n const startedAt = Date.now();\n\n upsertUploadSession({\n id,\n status: \"uploading\",\n path,\n transferred: 0,\n total: getFileSize(file),\n percent: 0,\n startedAt,\n updatedAt: startedAt,\n });\n\n let unsub: (() => void) | null = null;\n\n let resolveDone!: (v: UploadFileResumableResult) => void;\n let rejectDone!: (e: unknown) => void;\n const done = new Promise<UploadFileResumableResult>((res, rej) => {\n resolveDone = res;\n rejectDone = rej;\n });\n\n const onAbort = () => {\n try {\n task.cancel();\n } catch {}\n };\n\n if (signal) {\n if (signal.aborted) onAbort();\n signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n unsub = task.on(\n \"state_changed\",\n (snap: UploadTaskSnapshot) => {\n const transferred = snap.bytesTransferred ?? 0;\n const total = snap.totalBytes ?? getFileSize(file);\n const percent = total > 0 ? (transferred / total) * 100 : 0;\n\n upsertUploadSession({\n id,\n status:\n snap.state === \"paused\"\n ? \"paused\"\n : snap.state === \"running\"\n ? \"uploading\"\n : \"uploading\",\n path,\n transferred,\n total,\n percent,\n updatedAt: Date.now(),\n });\n\n onProgress?.({ transferred, total, percent, snapshot: snap });\n },\n async (err) => {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n if (unsub) unsub();\n\n // normalize aborts as \"canceled\" (not \"error\")\n if (isAbortError(err)) {\n upsertUploadSession({\n id,\n status: \"canceled\",\n path,\n updatedAt: Date.now(),\n error: err,\n });\n return rejectDone(err);\n }\n\n upsertUploadSession({\n id,\n status: \"error\",\n path,\n updatedAt: Date.now(),\n error: err,\n });\n\n rejectDone(err);\n },\n async () => {\n try {\n const downloadURL = await getDownloadURL(task.snapshot.ref);\n const contentType = task.snapshot.metadata?.contentType ?? null;\n const size = task.snapshot.totalBytes ?? getFileSize(file);\n\n const result: UploadFileResumableResult = {\n downloadURL,\n fullPath: task.snapshot.ref.fullPath,\n contentType,\n size,\n };\n\n upsertUploadSession({\n id,\n status: \"success\",\n path,\n transferred: size,\n total: size,\n percent: 100,\n updatedAt: Date.now(),\n result,\n });\n\n resolveDone(result);\n } catch (e) {\n upsertUploadSession({\n id,\n status: \"error\",\n path,\n updatedAt: Date.now(),\n error: e,\n });\n rejectDone(e);\n } finally {\n if (signal) signal.removeEventListener(\"abort\", onAbort);\n if (unsub) unsub();\n }\n }\n );\n\n return {\n id,\n task,\n pause: () => {\n try {\n return task.pause();\n } catch {\n return false;\n }\n },\n resume: () => {\n try {\n return task.resume();\n } catch {\n return false;\n }\n },\n cancel: () => {\n try {\n return task.cancel();\n } catch {\n return false;\n }\n },\n done,\n };\n}\n\nexport async function uploadFileResumable(\n args: Omit<Parameters<typeof startResumableUpload>[0], \"id\"> & {\n id?: string;\n /**\n * EXPLICIT opt-in for the NEUTRAL content type (canonical-upload-content-\n * classification, 2026-07-31): a picker file with unknown browser metadata\n * must reach the server AS unknown (`application/octet-stream`) so the\n * server-side byte inspector — the only classification authority — sees it\n * unrewritten. Default false preserves the historical media-only gate for\n * every other consumer. A concrete media MIME is still validated either\n * way; this flag never permits arbitrary declared types, only the one\n * neutral value.\n */\n allowNeutralContentType?: boolean;\n }\n): Promise<UploadFileResumableResult> {\n // Defense-in-depth: reject missing or invalid contentType before any bytes are sent.\n // file-input should already guarantee a valid or explicitly-neutral MIME, but this\n // is the last gate before Firebase Storage.\n const ct = args.metadata?.contentType;\n if (!ct) {\n throw new UploadError(\n 'missing_content_type',\n 'Upload rejected: contentType metadata is required. Ensure file.type is set before uploading.'\n );\n }\n const neutralAllowed =\n args.allowNeutralContentType === true && ct.toLowerCase().trim() === NEUTRAL_CONTENT_TYPE;\n if (!neutralAllowed && !isValidMediaContentType(ct)) {\n throw new UploadError(\n 'invalid_content_type',\n `Upload rejected: contentType \"${ct}\" is not a supported media MIME (expected image/*, video/*, or audio/*).`\n );\n }\n\n const { allowNeutralContentType: _allowNeutral, ...rest } = args;\n const id = args.id ?? `upl_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n const c = startResumableUpload({ ...rest, id });\n return c.done;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/upload-core",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ttt-productions/ttt-packages.git",
@@ -31,7 +31,8 @@
31
31
  "prepublishOnly": "npm run clean && npm run build"
32
32
  },
33
33
  "dependencies": {
34
- "@ttt-productions/firebase-helpers": "^0.12.4"
34
+ "@ttt-productions/firebase-helpers": "^0.12.9",
35
+ "@ttt-productions/media-schemas": "^0.14.6"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "firebase": ">=12.0.0"