@ai-matrx/data 0.22.4 → 0.22.6
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/CHANGELOG.md +8 -2
- package/dist/files/tus.cjs +2 -2
- package/dist/files/tus.cjs.map +1 -1
- package/dist/files/tus.js +2 -2
- package/dist/files/tus.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +314 -0
- package/dist/types.d.ts +314 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.22.
|
|
3
|
+
## 0.22.6
|
|
4
4
|
|
|
5
5
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
-
`git diff npm/data/v0.22.
|
|
6
|
+
`git diff npm/data/v0.22.5..npm/data/v0.22.6 -- apps/shared/data`).
|
|
7
|
+
No source changes intended and no consumer action required.
|
|
8
|
+
|
|
9
|
+
## 0.22.2
|
|
10
|
+
|
|
11
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
12
|
+
`git diff npm/data/v0.22.1..npm/data/v0.22.2 -- apps/shared/data`).
|
|
7
13
|
No source changes intended and no consumer action required.
|
|
8
14
|
|
|
9
15
|
## 0.22.1
|
package/dist/files/tus.cjs
CHANGED
|
@@ -38,7 +38,6 @@ __export(tus_exports, {
|
|
|
38
38
|
tusTransfer: () => tusTransfer
|
|
39
39
|
});
|
|
40
40
|
module.exports = __toCommonJS(tus_exports);
|
|
41
|
-
var tus = __toESM(require("tus-js-client"), 1);
|
|
42
41
|
var TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;
|
|
43
42
|
var TUS_UPLOAD_PATH = "/files/upload/tus";
|
|
44
43
|
function buildUploadMetadataEnvelope(metadata) {
|
|
@@ -164,8 +163,9 @@ async function tusTransfer(file, opts) {
|
|
|
164
163
|
let cldFileId = null;
|
|
165
164
|
let aborted = false;
|
|
166
165
|
try {
|
|
166
|
+
const { Upload } = await import("tus-js-client");
|
|
167
167
|
await new Promise((resolve, reject) => {
|
|
168
|
-
const upload = new
|
|
168
|
+
const upload = new Upload(file, {
|
|
169
169
|
endpoint: opts.endpoint,
|
|
170
170
|
chunkSize: TUS_CHUNK_SIZE_BYTES,
|
|
171
171
|
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
package/dist/files/tus.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/files/tus.ts"],"sourcesContent":["/**\n * @ai-matrx/data/files/tus — the ONE resumable (TUS) upload transport.\n *\n * Origin: `@ai-matrx/media/files/engine/upload/tusUpload.ts` (architecture\n * step 14, D1: only byte transport moves to data). Media keeps the Redux\n * side and the record hydration; every host — the app, the extension, the\n * desktop — rides this one wire:\n *\n * - creation POST carries `X-Idempotency-Key` (one intended upload, one key);\n * - FRESH headers on EVERY request (long uploads outlive a JWT; the\n * organization header must reach every chunk — aidream 8e5ee0b93 400s an\n * unscoped upload);\n * - one validated `metadata_json` envelope, byte-identical to the buffered\n * form field ({@link buildUploadMetadataEnvelope});\n * - automatic resume from the dedicated `mtx-tus-urls` IndexedDB store;\n * - the created file id comes back in `X-Cld-File-Id`, including the\n * completed-session recovery when the final response was lost.\n *\n * Its own subpath so `@ai-matrx/data/files` never loads tus-js-client.\n */\n\nimport * as tus from \"tus-js-client\";\n\n/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */\nexport const TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;\n\n/** The TUS creation route on the files service. */\nexport const TUS_UPLOAD_PATH = \"/files/upload/tus\";\n\n// ─── Shared metadata envelope (buffered ↔ TUS parity) ────────────────────────\n\n/**\n * The ONE builder for the upload metadata JSON object. The buffered path\n * serializes it into the `metadata_json` form field; the TUS path serializes\n * the SAME object into the `metadata_json` Upload-Metadata key. One builder ⇒\n * the two transports can never drift.\n *\n * `scope.organization_id` is the OWNING WORKSPACE of the uploaded file. The\n * server stamps `files.files.organization_id` from it (membership-checked);\n * omitted, the server falls back to the uploader's own organization — a silent\n * mis-filing (2026-08-30). Loud in development, never a hard failure.\n */\nexport function buildUploadMetadataEnvelope(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const envelope: Record<string, unknown> = {\n // Origin tag aids backend log triage when something goes wrong.\n origin: \"cloudUpload\",\n ...(metadata ?? {}),\n };\n if (process.env.NODE_ENV !== \"production\" && !hasUploadOrganization(envelope)) {\n console.warn(\n \"[buildUploadMetadataEnvelope] upload carries no scope.organization_id — \" +\n \"it will be filed in an organization nobody chose. Route this \" +\n \"upload through the host's organization gate before the bytes move.\",\n );\n }\n return envelope;\n}\n\nfunction hasUploadOrganization(envelope: Record<string, unknown>): boolean {\n if (typeof envelope.organization_id === \"string\" && envelope.organization_id) return true;\n const scope = envelope.scope;\n return Boolean(\n scope &&\n typeof scope === \"object\" &&\n typeof (scope as Record<string, unknown>).organization_id === \"string\" &&\n (scope as Record<string, unknown>).organization_id,\n );\n}\n\n// ─── Dedicated resume-URL storage (mtx-tus-urls) ─────────────────────────────\n\nconst URL_DB_NAME = \"mtx-tus-urls\";\nconst URL_STORE = \"urls\";\n\ninterface StoredTusUpload {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n parallelUploadUrls: string[] | null;\n}\n\nlet urlDbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openUrlDb(): Promise<IDBDatabase> {\n if (urlDbPromise) return urlDbPromise;\n urlDbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n if (typeof indexedDB === \"undefined\") {\n reject(new Error(\"[tusUpload] IndexedDB unavailable\"));\n return;\n }\n const req = indexedDB.open(URL_DB_NAME, 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(URL_STORE)) {\n const store = db.createObjectStore(URL_STORE, { keyPath: \"urlStorageKey\" });\n store.createIndex(\"fingerprint\", \"fingerprint\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"[tusUpload] failed to open mtx-tus-urls\"));\n });\n urlDbPromise.catch(() => {\n urlDbPromise = null;\n });\n return urlDbPromise;\n}\n\nfunction idbRequest<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"IndexedDB failed\"));\n });\n}\n\ntype PreviousUpload = Awaited<ReturnType<tus.Upload[\"findPreviousUploads\"]>>[number];\n\n/** tus-js-client HttpStack port (tests inject a mock wire). */\nexport type TusHttpStack = tus.HttpStack;\n/** tus-js-client file reader port (tests: Node builds cannot slice browser Files). */\nexport type TusFileReader = ConstructorParameters<typeof tus.Upload>[1][\"fileReader\"];\n\n/** tus-js-client UrlStorage port. */\nexport type TusUrlStorage = NonNullable<ConstructorParameters<typeof tus.Upload>[1][\"urlStorage\"]>;\n\n/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —\n * deliberately SEPARATE from any recorder chunk journal. */\nexport function createTusUrlStorage(): TusUrlStorage {\n async function write(record: StoredTusUpload | null, deleteKey?: string): Promise<void> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n if (record) tx.objectStore(URL_STORE).put(record);\n else if (deleteKey) tx.objectStore(URL_STORE).delete(deleteKey);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n }\n return {\n async findAllUploads(): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n },\n async findUploadsByFingerprint(fingerprint: string): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(\n tx.objectStore(URL_STORE).index(\"fingerprint\").getAll(fingerprint) as IDBRequest<StoredTusUpload[]>,\n );\n },\n async removeUpload(urlStorageKey: string): Promise<void> {\n await write(null, urlStorageKey);\n },\n async addUpload(fingerprint: string, upload: PreviousUpload): Promise<string> {\n const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;\n await write({\n urlStorageKey,\n fingerprint,\n size: upload.size,\n metadata: upload.metadata,\n creationTime: upload.creationTime,\n uploadUrl: upload.uploadUrl,\n parallelUploadUrls: upload.parallelUploadUrls,\n });\n return urlStorageKey;\n },\n };\n}\n\n/** Read-only summary of a stored TUS resume session (diagnostics surfaces). */\nexport interface StoredTusUploadSummary {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n}\n\n/**\n * List every stored TUS resume-URL entry. Read-only — resuming happens inside\n * {@link tusTransfer}; this only lets management surfaces SHOW a pending\n * session. Returns [] where IndexedDB is unavailable (never throws).\n */\nexport async function listStoredTusUploads(): Promise<StoredTusUploadSummary[]> {\n try {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n return all.map((r) => ({\n urlStorageKey: r.urlStorageKey,\n fingerprint: r.fingerprint,\n size: r.size,\n metadata: r.metadata,\n creationTime: r.creationTime,\n uploadUrl: r.uploadUrl,\n }));\n } catch (err) {\n console.error(\"[tusUpload] listStoredTusUploads failed:\", err);\n return [];\n }\n}\n\n// ─── The transport ───────────────────────────────────────────────────────────\n\nexport interface TusTransferOptions {\n /** Full creation URL (`{filesBase}${TUS_UPLOAD_PATH}`). */\n endpoint: string;\n /** Destination path including the file name. */\n filePath: string;\n /** Caller metadata; wrapped by {@link buildUploadMetadataEnvelope}. */\n metadata?: Record<string, unknown> | undefined;\n /** Optional Upload-Metadata `visibility` (transitional wire input). */\n visibility?: string | undefined;\n /** One intended upload, one key — sent on the creation POST only. */\n idempotencyKey: string;\n /**\n * FRESH request headers, read before EVERY request. Only `Authorization`,\n * `X-Guest-Fingerprint` and `X-Organization-Id` are forwarded.\n */\n headers: () => Promise<Record<string, string | undefined>>;\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n signal?: AbortSignal | undefined;\n /** Injected HttpStack (tests: mock wire, no XHR). */\n httpStack?: TusHttpStack | undefined;\n /** Injected file reader (tests: Node builds cannot slice browser Files). */\n fileReader?: TusFileReader | undefined;\n /** Override URL storage (tests). Default: {@link createTusUrlStorage}. */\n urlStorage?: TusUrlStorage | undefined;\n}\n\nexport type TusTransferErrorCode = \"upload_cancelled\" | \"tus_upload_failed\" | \"tus_missing_file_id\";\n\nexport type TusTransferResult =\n | { ok: true; fileId: string }\n | { ok: false; error: string; errorCode: TusTransferErrorCode };\n\nconst FORWARDED_HEADERS = [\"Authorization\", \"X-Guest-Fingerprint\", \"X-Organization-Id\"] as const;\n\nfunction errorText(err: unknown): string {\n if (err instanceof Error) return err.message;\n return typeof err === \"string\" ? err : String(err);\n}\n\n/**\n * Resumable upload of one file. Never throws — `{ ok: false, errorCode }` on\n * failure. Resumes a previous session for the same file automatically; when the\n * transfer errors but the server already exposed `X-Cld-File-Id`, the completed\n * session is recovered (loudly) instead of failing or re-uploading.\n */\nexport async function tusTransfer(file: File | Blob, opts: TusTransferOptions): Promise<TusTransferResult> {\n const fileName = typeof File !== \"undefined\" && file instanceof File ? file.name : opts.filePath.split(\"/\").pop() ?? \"upload\";\n const metadataEnvelope = buildUploadMetadataEnvelope(opts.metadata);\n let cldFileId: string | null = null;\n let aborted = false;\n\n try {\n await new Promise<void>((resolve, reject) => {\n const upload: tus.Upload = new tus.Upload(file, {\n endpoint: opts.endpoint,\n chunkSize: TUS_CHUNK_SIZE_BYTES,\n retryDelays: [0, 1000, 3000, 5000, 10000],\n removeFingerprintOnSuccess: true,\n urlStorage: opts.urlStorage ?? createTusUrlStorage(),\n ...(opts.httpStack ? { httpStack: opts.httpStack } : {}),\n ...(opts.fileReader ? { fileReader: opts.fileReader } : {}),\n metadata: {\n filename: fileName,\n filepath: opts.filePath,\n // tus-js-client base64-encodes the value per the Upload-Metadata\n // spec; the server merges it exactly like the buffered form field.\n metadata_json: JSON.stringify(metadataEnvelope),\n ...(opts.visibility ? { visibility: opts.visibility } : {}),\n },\n onBeforeRequest: async (req) => {\n const headers = await opts.headers();\n for (const name of FORWARDED_HEADERS) {\n const value = headers[name];\n if (value) req.setHeader(name, value);\n }\n if (req.getMethod() === \"POST\") req.setHeader(\"X-Idempotency-Key\", opts.idempotencyKey);\n },\n onAfterResponse: (_req, res) => {\n const id = res.getHeader(\"X-Cld-File-Id\");\n if (id) cldFileId = id;\n },\n onProgress: (bytesSent, bytesTotal) => {\n opts.onProgress?.(bytesSent, bytesTotal);\n },\n onSuccess: () => resolve(),\n onError: (error) => reject(error),\n });\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n aborted = true;\n reject(new Error(\"Upload cancelled\"));\n return;\n }\n opts.signal.addEventListener(\n \"abort\",\n () => {\n aborted = true;\n void upload.abort();\n reject(new Error(\"Upload cancelled\"));\n },\n { once: true },\n );\n }\n\n void upload\n .findPreviousUploads()\n .then((previous) => {\n const first = previous[0];\n if (first) upload.resumeFromPreviousUpload(first);\n upload.start();\n })\n .catch((err) => {\n console.warn(\"[tusUpload] previous-upload lookup failed — starting fresh:\", err);\n upload.start();\n });\n });\n } catch (err) {\n if (cldFileId && !aborted) {\n console.warn(\n `[tusUpload] transfer reported an error but the server exposed ` +\n `X-Cld-File-Id=${cldFileId} — recovering the completed session.`,\n );\n } else {\n return { ok: false, error: errorText(err), errorCode: aborted ? \"upload_cancelled\" : \"tus_upload_failed\" };\n }\n }\n\n if (!cldFileId) {\n return {\n ok: false,\n error:\n \"TUS upload finished but the server never exposed X-Cld-File-Id — \" +\n \"the file cannot be resolved. Check server CORS expose_headers.\",\n errorCode: \"tus_missing_file_id\",\n };\n }\n return { ok: true, fileId: cldFileId };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/files/tus.ts"],"sourcesContent":["/**\n * @ai-matrx/data/files/tus — the ONE resumable (TUS) upload transport.\n *\n * Origin: `@ai-matrx/media/files/engine/upload/tusUpload.ts` (architecture\n * step 14, D1: only byte transport moves to data). Media keeps the Redux\n * side and the record hydration; every host — the app, the extension, the\n * desktop — rides this one wire:\n *\n * - creation POST carries `X-Idempotency-Key` (one intended upload, one key);\n * - FRESH headers on EVERY request (long uploads outlive a JWT; the\n * organization header must reach every chunk — aidream 8e5ee0b93 400s an\n * unscoped upload);\n * - one validated `metadata_json` envelope, byte-identical to the buffered\n * form field ({@link buildUploadMetadataEnvelope});\n * - automatic resume from the dedicated `mtx-tus-urls` IndexedDB store;\n * - the created file id comes back in `X-Cld-File-Id`, including the\n * completed-session recovery when the final response was lost.\n *\n * Its own subpath so `@ai-matrx/data/files` never loads tus-js-client.\n */\n\n// Types only at import: tus-js-client (~15 kB gzip) loads inside `tusTransfer`, so the store factories\n// and upload helpers that merely import this subpath cost nothing until a file is sent.\nimport type * as tus from \"tus-js-client\";\n\n/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */\nexport const TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;\n\n/** The TUS creation route on the files service. */\nexport const TUS_UPLOAD_PATH = \"/files/upload/tus\";\n\n// ─── Shared metadata envelope (buffered ↔ TUS parity) ────────────────────────\n\n/**\n * The ONE builder for the upload metadata JSON object. The buffered path\n * serializes it into the `metadata_json` form field; the TUS path serializes\n * the SAME object into the `metadata_json` Upload-Metadata key. One builder ⇒\n * the two transports can never drift.\n *\n * `scope.organization_id` is the OWNING WORKSPACE of the uploaded file. The\n * server stamps `files.files.organization_id` from it (membership-checked);\n * omitted, the server falls back to the uploader's own organization — a silent\n * mis-filing (2026-08-30). Loud in development, never a hard failure.\n */\nexport function buildUploadMetadataEnvelope(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const envelope: Record<string, unknown> = {\n // Origin tag aids backend log triage when something goes wrong.\n origin: \"cloudUpload\",\n ...(metadata ?? {}),\n };\n if (process.env.NODE_ENV !== \"production\" && !hasUploadOrganization(envelope)) {\n console.warn(\n \"[buildUploadMetadataEnvelope] upload carries no scope.organization_id — \" +\n \"it will be filed in an organization nobody chose. Route this \" +\n \"upload through the host's organization gate before the bytes move.\",\n );\n }\n return envelope;\n}\n\nfunction hasUploadOrganization(envelope: Record<string, unknown>): boolean {\n if (typeof envelope.organization_id === \"string\" && envelope.organization_id) return true;\n const scope = envelope.scope;\n return Boolean(\n scope &&\n typeof scope === \"object\" &&\n typeof (scope as Record<string, unknown>).organization_id === \"string\" &&\n (scope as Record<string, unknown>).organization_id,\n );\n}\n\n// ─── Dedicated resume-URL storage (mtx-tus-urls) ─────────────────────────────\n\nconst URL_DB_NAME = \"mtx-tus-urls\";\nconst URL_STORE = \"urls\";\n\ninterface StoredTusUpload {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n parallelUploadUrls: string[] | null;\n}\n\nlet urlDbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openUrlDb(): Promise<IDBDatabase> {\n if (urlDbPromise) return urlDbPromise;\n urlDbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n if (typeof indexedDB === \"undefined\") {\n reject(new Error(\"[tusUpload] IndexedDB unavailable\"));\n return;\n }\n const req = indexedDB.open(URL_DB_NAME, 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(URL_STORE)) {\n const store = db.createObjectStore(URL_STORE, { keyPath: \"urlStorageKey\" });\n store.createIndex(\"fingerprint\", \"fingerprint\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"[tusUpload] failed to open mtx-tus-urls\"));\n });\n urlDbPromise.catch(() => {\n urlDbPromise = null;\n });\n return urlDbPromise;\n}\n\nfunction idbRequest<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"IndexedDB failed\"));\n });\n}\n\ntype PreviousUpload = Awaited<ReturnType<tus.Upload[\"findPreviousUploads\"]>>[number];\n\n/** tus-js-client HttpStack port (tests inject a mock wire). */\nexport type TusHttpStack = tus.HttpStack;\n/** tus-js-client file reader port (tests: Node builds cannot slice browser Files). */\nexport type TusFileReader = ConstructorParameters<typeof tus.Upload>[1][\"fileReader\"];\n\n/** tus-js-client UrlStorage port. */\nexport type TusUrlStorage = NonNullable<ConstructorParameters<typeof tus.Upload>[1][\"urlStorage\"]>;\n\n/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —\n * deliberately SEPARATE from any recorder chunk journal. */\nexport function createTusUrlStorage(): TusUrlStorage {\n async function write(record: StoredTusUpload | null, deleteKey?: string): Promise<void> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n if (record) tx.objectStore(URL_STORE).put(record);\n else if (deleteKey) tx.objectStore(URL_STORE).delete(deleteKey);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n }\n return {\n async findAllUploads(): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n },\n async findUploadsByFingerprint(fingerprint: string): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(\n tx.objectStore(URL_STORE).index(\"fingerprint\").getAll(fingerprint) as IDBRequest<StoredTusUpload[]>,\n );\n },\n async removeUpload(urlStorageKey: string): Promise<void> {\n await write(null, urlStorageKey);\n },\n async addUpload(fingerprint: string, upload: PreviousUpload): Promise<string> {\n const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;\n await write({\n urlStorageKey,\n fingerprint,\n size: upload.size,\n metadata: upload.metadata,\n creationTime: upload.creationTime,\n uploadUrl: upload.uploadUrl,\n parallelUploadUrls: upload.parallelUploadUrls,\n });\n return urlStorageKey;\n },\n };\n}\n\n/** Read-only summary of a stored TUS resume session (diagnostics surfaces). */\nexport interface StoredTusUploadSummary {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n}\n\n/**\n * List every stored TUS resume-URL entry. Read-only — resuming happens inside\n * {@link tusTransfer}; this only lets management surfaces SHOW a pending\n * session. Returns [] where IndexedDB is unavailable (never throws).\n */\nexport async function listStoredTusUploads(): Promise<StoredTusUploadSummary[]> {\n try {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n return all.map((r) => ({\n urlStorageKey: r.urlStorageKey,\n fingerprint: r.fingerprint,\n size: r.size,\n metadata: r.metadata,\n creationTime: r.creationTime,\n uploadUrl: r.uploadUrl,\n }));\n } catch (err) {\n console.error(\"[tusUpload] listStoredTusUploads failed:\", err);\n return [];\n }\n}\n\n// ─── The transport ───────────────────────────────────────────────────────────\n\nexport interface TusTransferOptions {\n /** Full creation URL (`{filesBase}${TUS_UPLOAD_PATH}`). */\n endpoint: string;\n /** Destination path including the file name. */\n filePath: string;\n /** Caller metadata; wrapped by {@link buildUploadMetadataEnvelope}. */\n metadata?: Record<string, unknown> | undefined;\n /** Optional Upload-Metadata `visibility` (transitional wire input). */\n visibility?: string | undefined;\n /** One intended upload, one key — sent on the creation POST only. */\n idempotencyKey: string;\n /**\n * FRESH request headers, read before EVERY request. Only `Authorization`,\n * `X-Guest-Fingerprint` and `X-Organization-Id` are forwarded.\n */\n headers: () => Promise<Record<string, string | undefined>>;\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n signal?: AbortSignal | undefined;\n /** Injected HttpStack (tests: mock wire, no XHR). */\n httpStack?: TusHttpStack | undefined;\n /** Injected file reader (tests: Node builds cannot slice browser Files). */\n fileReader?: TusFileReader | undefined;\n /** Override URL storage (tests). Default: {@link createTusUrlStorage}. */\n urlStorage?: TusUrlStorage | undefined;\n}\n\nexport type TusTransferErrorCode = \"upload_cancelled\" | \"tus_upload_failed\" | \"tus_missing_file_id\";\n\nexport type TusTransferResult =\n | { ok: true; fileId: string }\n | { ok: false; error: string; errorCode: TusTransferErrorCode };\n\nconst FORWARDED_HEADERS = [\"Authorization\", \"X-Guest-Fingerprint\", \"X-Organization-Id\"] as const;\n\nfunction errorText(err: unknown): string {\n if (err instanceof Error) return err.message;\n return typeof err === \"string\" ? err : String(err);\n}\n\n/**\n * Resumable upload of one file. Never throws — `{ ok: false, errorCode }` on\n * failure. Resumes a previous session for the same file automatically; when the\n * transfer errors but the server already exposed `X-Cld-File-Id`, the completed\n * session is recovered (loudly) instead of failing or re-uploading.\n */\nexport async function tusTransfer(file: File | Blob, opts: TusTransferOptions): Promise<TusTransferResult> {\n const fileName = typeof File !== \"undefined\" && file instanceof File ? file.name : opts.filePath.split(\"/\").pop() ?? \"upload\";\n const metadataEnvelope = buildUploadMetadataEnvelope(opts.metadata);\n let cldFileId: string | null = null;\n let aborted = false;\n\n try {\n const { Upload } = await import(\"tus-js-client\");\n await new Promise<void>((resolve, reject) => {\n const upload: tus.Upload = new Upload(file, {\n endpoint: opts.endpoint,\n chunkSize: TUS_CHUNK_SIZE_BYTES,\n retryDelays: [0, 1000, 3000, 5000, 10000],\n removeFingerprintOnSuccess: true,\n urlStorage: opts.urlStorage ?? createTusUrlStorage(),\n ...(opts.httpStack ? { httpStack: opts.httpStack } : {}),\n ...(opts.fileReader ? { fileReader: opts.fileReader } : {}),\n metadata: {\n filename: fileName,\n filepath: opts.filePath,\n // tus-js-client base64-encodes the value per the Upload-Metadata\n // spec; the server merges it exactly like the buffered form field.\n metadata_json: JSON.stringify(metadataEnvelope),\n ...(opts.visibility ? { visibility: opts.visibility } : {}),\n },\n onBeforeRequest: async (req) => {\n const headers = await opts.headers();\n for (const name of FORWARDED_HEADERS) {\n const value = headers[name];\n if (value) req.setHeader(name, value);\n }\n if (req.getMethod() === \"POST\") req.setHeader(\"X-Idempotency-Key\", opts.idempotencyKey);\n },\n onAfterResponse: (_req, res) => {\n const id = res.getHeader(\"X-Cld-File-Id\");\n if (id) cldFileId = id;\n },\n onProgress: (bytesSent, bytesTotal) => {\n opts.onProgress?.(bytesSent, bytesTotal);\n },\n onSuccess: () => resolve(),\n onError: (error) => reject(error),\n });\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n aborted = true;\n reject(new Error(\"Upload cancelled\"));\n return;\n }\n opts.signal.addEventListener(\n \"abort\",\n () => {\n aborted = true;\n void upload.abort();\n reject(new Error(\"Upload cancelled\"));\n },\n { once: true },\n );\n }\n\n void upload\n .findPreviousUploads()\n .then((previous) => {\n const first = previous[0];\n if (first) upload.resumeFromPreviousUpload(first);\n upload.start();\n })\n .catch((err) => {\n console.warn(\"[tusUpload] previous-upload lookup failed — starting fresh:\", err);\n upload.start();\n });\n });\n } catch (err) {\n if (cldFileId && !aborted) {\n console.warn(\n `[tusUpload] transfer reported an error but the server exposed ` +\n `X-Cld-File-Id=${cldFileId} — recovering the completed session.`,\n );\n } else {\n return { ok: false, error: errorText(err), errorCode: aborted ? \"upload_cancelled\" : \"tus_upload_failed\" };\n }\n }\n\n if (!cldFileId) {\n return {\n ok: false,\n error:\n \"TUS upload finished but the server never exposed X-Cld-File-Id — \" +\n \"the file cannot be resolved. Check server CORS expose_headers.\",\n errorCode: \"tus_missing_file_id\",\n };\n }\n return { ok: true, fileId: cldFileId };\n}\n"],"mappings":"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","names":[]}
|
package/dist/files/tus.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// src/files/tus.ts
|
|
2
|
-
import * as tus from "tus-js-client";
|
|
3
2
|
var TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;
|
|
4
3
|
var TUS_UPLOAD_PATH = "/files/upload/tus";
|
|
5
4
|
function buildUploadMetadataEnvelope(metadata) {
|
|
@@ -125,8 +124,9 @@ async function tusTransfer(file, opts) {
|
|
|
125
124
|
let cldFileId = null;
|
|
126
125
|
let aborted = false;
|
|
127
126
|
try {
|
|
127
|
+
const { Upload } = await import("tus-js-client");
|
|
128
128
|
await new Promise((resolve, reject) => {
|
|
129
|
-
const upload = new
|
|
129
|
+
const upload = new Upload(file, {
|
|
130
130
|
endpoint: opts.endpoint,
|
|
131
131
|
chunkSize: TUS_CHUNK_SIZE_BYTES,
|
|
132
132
|
retryDelays: [0, 1e3, 3e3, 5e3, 1e4],
|
package/dist/files/tus.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/files/tus.ts"],"sourcesContent":["/**\n * @ai-matrx/data/files/tus — the ONE resumable (TUS) upload transport.\n *\n * Origin: `@ai-matrx/media/files/engine/upload/tusUpload.ts` (architecture\n * step 14, D1: only byte transport moves to data). Media keeps the Redux\n * side and the record hydration; every host — the app, the extension, the\n * desktop — rides this one wire:\n *\n * - creation POST carries `X-Idempotency-Key` (one intended upload, one key);\n * - FRESH headers on EVERY request (long uploads outlive a JWT; the\n * organization header must reach every chunk — aidream 8e5ee0b93 400s an\n * unscoped upload);\n * - one validated `metadata_json` envelope, byte-identical to the buffered\n * form field ({@link buildUploadMetadataEnvelope});\n * - automatic resume from the dedicated `mtx-tus-urls` IndexedDB store;\n * - the created file id comes back in `X-Cld-File-Id`, including the\n * completed-session recovery when the final response was lost.\n *\n * Its own subpath so `@ai-matrx/data/files` never loads tus-js-client.\n */\n\nimport * as tus from \"tus-js-client\";\n\n/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */\nexport const TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;\n\n/** The TUS creation route on the files service. */\nexport const TUS_UPLOAD_PATH = \"/files/upload/tus\";\n\n// ─── Shared metadata envelope (buffered ↔ TUS parity) ────────────────────────\n\n/**\n * The ONE builder for the upload metadata JSON object. The buffered path\n * serializes it into the `metadata_json` form field; the TUS path serializes\n * the SAME object into the `metadata_json` Upload-Metadata key. One builder ⇒\n * the two transports can never drift.\n *\n * `scope.organization_id` is the OWNING WORKSPACE of the uploaded file. The\n * server stamps `files.files.organization_id` from it (membership-checked);\n * omitted, the server falls back to the uploader's own organization — a silent\n * mis-filing (2026-08-30). Loud in development, never a hard failure.\n */\nexport function buildUploadMetadataEnvelope(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const envelope: Record<string, unknown> = {\n // Origin tag aids backend log triage when something goes wrong.\n origin: \"cloudUpload\",\n ...(metadata ?? {}),\n };\n if (process.env.NODE_ENV !== \"production\" && !hasUploadOrganization(envelope)) {\n console.warn(\n \"[buildUploadMetadataEnvelope] upload carries no scope.organization_id — \" +\n \"it will be filed in an organization nobody chose. Route this \" +\n \"upload through the host's organization gate before the bytes move.\",\n );\n }\n return envelope;\n}\n\nfunction hasUploadOrganization(envelope: Record<string, unknown>): boolean {\n if (typeof envelope.organization_id === \"string\" && envelope.organization_id) return true;\n const scope = envelope.scope;\n return Boolean(\n scope &&\n typeof scope === \"object\" &&\n typeof (scope as Record<string, unknown>).organization_id === \"string\" &&\n (scope as Record<string, unknown>).organization_id,\n );\n}\n\n// ─── Dedicated resume-URL storage (mtx-tus-urls) ─────────────────────────────\n\nconst URL_DB_NAME = \"mtx-tus-urls\";\nconst URL_STORE = \"urls\";\n\ninterface StoredTusUpload {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n parallelUploadUrls: string[] | null;\n}\n\nlet urlDbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openUrlDb(): Promise<IDBDatabase> {\n if (urlDbPromise) return urlDbPromise;\n urlDbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n if (typeof indexedDB === \"undefined\") {\n reject(new Error(\"[tusUpload] IndexedDB unavailable\"));\n return;\n }\n const req = indexedDB.open(URL_DB_NAME, 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(URL_STORE)) {\n const store = db.createObjectStore(URL_STORE, { keyPath: \"urlStorageKey\" });\n store.createIndex(\"fingerprint\", \"fingerprint\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"[tusUpload] failed to open mtx-tus-urls\"));\n });\n urlDbPromise.catch(() => {\n urlDbPromise = null;\n });\n return urlDbPromise;\n}\n\nfunction idbRequest<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"IndexedDB failed\"));\n });\n}\n\ntype PreviousUpload = Awaited<ReturnType<tus.Upload[\"findPreviousUploads\"]>>[number];\n\n/** tus-js-client HttpStack port (tests inject a mock wire). */\nexport type TusHttpStack = tus.HttpStack;\n/** tus-js-client file reader port (tests: Node builds cannot slice browser Files). */\nexport type TusFileReader = ConstructorParameters<typeof tus.Upload>[1][\"fileReader\"];\n\n/** tus-js-client UrlStorage port. */\nexport type TusUrlStorage = NonNullable<ConstructorParameters<typeof tus.Upload>[1][\"urlStorage\"]>;\n\n/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —\n * deliberately SEPARATE from any recorder chunk journal. */\nexport function createTusUrlStorage(): TusUrlStorage {\n async function write(record: StoredTusUpload | null, deleteKey?: string): Promise<void> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n if (record) tx.objectStore(URL_STORE).put(record);\n else if (deleteKey) tx.objectStore(URL_STORE).delete(deleteKey);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n }\n return {\n async findAllUploads(): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n },\n async findUploadsByFingerprint(fingerprint: string): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(\n tx.objectStore(URL_STORE).index(\"fingerprint\").getAll(fingerprint) as IDBRequest<StoredTusUpload[]>,\n );\n },\n async removeUpload(urlStorageKey: string): Promise<void> {\n await write(null, urlStorageKey);\n },\n async addUpload(fingerprint: string, upload: PreviousUpload): Promise<string> {\n const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;\n await write({\n urlStorageKey,\n fingerprint,\n size: upload.size,\n metadata: upload.metadata,\n creationTime: upload.creationTime,\n uploadUrl: upload.uploadUrl,\n parallelUploadUrls: upload.parallelUploadUrls,\n });\n return urlStorageKey;\n },\n };\n}\n\n/** Read-only summary of a stored TUS resume session (diagnostics surfaces). */\nexport interface StoredTusUploadSummary {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n}\n\n/**\n * List every stored TUS resume-URL entry. Read-only — resuming happens inside\n * {@link tusTransfer}; this only lets management surfaces SHOW a pending\n * session. Returns [] where IndexedDB is unavailable (never throws).\n */\nexport async function listStoredTusUploads(): Promise<StoredTusUploadSummary[]> {\n try {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n return all.map((r) => ({\n urlStorageKey: r.urlStorageKey,\n fingerprint: r.fingerprint,\n size: r.size,\n metadata: r.metadata,\n creationTime: r.creationTime,\n uploadUrl: r.uploadUrl,\n }));\n } catch (err) {\n console.error(\"[tusUpload] listStoredTusUploads failed:\", err);\n return [];\n }\n}\n\n// ─── The transport ───────────────────────────────────────────────────────────\n\nexport interface TusTransferOptions {\n /** Full creation URL (`{filesBase}${TUS_UPLOAD_PATH}`). */\n endpoint: string;\n /** Destination path including the file name. */\n filePath: string;\n /** Caller metadata; wrapped by {@link buildUploadMetadataEnvelope}. */\n metadata?: Record<string, unknown> | undefined;\n /** Optional Upload-Metadata `visibility` (transitional wire input). */\n visibility?: string | undefined;\n /** One intended upload, one key — sent on the creation POST only. */\n idempotencyKey: string;\n /**\n * FRESH request headers, read before EVERY request. Only `Authorization`,\n * `X-Guest-Fingerprint` and `X-Organization-Id` are forwarded.\n */\n headers: () => Promise<Record<string, string | undefined>>;\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n signal?: AbortSignal | undefined;\n /** Injected HttpStack (tests: mock wire, no XHR). */\n httpStack?: TusHttpStack | undefined;\n /** Injected file reader (tests: Node builds cannot slice browser Files). */\n fileReader?: TusFileReader | undefined;\n /** Override URL storage (tests). Default: {@link createTusUrlStorage}. */\n urlStorage?: TusUrlStorage | undefined;\n}\n\nexport type TusTransferErrorCode = \"upload_cancelled\" | \"tus_upload_failed\" | \"tus_missing_file_id\";\n\nexport type TusTransferResult =\n | { ok: true; fileId: string }\n | { ok: false; error: string; errorCode: TusTransferErrorCode };\n\nconst FORWARDED_HEADERS = [\"Authorization\", \"X-Guest-Fingerprint\", \"X-Organization-Id\"] as const;\n\nfunction errorText(err: unknown): string {\n if (err instanceof Error) return err.message;\n return typeof err === \"string\" ? err : String(err);\n}\n\n/**\n * Resumable upload of one file. Never throws — `{ ok: false, errorCode }` on\n * failure. Resumes a previous session for the same file automatically; when the\n * transfer errors but the server already exposed `X-Cld-File-Id`, the completed\n * session is recovered (loudly) instead of failing or re-uploading.\n */\nexport async function tusTransfer(file: File | Blob, opts: TusTransferOptions): Promise<TusTransferResult> {\n const fileName = typeof File !== \"undefined\" && file instanceof File ? file.name : opts.filePath.split(\"/\").pop() ?? \"upload\";\n const metadataEnvelope = buildUploadMetadataEnvelope(opts.metadata);\n let cldFileId: string | null = null;\n let aborted = false;\n\n try {\n await new Promise<void>((resolve, reject) => {\n const upload: tus.Upload = new tus.Upload(file, {\n endpoint: opts.endpoint,\n chunkSize: TUS_CHUNK_SIZE_BYTES,\n retryDelays: [0, 1000, 3000, 5000, 10000],\n removeFingerprintOnSuccess: true,\n urlStorage: opts.urlStorage ?? createTusUrlStorage(),\n ...(opts.httpStack ? { httpStack: opts.httpStack } : {}),\n ...(opts.fileReader ? { fileReader: opts.fileReader } : {}),\n metadata: {\n filename: fileName,\n filepath: opts.filePath,\n // tus-js-client base64-encodes the value per the Upload-Metadata\n // spec; the server merges it exactly like the buffered form field.\n metadata_json: JSON.stringify(metadataEnvelope),\n ...(opts.visibility ? { visibility: opts.visibility } : {}),\n },\n onBeforeRequest: async (req) => {\n const headers = await opts.headers();\n for (const name of FORWARDED_HEADERS) {\n const value = headers[name];\n if (value) req.setHeader(name, value);\n }\n if (req.getMethod() === \"POST\") req.setHeader(\"X-Idempotency-Key\", opts.idempotencyKey);\n },\n onAfterResponse: (_req, res) => {\n const id = res.getHeader(\"X-Cld-File-Id\");\n if (id) cldFileId = id;\n },\n onProgress: (bytesSent, bytesTotal) => {\n opts.onProgress?.(bytesSent, bytesTotal);\n },\n onSuccess: () => resolve(),\n onError: (error) => reject(error),\n });\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n aborted = true;\n reject(new Error(\"Upload cancelled\"));\n return;\n }\n opts.signal.addEventListener(\n \"abort\",\n () => {\n aborted = true;\n void upload.abort();\n reject(new Error(\"Upload cancelled\"));\n },\n { once: true },\n );\n }\n\n void upload\n .findPreviousUploads()\n .then((previous) => {\n const first = previous[0];\n if (first) upload.resumeFromPreviousUpload(first);\n upload.start();\n })\n .catch((err) => {\n console.warn(\"[tusUpload] previous-upload lookup failed — starting fresh:\", err);\n upload.start();\n });\n });\n } catch (err) {\n if (cldFileId && !aborted) {\n console.warn(\n `[tusUpload] transfer reported an error but the server exposed ` +\n `X-Cld-File-Id=${cldFileId} — recovering the completed session.`,\n );\n } else {\n return { ok: false, error: errorText(err), errorCode: aborted ? \"upload_cancelled\" : \"tus_upload_failed\" };\n }\n }\n\n if (!cldFileId) {\n return {\n ok: false,\n error:\n \"TUS upload finished but the server never exposed X-Cld-File-Id — \" +\n \"the file cannot be resolved. Check server CORS expose_headers.\",\n errorCode: \"tus_missing_file_id\",\n };\n }\n return { ok: true, fileId: cldFileId };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/files/tus.ts"],"sourcesContent":["/**\n * @ai-matrx/data/files/tus — the ONE resumable (TUS) upload transport.\n *\n * Origin: `@ai-matrx/media/files/engine/upload/tusUpload.ts` (architecture\n * step 14, D1: only byte transport moves to data). Media keeps the Redux\n * side and the record hydration; every host — the app, the extension, the\n * desktop — rides this one wire:\n *\n * - creation POST carries `X-Idempotency-Key` (one intended upload, one key);\n * - FRESH headers on EVERY request (long uploads outlive a JWT; the\n * organization header must reach every chunk — aidream 8e5ee0b93 400s an\n * unscoped upload);\n * - one validated `metadata_json` envelope, byte-identical to the buffered\n * form field ({@link buildUploadMetadataEnvelope});\n * - automatic resume from the dedicated `mtx-tus-urls` IndexedDB store;\n * - the created file id comes back in `X-Cld-File-Id`, including the\n * completed-session recovery when the final response was lost.\n *\n * Its own subpath so `@ai-matrx/data/files` never loads tus-js-client.\n */\n\n// Types only at import: tus-js-client (~15 kB gzip) loads inside `tusTransfer`, so the store factories\n// and upload helpers that merely import this subpath cost nothing until a file is sent.\nimport type * as tus from \"tus-js-client\";\n\n/** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */\nexport const TUS_CHUNK_SIZE_BYTES = 16 * 1024 * 1024;\n\n/** The TUS creation route on the files service. */\nexport const TUS_UPLOAD_PATH = \"/files/upload/tus\";\n\n// ─── Shared metadata envelope (buffered ↔ TUS parity) ────────────────────────\n\n/**\n * The ONE builder for the upload metadata JSON object. The buffered path\n * serializes it into the `metadata_json` form field; the TUS path serializes\n * the SAME object into the `metadata_json` Upload-Metadata key. One builder ⇒\n * the two transports can never drift.\n *\n * `scope.organization_id` is the OWNING WORKSPACE of the uploaded file. The\n * server stamps `files.files.organization_id` from it (membership-checked);\n * omitted, the server falls back to the uploader's own organization — a silent\n * mis-filing (2026-08-30). Loud in development, never a hard failure.\n */\nexport function buildUploadMetadataEnvelope(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> {\n const envelope: Record<string, unknown> = {\n // Origin tag aids backend log triage when something goes wrong.\n origin: \"cloudUpload\",\n ...(metadata ?? {}),\n };\n if (process.env.NODE_ENV !== \"production\" && !hasUploadOrganization(envelope)) {\n console.warn(\n \"[buildUploadMetadataEnvelope] upload carries no scope.organization_id — \" +\n \"it will be filed in an organization nobody chose. Route this \" +\n \"upload through the host's organization gate before the bytes move.\",\n );\n }\n return envelope;\n}\n\nfunction hasUploadOrganization(envelope: Record<string, unknown>): boolean {\n if (typeof envelope.organization_id === \"string\" && envelope.organization_id) return true;\n const scope = envelope.scope;\n return Boolean(\n scope &&\n typeof scope === \"object\" &&\n typeof (scope as Record<string, unknown>).organization_id === \"string\" &&\n (scope as Record<string, unknown>).organization_id,\n );\n}\n\n// ─── Dedicated resume-URL storage (mtx-tus-urls) ─────────────────────────────\n\nconst URL_DB_NAME = \"mtx-tus-urls\";\nconst URL_STORE = \"urls\";\n\ninterface StoredTusUpload {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n parallelUploadUrls: string[] | null;\n}\n\nlet urlDbPromise: Promise<IDBDatabase> | null = null;\n\nfunction openUrlDb(): Promise<IDBDatabase> {\n if (urlDbPromise) return urlDbPromise;\n urlDbPromise = new Promise<IDBDatabase>((resolve, reject) => {\n if (typeof indexedDB === \"undefined\") {\n reject(new Error(\"[tusUpload] IndexedDB unavailable\"));\n return;\n }\n const req = indexedDB.open(URL_DB_NAME, 1);\n req.onupgradeneeded = () => {\n const db = req.result;\n if (!db.objectStoreNames.contains(URL_STORE)) {\n const store = db.createObjectStore(URL_STORE, { keyPath: \"urlStorageKey\" });\n store.createIndex(\"fingerprint\", \"fingerprint\", { unique: false });\n }\n };\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"[tusUpload] failed to open mtx-tus-urls\"));\n });\n urlDbPromise.catch(() => {\n urlDbPromise = null;\n });\n return urlDbPromise;\n}\n\nfunction idbRequest<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error ?? new Error(\"IndexedDB failed\"));\n });\n}\n\ntype PreviousUpload = Awaited<ReturnType<tus.Upload[\"findPreviousUploads\"]>>[number];\n\n/** tus-js-client HttpStack port (tests inject a mock wire). */\nexport type TusHttpStack = tus.HttpStack;\n/** tus-js-client file reader port (tests: Node builds cannot slice browser Files). */\nexport type TusFileReader = ConstructorParameters<typeof tus.Upload>[1][\"fileReader\"];\n\n/** tus-js-client UrlStorage port. */\nexport type TusUrlStorage = NonNullable<ConstructorParameters<typeof tus.Upload>[1][\"urlStorage\"]>;\n\n/** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —\n * deliberately SEPARATE from any recorder chunk journal. */\nexport function createTusUrlStorage(): TusUrlStorage {\n async function write(record: StoredTusUpload | null, deleteKey?: string): Promise<void> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readwrite\");\n if (record) tx.objectStore(URL_STORE).put(record);\n else if (deleteKey) tx.objectStore(URL_STORE).delete(deleteKey);\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error ?? new Error(\"IndexedDB tx failed\"));\n });\n }\n return {\n async findAllUploads(): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n },\n async findUploadsByFingerprint(fingerprint: string): Promise<PreviousUpload[]> {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n return idbRequest(\n tx.objectStore(URL_STORE).index(\"fingerprint\").getAll(fingerprint) as IDBRequest<StoredTusUpload[]>,\n );\n },\n async removeUpload(urlStorageKey: string): Promise<void> {\n await write(null, urlStorageKey);\n },\n async addUpload(fingerprint: string, upload: PreviousUpload): Promise<string> {\n const urlStorageKey = `tus::${fingerprint}::${Date.now().toString(36)}`;\n await write({\n urlStorageKey,\n fingerprint,\n size: upload.size,\n metadata: upload.metadata,\n creationTime: upload.creationTime,\n uploadUrl: upload.uploadUrl,\n parallelUploadUrls: upload.parallelUploadUrls,\n });\n return urlStorageKey;\n },\n };\n}\n\n/** Read-only summary of a stored TUS resume session (diagnostics surfaces). */\nexport interface StoredTusUploadSummary {\n urlStorageKey: string;\n fingerprint: string;\n size: number | null;\n metadata: Record<string, string>;\n creationTime: string;\n uploadUrl: string | null;\n}\n\n/**\n * List every stored TUS resume-URL entry. Read-only — resuming happens inside\n * {@link tusTransfer}; this only lets management surfaces SHOW a pending\n * session. Returns [] where IndexedDB is unavailable (never throws).\n */\nexport async function listStoredTusUploads(): Promise<StoredTusUploadSummary[]> {\n try {\n const db = await openUrlDb();\n const tx = db.transaction(URL_STORE, \"readonly\");\n const all = await idbRequest(tx.objectStore(URL_STORE).getAll() as IDBRequest<StoredTusUpload[]>);\n return all.map((r) => ({\n urlStorageKey: r.urlStorageKey,\n fingerprint: r.fingerprint,\n size: r.size,\n metadata: r.metadata,\n creationTime: r.creationTime,\n uploadUrl: r.uploadUrl,\n }));\n } catch (err) {\n console.error(\"[tusUpload] listStoredTusUploads failed:\", err);\n return [];\n }\n}\n\n// ─── The transport ───────────────────────────────────────────────────────────\n\nexport interface TusTransferOptions {\n /** Full creation URL (`{filesBase}${TUS_UPLOAD_PATH}`). */\n endpoint: string;\n /** Destination path including the file name. */\n filePath: string;\n /** Caller metadata; wrapped by {@link buildUploadMetadataEnvelope}. */\n metadata?: Record<string, unknown> | undefined;\n /** Optional Upload-Metadata `visibility` (transitional wire input). */\n visibility?: string | undefined;\n /** One intended upload, one key — sent on the creation POST only. */\n idempotencyKey: string;\n /**\n * FRESH request headers, read before EVERY request. Only `Authorization`,\n * `X-Guest-Fingerprint` and `X-Organization-Id` are forwarded.\n */\n headers: () => Promise<Record<string, string | undefined>>;\n onProgress?: ((loaded: number, total: number) => void) | undefined;\n signal?: AbortSignal | undefined;\n /** Injected HttpStack (tests: mock wire, no XHR). */\n httpStack?: TusHttpStack | undefined;\n /** Injected file reader (tests: Node builds cannot slice browser Files). */\n fileReader?: TusFileReader | undefined;\n /** Override URL storage (tests). Default: {@link createTusUrlStorage}. */\n urlStorage?: TusUrlStorage | undefined;\n}\n\nexport type TusTransferErrorCode = \"upload_cancelled\" | \"tus_upload_failed\" | \"tus_missing_file_id\";\n\nexport type TusTransferResult =\n | { ok: true; fileId: string }\n | { ok: false; error: string; errorCode: TusTransferErrorCode };\n\nconst FORWARDED_HEADERS = [\"Authorization\", \"X-Guest-Fingerprint\", \"X-Organization-Id\"] as const;\n\nfunction errorText(err: unknown): string {\n if (err instanceof Error) return err.message;\n return typeof err === \"string\" ? err : String(err);\n}\n\n/**\n * Resumable upload of one file. Never throws — `{ ok: false, errorCode }` on\n * failure. Resumes a previous session for the same file automatically; when the\n * transfer errors but the server already exposed `X-Cld-File-Id`, the completed\n * session is recovered (loudly) instead of failing or re-uploading.\n */\nexport async function tusTransfer(file: File | Blob, opts: TusTransferOptions): Promise<TusTransferResult> {\n const fileName = typeof File !== \"undefined\" && file instanceof File ? file.name : opts.filePath.split(\"/\").pop() ?? \"upload\";\n const metadataEnvelope = buildUploadMetadataEnvelope(opts.metadata);\n let cldFileId: string | null = null;\n let aborted = false;\n\n try {\n const { Upload } = await import(\"tus-js-client\");\n await new Promise<void>((resolve, reject) => {\n const upload: tus.Upload = new Upload(file, {\n endpoint: opts.endpoint,\n chunkSize: TUS_CHUNK_SIZE_BYTES,\n retryDelays: [0, 1000, 3000, 5000, 10000],\n removeFingerprintOnSuccess: true,\n urlStorage: opts.urlStorage ?? createTusUrlStorage(),\n ...(opts.httpStack ? { httpStack: opts.httpStack } : {}),\n ...(opts.fileReader ? { fileReader: opts.fileReader } : {}),\n metadata: {\n filename: fileName,\n filepath: opts.filePath,\n // tus-js-client base64-encodes the value per the Upload-Metadata\n // spec; the server merges it exactly like the buffered form field.\n metadata_json: JSON.stringify(metadataEnvelope),\n ...(opts.visibility ? { visibility: opts.visibility } : {}),\n },\n onBeforeRequest: async (req) => {\n const headers = await opts.headers();\n for (const name of FORWARDED_HEADERS) {\n const value = headers[name];\n if (value) req.setHeader(name, value);\n }\n if (req.getMethod() === \"POST\") req.setHeader(\"X-Idempotency-Key\", opts.idempotencyKey);\n },\n onAfterResponse: (_req, res) => {\n const id = res.getHeader(\"X-Cld-File-Id\");\n if (id) cldFileId = id;\n },\n onProgress: (bytesSent, bytesTotal) => {\n opts.onProgress?.(bytesSent, bytesTotal);\n },\n onSuccess: () => resolve(),\n onError: (error) => reject(error),\n });\n\n if (opts.signal) {\n if (opts.signal.aborted) {\n aborted = true;\n reject(new Error(\"Upload cancelled\"));\n return;\n }\n opts.signal.addEventListener(\n \"abort\",\n () => {\n aborted = true;\n void upload.abort();\n reject(new Error(\"Upload cancelled\"));\n },\n { once: true },\n );\n }\n\n void upload\n .findPreviousUploads()\n .then((previous) => {\n const first = previous[0];\n if (first) upload.resumeFromPreviousUpload(first);\n upload.start();\n })\n .catch((err) => {\n console.warn(\"[tusUpload] previous-upload lookup failed — starting fresh:\", err);\n upload.start();\n });\n });\n } catch (err) {\n if (cldFileId && !aborted) {\n console.warn(\n `[tusUpload] transfer reported an error but the server exposed ` +\n `X-Cld-File-Id=${cldFileId} — recovering the completed session.`,\n );\n } else {\n return { ok: false, error: errorText(err), errorCode: aborted ? \"upload_cancelled\" : \"tus_upload_failed\" };\n }\n }\n\n if (!cldFileId) {\n return {\n ok: false,\n error:\n \"TUS upload finished but the server never exposed X-Cld-File-Id — \" +\n \"the file cannot be resolved. Check server CORS expose_headers.\",\n errorCode: \"tus_missing_file_id\",\n };\n }\n return { ok: true, fileId: cldFileId };\n}\n"],"mappings":";AA0BO,IAAM,uBAAuB,KAAK,OAAO;AAGzC,IAAM,kBAAkB;AAexB,SAAS,4BACd,UACyB;AACzB,QAAM,WAAoC;AAAA;AAAA,IAExC,QAAQ;AAAA,IACR,GAAI,YAAY,CAAC;AAAA,EACnB;AACA,MAAI,yBAAyB,gBAAgB,CAAC,sBAAsB,QAAQ,GAAG;AAC7E,YAAQ;AAAA,MACN;AAAA,IAGF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,UAA4C;AACzE,MAAI,OAAO,SAAS,oBAAoB,YAAY,SAAS,gBAAiB,QAAO;AACrF,QAAM,QAAQ,SAAS;AACvB,SAAO;AAAA,IACL,SACE,OAAO,UAAU,YACjB,OAAQ,MAAkC,oBAAoB,YAC7D,MAAkC;AAAA,EACvC;AACF;AAIA,IAAM,cAAc;AACpB,IAAM,YAAY;AAYlB,IAAI,eAA4C;AAEhD,SAAS,YAAkC;AACzC,MAAI,aAAc,QAAO;AACzB,iBAAe,IAAI,QAAqB,CAAC,SAAS,WAAW;AAC3D,QAAI,OAAO,cAAc,aAAa;AACpC,aAAO,IAAI,MAAM,mCAAmC,CAAC;AACrD;AAAA,IACF;AACA,UAAM,MAAM,UAAU,KAAK,aAAa,CAAC;AACzC,QAAI,kBAAkB,MAAM;AAC1B,YAAM,KAAK,IAAI;AACf,UAAI,CAAC,GAAG,iBAAiB,SAAS,SAAS,GAAG;AAC5C,cAAM,QAAQ,GAAG,kBAAkB,WAAW,EAAE,SAAS,gBAAgB,CAAC;AAC1E,cAAM,YAAY,eAAe,eAAe,EAAE,QAAQ,MAAM,CAAC;AAAA,MACnE;AAAA,IACF;AACA,QAAI,YAAY,MAAM,QAAQ,IAAI,MAAM;AACxC,QAAI,UAAU,MAAM,OAAO,IAAI,SAAS,IAAI,MAAM,yCAAyC,CAAC;AAAA,EAC9F,CAAC;AACD,eAAa,MAAM,MAAM;AACvB,mBAAe;AAAA,EACjB,CAAC;AACD,SAAO;AACT;AAEA,SAAS,WAAc,KAAgC;AACrD,SAAO,IAAI,QAAW,CAAC,SAAS,WAAW;AACzC,QAAI,YAAY,MAAM,QAAQ,IAAI,MAAM;AACxC,QAAI,UAAU,MAAM,OAAO,IAAI,SAAS,IAAI,MAAM,kBAAkB,CAAC;AAAA,EACvE,CAAC;AACH;AAcO,SAAS,sBAAqC;AACnD,iBAAe,MAAM,QAAgC,WAAmC;AACtF,UAAM,KAAK,MAAM,UAAU;AAC3B,UAAM,KAAK,GAAG,YAAY,WAAW,WAAW;AAChD,QAAI,OAAQ,IAAG,YAAY,SAAS,EAAE,IAAI,MAAM;AAAA,aACvC,UAAW,IAAG,YAAY,SAAS,EAAE,OAAO,SAAS;AAC9D,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,SAAG,aAAa,MAAM,QAAQ;AAC9B,SAAG,UAAU,MAAM,OAAO,GAAG,SAAS,IAAI,MAAM,qBAAqB,CAAC;AAAA,IACxE,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL,MAAM,iBAA4C;AAChD,YAAM,KAAK,MAAM,UAAU;AAC3B,YAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,aAAO,WAAW,GAAG,YAAY,SAAS,EAAE,OAAO,CAAkC;AAAA,IACvF;AAAA,IACA,MAAM,yBAAyB,aAAgD;AAC7E,YAAM,KAAK,MAAM,UAAU;AAC3B,YAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,aAAO;AAAA,QACL,GAAG,YAAY,SAAS,EAAE,MAAM,aAAa,EAAE,OAAO,WAAW;AAAA,MACnE;AAAA,IACF;AAAA,IACA,MAAM,aAAa,eAAsC;AACvD,YAAM,MAAM,MAAM,aAAa;AAAA,IACjC;AAAA,IACA,MAAM,UAAU,aAAqB,QAAyC;AAC5E,YAAM,gBAAgB,QAAQ,WAAW,KAAK,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC;AACrE,YAAM,MAAM;AAAA,QACV;AAAA,QACA;AAAA,QACA,MAAM,OAAO;AAAA,QACb,UAAU,OAAO;AAAA,QACjB,cAAc,OAAO;AAAA,QACrB,WAAW,OAAO;AAAA,QAClB,oBAAoB,OAAO;AAAA,MAC7B,CAAC;AACD,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAiBA,eAAsB,uBAA0D;AAC9E,MAAI;AACF,UAAM,KAAK,MAAM,UAAU;AAC3B,UAAM,KAAK,GAAG,YAAY,WAAW,UAAU;AAC/C,UAAM,MAAM,MAAM,WAAW,GAAG,YAAY,SAAS,EAAE,OAAO,CAAkC;AAChG,WAAO,IAAI,IAAI,CAAC,OAAO;AAAA,MACrB,eAAe,EAAE;AAAA,MACjB,aAAa,EAAE;AAAA,MACf,MAAM,EAAE;AAAA,MACR,UAAU,EAAE;AAAA,MACZ,cAAc,EAAE;AAAA,MAChB,WAAW,EAAE;AAAA,IACf,EAAE;AAAA,EACJ,SAAS,KAAK;AACZ,YAAQ,MAAM,4CAA4C,GAAG;AAC7D,WAAO,CAAC;AAAA,EACV;AACF;AAoCA,IAAM,oBAAoB,CAAC,iBAAiB,uBAAuB,mBAAmB;AAEtF,SAAS,UAAU,KAAsB;AACvC,MAAI,eAAe,MAAO,QAAO,IAAI;AACrC,SAAO,OAAO,QAAQ,WAAW,MAAM,OAAO,GAAG;AACnD;AAQA,eAAsB,YAAY,MAAmB,MAAsD;AACzG,QAAM,WAAW,OAAO,SAAS,eAAe,gBAAgB,OAAO,KAAK,OAAO,KAAK,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK;AACrH,QAAM,mBAAmB,4BAA4B,KAAK,QAAQ;AAClE,MAAI,YAA2B;AAC/B,MAAI,UAAU;AAEd,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,OAAO,eAAe;AAC/C,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,YAAM,SAAqB,IAAI,OAAO,MAAM;AAAA,QAC1C,UAAU,KAAK;AAAA,QACf,WAAW;AAAA,QACX,aAAa,CAAC,GAAG,KAAM,KAAM,KAAM,GAAK;AAAA,QACxC,4BAA4B;AAAA,QAC5B,YAAY,KAAK,cAAc,oBAAoB;AAAA,QACnD,GAAI,KAAK,YAAY,EAAE,WAAW,KAAK,UAAU,IAAI,CAAC;AAAA,QACtD,GAAI,KAAK,aAAa,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;AAAA,QACzD,UAAU;AAAA,UACR,UAAU;AAAA,UACV,UAAU,KAAK;AAAA;AAAA;AAAA,UAGf,eAAe,KAAK,UAAU,gBAAgB;AAAA,UAC9C,GAAI,KAAK,aAAa,EAAE,YAAY,KAAK,WAAW,IAAI,CAAC;AAAA,QAC3D;AAAA,QACA,iBAAiB,OAAO,QAAQ;AAC9B,gBAAM,UAAU,MAAM,KAAK,QAAQ;AACnC,qBAAW,QAAQ,mBAAmB;AACpC,kBAAM,QAAQ,QAAQ,IAAI;AAC1B,gBAAI,MAAO,KAAI,UAAU,MAAM,KAAK;AAAA,UACtC;AACA,cAAI,IAAI,UAAU,MAAM,OAAQ,KAAI,UAAU,qBAAqB,KAAK,cAAc;AAAA,QACxF;AAAA,QACA,iBAAiB,CAAC,MAAM,QAAQ;AAC9B,gBAAM,KAAK,IAAI,UAAU,eAAe;AACxC,cAAI,GAAI,aAAY;AAAA,QACtB;AAAA,QACA,YAAY,CAAC,WAAW,eAAe;AACrC,eAAK,aAAa,WAAW,UAAU;AAAA,QACzC;AAAA,QACA,WAAW,MAAM,QAAQ;AAAA,QACzB,SAAS,CAAC,UAAU,OAAO,KAAK;AAAA,MAClC,CAAC;AAED,UAAI,KAAK,QAAQ;AACf,YAAI,KAAK,OAAO,SAAS;AACvB,oBAAU;AACV,iBAAO,IAAI,MAAM,kBAAkB,CAAC;AACpC;AAAA,QACF;AACA,aAAK,OAAO;AAAA,UACV;AAAA,UACA,MAAM;AACJ,sBAAU;AACV,iBAAK,OAAO,MAAM;AAClB,mBAAO,IAAI,MAAM,kBAAkB,CAAC;AAAA,UACtC;AAAA,UACA,EAAE,MAAM,KAAK;AAAA,QACf;AAAA,MACF;AAEA,WAAK,OACF,oBAAoB,EACpB,KAAK,CAAC,aAAa;AAClB,cAAM,QAAQ,SAAS,CAAC;AACxB,YAAI,MAAO,QAAO,yBAAyB,KAAK;AAChD,eAAO,MAAM;AAAA,MACf,CAAC,EACA,MAAM,CAAC,QAAQ;AACd,gBAAQ,KAAK,oEAA+D,GAAG;AAC/E,eAAO,MAAM;AAAA,MACf,CAAC;AAAA,IACL,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,QAAI,aAAa,CAAC,SAAS;AACzB,cAAQ;AAAA,QACN,+EACmB,SAAS;AAAA,MAC9B;AAAA,IACF,OAAO;AACL,aAAO,EAAE,IAAI,OAAO,OAAO,UAAU,GAAG,GAAG,WAAW,UAAU,qBAAqB,oBAAoB;AAAA,IAC3G;AAAA,EACF;AAEA,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OACE;AAAA,MAEF,WAAW;AAAA,IACb;AAAA,EACF;AACA,SAAO,EAAE,IAAI,MAAM,QAAQ,UAAU;AACvC;","names":[]}
|