@ttt-productions/upload-core 0.7.2 → 0.7.3
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/storage/upload.d.ts
CHANGED
|
@@ -12,5 +12,16 @@ export declare function startResumableUpload(args: {
|
|
|
12
12
|
}): UploadController;
|
|
13
13
|
export declare function uploadFileResumable(args: Omit<Parameters<typeof startResumableUpload>[0], "id"> & {
|
|
14
14
|
id?: string;
|
|
15
|
+
/**
|
|
16
|
+
* EXPLICIT opt-in for the NEUTRAL content type (canonical-upload-content-
|
|
17
|
+
* classification, 2026-07-31): a picker file with unknown browser metadata
|
|
18
|
+
* must reach the server AS unknown (`application/octet-stream`) so the
|
|
19
|
+
* server-side byte inspector — the only classification authority — sees it
|
|
20
|
+
* unrewritten. Default false preserves the historical media-only gate for
|
|
21
|
+
* every other consumer. A concrete media MIME is still validated either
|
|
22
|
+
* way; this flag never permits arbitrary declared types, only the one
|
|
23
|
+
* neutral value.
|
|
24
|
+
*/
|
|
25
|
+
allowNeutralContentType?: boolean;
|
|
15
26
|
}): Promise<UploadFileResumableResult>;
|
|
16
27
|
//# sourceMappingURL=upload.d.ts.map
|
|
@@ -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;
|
|
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"}
|
package/dist/storage/upload.js
CHANGED
|
@@ -152,17 +152,19 @@ export function startResumableUpload(args) {
|
|
|
152
152
|
}
|
|
153
153
|
export async function uploadFileResumable(args) {
|
|
154
154
|
// Defense-in-depth: reject missing or invalid contentType before any bytes are sent.
|
|
155
|
-
// file-input should already guarantee a valid MIME, but this
|
|
156
|
-
//
|
|
155
|
+
// file-input should already guarantee a valid or explicitly-neutral MIME, but this
|
|
156
|
+
// is the last gate before Firebase Storage.
|
|
157
157
|
const ct = args.metadata?.contentType;
|
|
158
158
|
if (!ct) {
|
|
159
159
|
throw new UploadError('missing_content_type', 'Upload rejected: contentType metadata is required. Ensure file.type is set before uploading.');
|
|
160
160
|
}
|
|
161
|
-
|
|
161
|
+
const neutralAllowed = args.allowNeutralContentType === true && ct.toLowerCase().trim() === 'application/octet-stream';
|
|
162
|
+
if (!neutralAllowed && !isValidMediaContentType(ct)) {
|
|
162
163
|
throw new UploadError('invalid_content_type', `Upload rejected: contentType "${ct}" is not a supported media MIME (expected image/*, video/*, or audio/*).`);
|
|
163
164
|
}
|
|
165
|
+
const { allowNeutralContentType: _allowNeutral, ...rest } = args;
|
|
164
166
|
const id = args.id ?? `upl_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
|
|
165
|
-
const c = startResumableUpload({ ...
|
|
167
|
+
const c = startResumableUpload({ ...rest, id });
|
|
166
168
|
return c.done;
|
|
167
169
|
}
|
|
168
170
|
//# sourceMappingURL=upload.js.map
|
|
@@ -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\"> & { id?: string }\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 MIME, but this is the last gate\n // before Firebase Storage (whose rules reject application/octet-stream anyway).\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 if (!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 id = args.id ?? `upl_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;\n const c = startResumableUpload({ ...args, 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 { 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/upload-core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/ttt-productions/ttt-packages.git",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"prepublishOnly": "npm run clean && npm run build"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ttt-productions/firebase-helpers": "^0.12.
|
|
34
|
+
"@ttt-productions/firebase-helpers": "^0.12.4"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"firebase": ">=12.0.0"
|