@ai-matrx/media 0.10.7 → 0.12.0

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # @ai-matrx/media
2
2
 
3
+
4
+ ## 0.12.0 — upload transport rides @ai-matrx/data
5
+
6
+ - The TUS wire (`tus-js-client`, the `mtx-tus-urls` resume store, `buildUploadMetadataEnvelope`) and
7
+ the upload dedup guard now live in `@ai-matrx/data` (`./files/tus`, `./files`); `tusUploadRaw`
8
+ binds the host's fresh headers and base URL and hydrates the row — same signature, same result.
9
+ `upload/tusUpload` and `upload/uploadDedupGuard` re-export the moved names (architecture step 14, D1).
10
+ - `FileUrls` is data's type; `fileUrls(fileId)` here is the one caller-facing signature, built on
11
+ data's `buildFileUrls(filesBaseUrl, fileId)`.
12
+ - `tus-js-client` is no longer a direct dependency (it arrives through `@ai-matrx/data`).
13
+
14
+ Consumer action: none — every import path and signature is unchanged. Needs `@ai-matrx/data` ≥ 0.21.0.
15
+
16
+ ## 0.11.0 — file errors and the transport decision come from data
17
+
18
+ - `FileHandlerError` and `FileAccessDeniedError`, `FileNotFoundError`, `FileDeletedError`,
19
+ `ShareLinkInvalidError`, `ExternalFetchError`, `FileUploadError` are `@ai-matrx/data/files`'
20
+ classes, re-exported under the same names; `TUS_TRANSPORT_THRESHOLD_BYTES` /
21
+ `resolveUploadTransport` / `UploadTransport` likewise (architecture step 14). An error thrown by
22
+ data's file client is now `instanceof` media's classes.
23
+ - The base class's `name` is `"FileError"` (was `"FileHandlerError"`); subclasses keep their names.
24
+
25
+ Consumer action: none, unless code compares `error.name === "FileHandlerError"` (none found).
26
+
3
27
  ## 0.10.7
4
28
 
5
29
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -22,6 +22,8 @@
22
22
  * `createShareLink` in `@/utils/permissions/shareLinks` — the landing page is
23
23
  * `/s/{token}` and the clean byte endpoint is Python's `/share/{token}`.
24
24
  */
25
+ import { type FileUrls } from "@ai-matrx/data/files";
26
+ export type { FileUrls };
25
27
  export declare function pythonBaseUrl(): string;
26
28
  export interface ShareUrls {
27
29
  /**
@@ -72,19 +74,6 @@ export declare function shareChildFileUrls(token: string, fileId: string, opts?:
72
74
  * single canonical embeddable URL.
73
75
  */
74
76
  export declare function pythonShareUrl(token: string): string;
75
- export interface FileUrls {
76
- /**
77
- * Authenticated direct-download endpoint. CORS-safe for `fetch()`.
78
- * Returns bytes with `Content-Disposition: attachment` so a `<a>`
79
- * click triggers a download.
80
- */
81
- download: string;
82
- /**
83
- * Authenticated inline endpoint — same bytes, `Content-Disposition:
84
- * inline` so the URL works as `<img src>`, `<video src>`, etc.
85
- */
86
- inline: string;
87
- }
88
77
  /**
89
78
  * Build every URL variant for a cld_files file id in one call. Cheap —
90
79
  * just string concatenation. These are DURABLE URLs: they never expire and
@@ -1,4 +1,4 @@
1
- import { fileUrls as packageFileUrls, shareUrl as packageShareUrl } from "@ai-matrx/data/files";
1
+ import { buildFileUrls, shareUrl as packageShareUrl } from "@ai-matrx/data/files";
2
2
  import { resolveFilesBaseUrl } from "../../host/python-client.js";
3
3
  import { shareLinkUrl } from "../../host/share-links.js";
4
4
  function pythonBaseUrl() {
@@ -22,7 +22,7 @@ function pythonShareUrl(token) {
22
22
  return shareUrls(token).public;
23
23
  }
24
24
  function fileUrls(fileId) {
25
- return packageFileUrls(pythonBaseUrl(), fileId);
25
+ return buildFileUrls(pythonBaseUrl(), fileId);
26
26
  }
27
27
  const AUTHENTICATED_FILE_BYTES_RE = /\/files\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/download(?:[?#]|$)/i;
28
28
  function isAuthenticatedFileBytesUrl(url) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../src/files/engine/handler/utils/python-base.ts"],"sourcesContent":["/**\n * features/files/handler/utils/python-base.ts\n *\n * Resolve Python backend URLs for direct browser → Python file traffic.\n * The handler emits URLs that point at Python, never at Next.js — there\n * is no server-side file handler in this app and no `/app/api/*` proxy\n * involvement.\n *\n * Design note — \"all variants in one object\":\n * Python exposes the same bytes via slightly-different URLs (inline vs\n * attachment) and the FE has a share landing page. Rather than add a new\n * function for every future tweak, the canonical builders here\n * (`shareUrls`, `fileUrls`) return ALL the variants up front and let the\n * caller pick. Single spelling for \"everything you can do with a\n * token / file id.\"\n *\n * Single-string helpers (`pythonShareUrl`, `pythonFileDownloadUrl`,\n * `pythonFileInlineUrl`) are kept as thin wrappers so existing callers\n * don't churn. Prefer the object form in new code.\n *\n * Tokens are CANONICAL share-link tokens (`platform.share_links`), minted via\n * `createShareLink` in `@/utils/permissions/shareLinks` — the landing page is\n * `/s/{token}` and the clean byte endpoint is Python's `/share/{token}`.\n */\n\nimport { fileUrls as packageFileUrls, shareUrl as packageShareUrl } from \"@ai-matrx/data/files\";\nimport { resolveFilesBaseUrl } from \"../../host/python-client\";\nimport { shareLinkUrl } from \"../../host/share-links\";\n\nexport function pythonBaseUrl(): string {\n return resolveFilesBaseUrl();\n}\n\n// ---------------------------------------------------------------------------\n// Share-link URLs (token-based, public, no auth)\n// ---------------------------------------------------------------------------\n\nexport interface ShareUrls {\n /**\n * Canonical clean public URL with `inline` Content-Disposition (the default).\n * Works as `<img src>`, `<video src>`, `<audio src>`, PDF preview, or a\n * normal browser link a recipient can paste anywhere.\n *\n * Image / video / audio / PDF render inline. Dangerous types\n * (HTML, SVG, JS) are forced to `attachment` server-side.\n */\n public: string;\n /**\n * Explicit download endpoint with `?inline=false` — forces\n * `Content-Disposition: attachment` so the browser triggers a\n * download dialog instead of rendering inline. Use for \"Save As…\"\n * style affordances on images / PDFs.\n */\n attachment: string;\n /**\n * The canonical share landing page on the FE — `/s/{token}` (metadata via\n * the `resolve_share_token` RPC + preview + download button). Useful as a\n * clickable link in chat / email / docs; NOT a media src.\n */\n page: string;\n}\n\n/**\n * Build every URL variant for a share token in one call. Cheap —\n * just string concatenation, no I/O.\n */\nexport function shareUrls(\n token: string,\n opts?: { appOrigin?: string },\n): ShareUrls {\n const t = encodeURIComponent(token);\n // The URL grammar is @ai-matrx/data/files'; the host only binds the base.\n const base = packageShareUrl(pythonBaseUrl(), token);\n return {\n public: base,\n attachment: `${base}/download?inline=false`,\n page: opts?.appOrigin\n ? `${opts.appOrigin.replace(/\\/$/, \"\")}/s/${t}`\n : shareLinkUrl(t),\n };\n}\n\n/**\n * Byte URLs for ONE child file of a shared record — a shared chat's\n * attachment — reached with the parent's share token (access ladder T-19b).\n * The files service answers `/share/{token}/files/{fileId}` only when the file\n * is a child of the record that token shares; nothing else is reachable.\n */\nexport function shareChildFileUrls(\n token: string,\n fileId: string,\n opts?: { baseUrl?: string },\n): { inline: string; attachment: string } {\n const backend = (opts?.baseUrl ?? pythonBaseUrl()).replace(/\\/$/, \"\");\n const base = `${backend}/share/${encodeURIComponent(token)}/files/${encodeURIComponent(fileId)}`;\n return { inline: base, attachment: `${base}?inline=false` };\n}\n\n/**\n * Python's clean public byte-streaming share endpoint. Convenience wrapper\n * around `shareUrls(token).public` for callers that just want the\n * single canonical embeddable URL.\n */\nexport function pythonShareUrl(token: string): string {\n return shareUrls(token).public;\n}\n\n// ---------------------------------------------------------------------------\n// Authenticated file URLs (file-id, owner / shared-permission required)\n// ---------------------------------------------------------------------------\n\nexport interface FileUrls {\n /**\n * Authenticated direct-download endpoint. CORS-safe for `fetch()`.\n * Returns bytes with `Content-Disposition: attachment` so a `<a>`\n * click triggers a download.\n */\n download: string;\n /**\n * Authenticated inline endpoint — same bytes, `Content-Disposition:\n * inline` so the URL works as `<img src>`, `<video src>`, etc.\n */\n inline: string;\n}\n\n/**\n * Build every URL variant for a cld_files file id in one call. Cheap —\n * just string concatenation. These are DURABLE URLs: they never expire and\n * carry only the file id. Auth is either an Authorization header on\n * `fetch()` (the python-client attaches it) or the `mx_files_session`\n * cookie for plain `<img>`/`<video>` bindings (see `../session.ts`).\n * `?inline=1` matches the exact spelling the backend emits on\n * `FileRecord.url`, keeping cache keys identical either way.\n */\nexport function fileUrls(fileId: string): FileUrls {\n // The URL grammar is @ai-matrx/data/files'; the host only binds the base.\n return packageFileUrls(pythonBaseUrl(), fileId);\n}\n\nconst AUTHENTICATED_FILE_BYTES_RE =\n /\\/files\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\/download(?:[?#]|$)/i;\n\n/**\n * True for a durable `/files/{id}/download` URL (either host, either\n * spelling) — bytes that answer 401 unless the request carries the person's\n * Authorization header or the `mx_files_session` cookie. A consumer that\n * loads bytes with its own fetch (PDF.js) must never send one bare.\n */\nexport function isAuthenticatedFileBytesUrl(url: string | null | undefined): boolean {\n return !!url && AUTHENTICATED_FILE_BYTES_RE.test(url);\n}\n\n/** Single-URL convenience wrapper around `fileUrls(fileId).download`. */\nexport function pythonFileDownloadUrl(fileId: string): string {\n return fileUrls(fileId).download;\n}\n\n/** Single-URL convenience wrapper around `fileUrls(fileId).inline`. */\nexport function pythonFileInlineUrl(fileId: string): string {\n return fileUrls(fileId).inline;\n}\n\n// ---------------------------------------------------------------------------\n// URL classification + viewer-page derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Match anything that ends in `/share/{token}` or `/share/{token}/download`\n * (with optional query string / hash) where `{token}` is a hex-ish string of\n * at least 8 chars (covers both UUIDs and shorter custom tokens). Used to\n * recognize URLs that we ourselves emitted via `shareUrls()` so we can\n * round-trip them back into a viewer URL.\n */\nconst SHARE_TOKEN_RE = /\\/share\\/([0-9a-f-]{8,})(?:\\/download)?(?:[/?#]|$)/i;\n\n/**\n * Extract the share token from any URL we recognize. Returns `null` for\n * URLs that aren't ours (opaque external and third-party URLs).\n */\nexport function tokenFromShareUrl(url: string): string | null {\n if (!url) return null;\n const match = url.match(SHARE_TOKEN_RE);\n return match ? match[1] : null;\n}\n\n/**\n * \"Best-effort viewer URL\" for any image / file URL we might have stored\n * in the database. The intent is what the user clicks when they want to\n * SEE the file (not download it):\n *\n * - Our bare `/share/{token}` (or legacy `/share/{token}/download`) URLs → the\n * canonical FE landing page at `${origin}/s/{token}` (metadata + preview\n * + download button). This is what an admin wants when they click a\n * screenshot in the feedback dialog.\n * - Anything else (external URLs) →\n * returned unchanged. Browsers render image bytes inline regardless,\n * and we have no viewer page for them anyway.\n */\nexport function imageViewUrl(\n url: string,\n opts?: { appOrigin?: string },\n): string {\n const token = tokenFromShareUrl(url);\n if (!token) return url;\n return shareUrls(token, opts).page;\n}\n"],"mappings":"AAyBA,SAAS,YAAY,iBAAiB,YAAY,uBAAuB;AACzE,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAEtB,SAAS,gBAAwB;AACtC,SAAO,oBAAoB;AAC7B;AAmCO,SAAS,UACd,OACA,MACW;AACX,QAAM,IAAI,mBAAmB,KAAK;AAElC,QAAM,OAAO,gBAAgB,cAAc,GAAG,KAAK;AACnD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,YAAY,GAAG,IAAI;AAAA,IACnB,MAAM,MAAM,YACR,GAAG,KAAK,UAAU,QAAQ,OAAO,EAAE,CAAC,MAAM,CAAC,KAC3C,aAAa,CAAC;AAAA,EACpB;AACF;AAQO,SAAS,mBACd,OACA,QACA,MACwC;AACxC,QAAM,WAAW,MAAM,WAAW,cAAc,GAAG,QAAQ,OAAO,EAAE;AACpE,QAAM,OAAO,GAAG,OAAO,UAAU,mBAAmB,KAAK,CAAC,UAAU,mBAAmB,MAAM,CAAC;AAC9F,SAAO,EAAE,QAAQ,MAAM,YAAY,GAAG,IAAI,gBAAgB;AAC5D;AAOO,SAAS,eAAe,OAAuB;AACpD,SAAO,UAAU,KAAK,EAAE;AAC1B;AA6BO,SAAS,SAAS,QAA0B;AAEjD,SAAO,gBAAgB,cAAc,GAAG,MAAM;AAChD;AAEA,MAAM,8BACJ;AAQK,SAAS,4BAA4B,KAAyC;AACnF,SAAO,CAAC,CAAC,OAAO,4BAA4B,KAAK,GAAG;AACtD;AAGO,SAAS,sBAAsB,QAAwB;AAC5D,SAAO,SAAS,MAAM,EAAE;AAC1B;AAGO,SAAS,oBAAoB,QAAwB;AAC1D,SAAO,SAAS,MAAM,EAAE;AAC1B;AAaA,MAAM,iBAAiB;AAMhB,SAAS,kBAAkB,KAA4B;AAC5D,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,IAAI,MAAM,cAAc;AACtC,SAAO,QAAQ,MAAM,CAAC,IAAI;AAC5B;AAeO,SAAS,aACd,KACA,MACQ;AACR,QAAM,QAAQ,kBAAkB,GAAG;AACnC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,UAAU,OAAO,IAAI,EAAE;AAChC;","names":[]}
1
+ {"version":3,"sources":["../../../../../src/files/engine/handler/utils/python-base.ts"],"sourcesContent":["/**\n * features/files/handler/utils/python-base.ts\n *\n * Resolve Python backend URLs for direct browser → Python file traffic.\n * The handler emits URLs that point at Python, never at Next.js — there\n * is no server-side file handler in this app and no `/app/api/*` proxy\n * involvement.\n *\n * Design note — \"all variants in one object\":\n * Python exposes the same bytes via slightly-different URLs (inline vs\n * attachment) and the FE has a share landing page. Rather than add a new\n * function for every future tweak, the canonical builders here\n * (`shareUrls`, `fileUrls`) return ALL the variants up front and let the\n * caller pick. Single spelling for \"everything you can do with a\n * token / file id.\"\n *\n * Single-string helpers (`pythonShareUrl`, `pythonFileDownloadUrl`,\n * `pythonFileInlineUrl`) are kept as thin wrappers so existing callers\n * don't churn. Prefer the object form in new code.\n *\n * Tokens are CANONICAL share-link tokens (`platform.share_links`), minted via\n * `createShareLink` in `@/utils/permissions/shareLinks` — the landing page is\n * `/s/{token}` and the clean byte endpoint is Python's `/share/{token}`.\n */\n\nimport { buildFileUrls, shareUrl as packageShareUrl, type FileUrls } from \"@ai-matrx/data/files\";\n\nexport type { FileUrls };\nimport { resolveFilesBaseUrl } from \"../../host/python-client\";\nimport { shareLinkUrl } from \"../../host/share-links\";\n\nexport function pythonBaseUrl(): string {\n return resolveFilesBaseUrl();\n}\n\n// ---------------------------------------------------------------------------\n// Share-link URLs (token-based, public, no auth)\n// ---------------------------------------------------------------------------\n\nexport interface ShareUrls {\n /**\n * Canonical clean public URL with `inline` Content-Disposition (the default).\n * Works as `<img src>`, `<video src>`, `<audio src>`, PDF preview, or a\n * normal browser link a recipient can paste anywhere.\n *\n * Image / video / audio / PDF render inline. Dangerous types\n * (HTML, SVG, JS) are forced to `attachment` server-side.\n */\n public: string;\n /**\n * Explicit download endpoint with `?inline=false` — forces\n * `Content-Disposition: attachment` so the browser triggers a\n * download dialog instead of rendering inline. Use for \"Save As…\"\n * style affordances on images / PDFs.\n */\n attachment: string;\n /**\n * The canonical share landing page on the FE — `/s/{token}` (metadata via\n * the `resolve_share_token` RPC + preview + download button). Useful as a\n * clickable link in chat / email / docs; NOT a media src.\n */\n page: string;\n}\n\n/**\n * Build every URL variant for a share token in one call. Cheap —\n * just string concatenation, no I/O.\n */\nexport function shareUrls(\n token: string,\n opts?: { appOrigin?: string },\n): ShareUrls {\n const t = encodeURIComponent(token);\n // The URL grammar is @ai-matrx/data/files'; the host only binds the base.\n const base = packageShareUrl(pythonBaseUrl(), token);\n return {\n public: base,\n attachment: `${base}/download?inline=false`,\n page: opts?.appOrigin\n ? `${opts.appOrigin.replace(/\\/$/, \"\")}/s/${t}`\n : shareLinkUrl(t),\n };\n}\n\n/**\n * Byte URLs for ONE child file of a shared record — a shared chat's\n * attachment — reached with the parent's share token (access ladder T-19b).\n * The files service answers `/share/{token}/files/{fileId}` only when the file\n * is a child of the record that token shares; nothing else is reachable.\n */\nexport function shareChildFileUrls(\n token: string,\n fileId: string,\n opts?: { baseUrl?: string },\n): { inline: string; attachment: string } {\n const backend = (opts?.baseUrl ?? pythonBaseUrl()).replace(/\\/$/, \"\");\n const base = `${backend}/share/${encodeURIComponent(token)}/files/${encodeURIComponent(fileId)}`;\n return { inline: base, attachment: `${base}?inline=false` };\n}\n\n/**\n * Python's clean public byte-streaming share endpoint. Convenience wrapper\n * around `shareUrls(token).public` for callers that just want the\n * single canonical embeddable URL.\n */\nexport function pythonShareUrl(token: string): string {\n return shareUrls(token).public;\n}\n\n// ---------------------------------------------------------------------------\n// Authenticated file URLs (file-id, owner / shared-permission required)\n// ---------------------------------------------------------------------------\n\n\n/**\n * Build every URL variant for a cld_files file id in one call. Cheap —\n * just string concatenation. These are DURABLE URLs: they never expire and\n * carry only the file id. Auth is either an Authorization header on\n * `fetch()` (the python-client attaches it) or the `mx_files_session`\n * cookie for plain `<img>`/`<video>` bindings (see `../session.ts`).\n * `?inline=1` matches the exact spelling the backend emits on\n * `FileRecord.url`, keeping cache keys identical either way.\n */\nexport function fileUrls(fileId: string): FileUrls {\n // The URL grammar is @ai-matrx/data/files'; the host only binds the base.\n return buildFileUrls(pythonBaseUrl(), fileId);\n}\n\nconst AUTHENTICATED_FILE_BYTES_RE =\n /\\/files\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\/download(?:[?#]|$)/i;\n\n/**\n * True for a durable `/files/{id}/download` URL (either host, either\n * spelling) — bytes that answer 401 unless the request carries the person's\n * Authorization header or the `mx_files_session` cookie. A consumer that\n * loads bytes with its own fetch (PDF.js) must never send one bare.\n */\nexport function isAuthenticatedFileBytesUrl(url: string | null | undefined): boolean {\n return !!url && AUTHENTICATED_FILE_BYTES_RE.test(url);\n}\n\n/** Single-URL convenience wrapper around `fileUrls(fileId).download`. */\nexport function pythonFileDownloadUrl(fileId: string): string {\n return fileUrls(fileId).download;\n}\n\n/** Single-URL convenience wrapper around `fileUrls(fileId).inline`. */\nexport function pythonFileInlineUrl(fileId: string): string {\n return fileUrls(fileId).inline;\n}\n\n// ---------------------------------------------------------------------------\n// URL classification + viewer-page derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Match anything that ends in `/share/{token}` or `/share/{token}/download`\n * (with optional query string / hash) where `{token}` is a hex-ish string of\n * at least 8 chars (covers both UUIDs and shorter custom tokens). Used to\n * recognize URLs that we ourselves emitted via `shareUrls()` so we can\n * round-trip them back into a viewer URL.\n */\nconst SHARE_TOKEN_RE = /\\/share\\/([0-9a-f-]{8,})(?:\\/download)?(?:[/?#]|$)/i;\n\n/**\n * Extract the share token from any URL we recognize. Returns `null` for\n * URLs that aren't ours (opaque external and third-party URLs).\n */\nexport function tokenFromShareUrl(url: string): string | null {\n if (!url) return null;\n const match = url.match(SHARE_TOKEN_RE);\n return match ? match[1] : null;\n}\n\n/**\n * \"Best-effort viewer URL\" for any image / file URL we might have stored\n * in the database. The intent is what the user clicks when they want to\n * SEE the file (not download it):\n *\n * - Our bare `/share/{token}` (or legacy `/share/{token}/download`) URLs → the\n * canonical FE landing page at `${origin}/s/{token}` (metadata + preview\n * + download button). This is what an admin wants when they click a\n * screenshot in the feedback dialog.\n * - Anything else (external URLs) →\n * returned unchanged. Browsers render image bytes inline regardless,\n * and we have no viewer page for them anyway.\n */\nexport function imageViewUrl(\n url: string,\n opts?: { appOrigin?: string },\n): string {\n const token = tokenFromShareUrl(url);\n if (!token) return url;\n return shareUrls(token, opts).page;\n}\n"],"mappings":"AAyBA,SAAS,eAAe,YAAY,uBAAsC;AAG1E,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAEtB,SAAS,gBAAwB;AACtC,SAAO,oBAAoB;AAC7B;AAmCO,SAAS,UACd,OACA,MACW;AACX,QAAM,IAAI,mBAAmB,KAAK;AAElC,QAAM,OAAO,gBAAgB,cAAc,GAAG,KAAK;AACnD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,YAAY,GAAG,IAAI;AAAA,IACnB,MAAM,MAAM,YACR,GAAG,KAAK,UAAU,QAAQ,OAAO,EAAE,CAAC,MAAM,CAAC,KAC3C,aAAa,CAAC;AAAA,EACpB;AACF;AAQO,SAAS,mBACd,OACA,QACA,MACwC;AACxC,QAAM,WAAW,MAAM,WAAW,cAAc,GAAG,QAAQ,OAAO,EAAE;AACpE,QAAM,OAAO,GAAG,OAAO,UAAU,mBAAmB,KAAK,CAAC,UAAU,mBAAmB,MAAM,CAAC;AAC9F,SAAO,EAAE,QAAQ,MAAM,YAAY,GAAG,IAAI,gBAAgB;AAC5D;AAOO,SAAS,eAAe,OAAuB;AACpD,SAAO,UAAU,KAAK,EAAE;AAC1B;AAgBO,SAAS,SAAS,QAA0B;AAEjD,SAAO,cAAc,cAAc,GAAG,MAAM;AAC9C;AAEA,MAAM,8BACJ;AAQK,SAAS,4BAA4B,KAAyC;AACnF,SAAO,CAAC,CAAC,OAAO,4BAA4B,KAAK,GAAG;AACtD;AAGO,SAAS,sBAAsB,QAAwB;AAC5D,SAAO,SAAS,MAAM,EAAE;AAC1B;AAGO,SAAS,oBAAoB,QAAwB;AAC1D,SAAO,SAAS,MAAM,EAAE;AAC1B;AAaA,MAAM,iBAAiB;AAMhB,SAAS,kBAAkB,KAA4B;AAC5D,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,IAAI,MAAM,cAAc;AACtC,SAAO,QAAQ,MAAM,CAAC,IAAI;AAC5B;AAeO,SAAS,aACd,KACA,MACQ;AACR,QAAM,QAAQ,kBAAkB,GAAG;AACnC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,UAAU,OAAO,IAAI,EAAE;AAChC;","names":[]}
@@ -39,16 +39,8 @@
39
39
  import { type UploadProgressEvent } from "../host/python-client.js";
40
40
  import type { AppDispatch } from "../host/store.js";
41
41
  import type { PermissionLevel, Visibility } from "../types.js";
42
- /**
43
- * Files at or above this size route through the resumable TUS transport
44
- * (`tusUpload.ts`); smaller files use the buffered multipart POST. Starting
45
- * value 80 MB — tune only on evidence. Callers can force either transport
46
- * via `CloudUploadOptions.transport`.
47
- */
48
- export declare const TUS_TRANSPORT_THRESHOLD_BYTES: number;
49
- export type UploadTransport = "buffered" | "tus";
50
- /** The ONE place the buffered-vs-TUS decision is made. */
51
- export declare function resolveUploadTransport(fileSizeBytes: number, override?: UploadTransport): UploadTransport;
42
+ export { resolveUploadTransport, TUS_TRANSPORT_THRESHOLD_BYTES, type UploadTransport, } from "@ai-matrx/data/files";
43
+ import { type UploadTransport } from "@ai-matrx/data/files";
52
44
  export interface CloudUploadOptions {
53
45
  /**
54
46
  * Full logical file path INCLUDING the filename. Use this when you
@@ -27,11 +27,11 @@ import {
27
27
  buildUploadMetadataEnvelope,
28
28
  tusUploadRaw
29
29
  } from "./tusUpload.js";
30
- const TUS_TRANSPORT_THRESHOLD_BYTES = 80 * 1024 * 1024;
31
- function resolveUploadTransport(fileSizeBytes, override) {
32
- if (override) return override;
33
- return fileSizeBytes >= TUS_TRANSPORT_THRESHOLD_BYTES ? "tus" : "buffered";
34
- }
30
+ import {
31
+ resolveUploadTransport,
32
+ TUS_TRANSPORT_THRESHOLD_BYTES
33
+ } from "@ai-matrx/data/files";
34
+ import { resolveUploadTransport as resolveUploadTransport2 } from "@ai-matrx/data/files";
35
35
  function isCloudUploadFailure(result) {
36
36
  return result.ok === false;
37
37
  }
@@ -112,7 +112,7 @@ async function cloudUploadRaw(file, rawOptions = {}) {
112
112
  }
113
113
  const filePath = resolveFilePath(file, options);
114
114
  const requestId = newRequestId();
115
- if (resolveUploadTransport(file.size, options.transport) === "tus") {
115
+ if (resolveUploadTransport2(file.size, options.transport) === "tus") {
116
116
  const tusResult = await tusUploadRaw(file, filePath, options, requestId);
117
117
  if (!tusResult.ok || !options.createShareLink) return tusResult;
118
118
  try {
@@ -216,7 +216,7 @@ async function cloudUpload(file, rawOptions, dispatch) {
216
216
  });
217
217
  try {
218
218
  let uploadData;
219
- if (resolveUploadTransport(file.size, options.transport) === "tus") {
219
+ if (resolveUploadTransport2(file.size, options.transport) === "tus") {
220
220
  const tusResult = await tusUploadRaw(
221
221
  file,
222
222
  filePath,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/files/engine/upload/cloudUpload.ts"],"sourcesContent":["/**\n * features/files/upload/cloudUpload.ts\n *\n * THE single source of truth for file uploads in this app.\n *\n * ════════════════════════════════════════════════════════════════════════\n *\n * Why this exists\n * ───────────────\n * The single underlying upload primitive. Public callers go through the\n * universal file handler (`fileHandler.upload(...)` /\n * `useFileUpload`), which calls this. Server-side routes use\n * `Api.Server.uploadAndShare` which wraps the same Python `/files/upload`\n * endpoint with a server context.\n *\n * Rule: **every upload in the app goes through `cloudUpload` (this file)\n * via the universal handler, OR through `Api.Server.uploadAndShare`.**\n * Never call `supabase.from(\"cld_*\").upsert(...)` or `ensureFolderPath`\n * from a code path whose goal is \"upload a file.\" The Python backend\n * auto-creates any missing folders when you POST a full `file_path` —\n * the browser never needs to touch `cld_folders` directly. That\n * sidesteps the known RLS recursion bug AND keeps logic centralized.\n *\n * What this module owns:\n * • Resolving a logical `file_path` from caller-supplied options.\n * • Calling the Python `/files/upload` endpoint with progress.\n * • Optionally creating a permanent share link.\n * • Dispatching Redux upserts so the slice stays in sync.\n * • Returning a uniform `{ ok: true, ... } | { ok: false, error }` shape\n * — never null, never `[object Object]`, never silent.\n *\n * What this module does NOT do:\n * • Touch `supabase.from(\"cld_*\")` directly. Reads happen via the\n * SECURITY DEFINER tree RPC (`get_user_file_tree`) and\n * supabase realtime. Writes happen via the Python backend.\n * • Block on folder lookups. The backend handles folder creation\n * atomically as part of upload.\n */\n\nimport {\n uploadFile as Files_uploadFile,\n uploadFileWithProgress as Files_uploadFileWithProgress,\n} from \"../api/files\";\nimport {\n createShareLink as createCanonicalShareLink,\n shareLinkUrl,\n} from \"../host/share-links\";\nimport { pythonShareUrl } from \"../handler/utils/python-base\";\nimport {\n newRequestId,\n type ResponseMeta,\n type UploadProgressEvent,\n} from \"../host/python-client\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\nimport {\n registerRequest,\n releaseRequest,\n} from \"../redux/request-ledger\";\nimport {\n attachChildToFolder,\n trackUploadStart,\n updateUploadProgress,\n updateUploadStatus,\n upsertFile,\n} from \"../redux/slice\";\nimport { apiFileRecordToCloudFile } from \"../redux/converters\";\n// eslint-disable-next-line no-restricted-imports -- transport sibling INSIDE the ring-fenced upload internals; cloudUpload is the ONE transport-policy chokepoint\nimport {\n buildUploadMetadataEnvelope,\n tusUploadRaw,\n} from \"./tusUpload\";\nimport type { AppDispatch } from \"../host/store\";\nimport type { PermissionLevel, Visibility } from \"../types\";\n\n// ─── Transport policy ────────────────────────────────────────────────────\n\n/**\n * Files at or above this size route through the resumable TUS transport\n * (`tusUpload.ts`); smaller files use the buffered multipart POST. Starting\n * value 80 MB — tune only on evidence. Callers can force either transport\n * via `CloudUploadOptions.transport`.\n */\nexport const TUS_TRANSPORT_THRESHOLD_BYTES = 80 * 1024 * 1024;\n\nexport type UploadTransport = \"buffered\" | \"tus\";\n\n/** The ONE place the buffered-vs-TUS decision is made. */\nexport function resolveUploadTransport(\n fileSizeBytes: number,\n override?: UploadTransport,\n): UploadTransport {\n if (override) return override;\n return fileSizeBytes >= TUS_TRANSPORT_THRESHOLD_BYTES ? \"tus\" : \"buffered\";\n}\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\nexport interface CloudUploadOptions {\n /**\n * Full logical file path INCLUDING the filename. Use this when you\n * want to control the exact name (e.g. server-generated \"{uuid}.jpg\").\n */\n filePath?: string;\n /**\n * Folder path (no filename). The browser appends `file.name`\n * automatically. The Python backend auto-creates any missing\n * folders. This is the **preferred** option for almost every caller.\n *\n * Example: `folderPath: \"Images/Chat\"` → uploaded path becomes\n * `\"Images/Chat/<file.name>\"`.\n */\n folderPath?: string;\n /**\n * Existing parentFolderId — only useful when you've already loaded the\n * folder via the tree RPC or realtime. Most callers should pass\n * `folderPath` instead so the backend handles folder creation.\n */\n parentFolderId?: string | null;\n visibility?: Visibility;\n shareWith?: string[];\n shareLevel?: PermissionLevel;\n changeSummary?: string;\n metadata?: Record<string, unknown>;\n /** Progress callback (XHR upload progress). */\n onProgress?: (event: UploadProgressEvent) => void;\n signal?: AbortSignal;\n /**\n * Transport override. Default: `resolveUploadTransport` picks TUS for\n * files ≥ `TUS_TRANSPORT_THRESHOLD_BYTES`, buffered otherwise.\n */\n transport?: UploadTransport;\n /**\n * If true, also creates a permanent share link after upload. Returns\n * the `shareUrl` (`/s/:token`) and the raw `shareToken`.\n */\n createShareLink?: boolean;\n /** Share-link permission — canonical `viewer` / `editor` levels. */\n shareLinkPermissionLevel?: \"viewer\" | \"editor\";\n shareLinkExpiresAt?: string | null;\n shareLinkMaxUses?: number | null;\n}\n\nexport interface CloudUploadSuccess {\n ok: true;\n fileId: string;\n filePath: string;\n fileSize: number | null;\n versionNumber: number;\n /** Backend-provided URL (storage URL — typically requires auth or signed). */\n url: string | null;\n shareToken?: string;\n /**\n * The PUBLIC LANDING PAGE for the share link, e.g.\n * `https://app.example.com/s/<token>`. Renders an HTML page with\n * file metadata and a download button. **Do NOT** use this for\n * `<img src>`, `<video src>`, or hot-linking — the page is HTML, not\n * the file bytes.\n */\n shareUrl?: string;\n /**\n * The PUBLIC DIRECT URL for the file bytes — points at Python's\n * `{BACKEND}/share/{token}` endpoint, which serves inline-safe bytes (and may\n * 302-redirect public files to their permanent CDN URL). Embed it in\n * `<img src>`, Slack/Notion unfurls, or OG images. No Next.js hop.\n *\n * Populated whenever `shareUrl` is — they always go in pairs because\n * both are derived from the same share token.\n */\n directUrl?: string;\n}\n\nexport interface CloudUploadFailure {\n ok: false;\n error: string;\n /** Backend error code if available (e.g. \"auth_required\", \"file_too_large\"). */\n errorCode?: string;\n /** Filename for caller reference. */\n fileName: string;\n}\n\nexport type CloudUploadResult = CloudUploadSuccess | CloudUploadFailure;\n\n/**\n * Type guard — when `result.ok` discriminator narrowing isn't enough for\n * TS (e.g. inside loops or where the union is inferred indirectly),\n * call this and the compiler will narrow the branches correctly.\n */\nexport function isCloudUploadFailure(\n result: CloudUploadResult,\n): result is CloudUploadFailure {\n return result.ok === false;\n}\n\nexport function isCloudUploadSuccess(\n result: CloudUploadResult,\n): result is CloudUploadSuccess {\n return result.ok === true;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────\n\nfunction resolveFilePath(file: File, options: CloudUploadOptions): string {\n if (options.filePath) return options.filePath.replace(/^\\/+/, \"\");\n if (options.folderPath) {\n const folder = options.folderPath.replace(/^\\/+|\\/+$/g, \"\");\n return folder ? `${folder}/${file.name}` : file.name;\n }\n return file.name;\n}\n\n/**\n * Build the public DIRECT-FILE URL for a share token — points at Python's\n * `/share/{token}` byte endpoint. No Next.js hop. Embed in\n * `<img src>`, `<video src>`, downloads, Slack unfurls, etc.\n */\nfunction buildDirectShareUrl(token: string): string {\n return pythonShareUrl(token);\n}\n\n/**\n * Mint a canonical share link (`platform.share_links`) for a freshly\n * uploaded file. Throws on failure so both upload paths surface the\n * distinct `share_link_failed` error.\n */\nasync function mintUploadShareLink(\n fileId: string,\n options: CloudUploadOptions,\n): Promise<{ shareToken: string; shareUrl: string; directUrl: string }> {\n const created = await createCanonicalShareLink({\n resourceType: \"file\",\n resourceId: fileId,\n permissionLevel: options.shareLinkPermissionLevel ?? \"viewer\",\n expiresAt: options.shareLinkExpiresAt ?? null,\n maxUses: options.shareLinkMaxUses ?? null,\n });\n if (!created.success || !created.token) {\n throw new Error(created.error ?? \"Share link creation failed\");\n }\n return {\n shareToken: created.token,\n shareUrl: shareLinkUrl(created.token),\n directUrl: buildDirectShareUrl(created.token),\n };\n}\n\n// ─── The upload organization — resolved ONCE, for every transport ────────\n\n/**\n * Bind the owning organization to an upload before a byte moves.\n *\n * 🚨 THE BUG THIS EXISTS FOR (2026-08-30). `InlineUploadArea` — the composer's\n * attach button — sent no organization at all. The server has honoured\n * `metadata.scope.organization_id` all along (membership-checked against\n * `iam.has_org_access_for`), but with nothing supplied it fell back to the\n * uploader's PERSONAL workspace. So a screenshot attached while no organization\n * was selected silently became a personal-workspace file; a minute later the\n * person picked their team organization, and the two no longer agreed.\n *\n * Fixing that one component would have been a patch on one of several upload\n * doors. This is the choke point instead: both transports and every caller go\n * through here, so an upload that forgets its organization is not possible\n * rather than merely unusual.\n *\n * If nothing is selected, the organization gate ASKS (one dialog, then the\n * upload continues into the chosen workspace). Cancelling throws\n * `OrganizationSelectionCancelled`, which the upload paths report as an\n * ordinary cancelled result — no bytes sent, nothing written, nothing to\n * clean up.\n */\nasync function bindUploadOrganization(\n options: CloudUploadOptions,\n): Promise<CloudUploadOptions> {\n const { ensureOrganizationContext } = await import(\n \"../host/org\"\n );\n const declared = organizationIdFromUploadMetadata(options.metadata);\n const organizationId = await ensureOrganizationContext({\n organizationId: declared,\n });\n return {\n ...options,\n metadata: {\n ...(options.metadata ?? {}),\n scope: {\n ...(typeof options.metadata?.scope === \"object\" &&\n options.metadata?.scope !== null\n ? (options.metadata.scope as Record<string, unknown>)\n : {}),\n organization_id: organizationId,\n },\n },\n };\n}\n\n/**\n * Turn a failed organization binding into an ordinary upload result.\n *\n * `cloudUpload`/`cloudUploadRaw` never throw — every caller reads\n * `{ ok: false, error }` — so a cancelled prompt keeps that contract. It is\n * marked `upload_cancelled` rather than an error code so the UI can stay silent:\n * the person did not fail at anything, they declined, and declining must return\n * them exactly where they were.\n */\nfunction uploadOrganizationFailure(\n err: unknown,\n fileName: string,\n): CloudUploadResult {\n const cancelled =\n err instanceof Error && err.name === \"OrganizationSelectionCancelled\";\n return {\n ok: false,\n error: cancelled\n ? \"Upload cancelled — no workspace was selected.\"\n : extractErrorMessage(err),\n errorCode: cancelled ? \"upload_cancelled\" : \"organization_required\",\n fileName,\n };\n}\n\n/** Read an organization a caller already declared, in either accepted shape. */\nexport function organizationIdFromUploadMetadata(\n metadata: Record<string, unknown> | undefined,\n): string | undefined {\n if (!metadata) return undefined;\n const direct = metadata.organization_id;\n if (typeof direct === \"string\" && direct.length > 0) return direct;\n const scope = metadata.scope;\n if (scope && typeof scope === \"object\") {\n const nested = (scope as Record<string, unknown>).organization_id;\n if (typeof nested === \"string\" && nested.length > 0) return nested;\n }\n return undefined;\n}\n\n// ─── Upload primitive (no Redux side effects) ────────────────────────────\n\n/**\n * Pure upload — POSTs to /files/upload with a full `file_path`. Backend\n * auto-creates folders. Use this when you need raw access without Redux\n * dispatches (e.g. server-side route handlers, isolated tests).\n *\n * For browser code that wants the file to appear in the UI, use\n * `cloudUpload` (with dispatch) instead.\n */\nexport async function cloudUploadRaw(\n file: File,\n rawOptions: CloudUploadOptions = {},\n): Promise<CloudUploadResult> {\n let options: CloudUploadOptions;\n try {\n options = await bindUploadOrganization(rawOptions);\n } catch (err) {\n return uploadOrganizationFailure(err, file.name);\n }\n const filePath = resolveFilePath(file, options);\n const requestId = newRequestId();\n\n // Transport policy: large files (or an explicit override) go resumable.\n if (resolveUploadTransport(file.size, options.transport) === \"tus\") {\n const tusResult = await tusUploadRaw(file, filePath, options, requestId);\n if (!tusResult.ok || !options.createShareLink) return tusResult;\n try {\n const link = await mintUploadShareLink(tusResult.fileId, options);\n return { ...tusResult, ...link };\n } catch (linkErr) {\n return {\n ok: false,\n error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,\n errorCode: \"share_link_failed\",\n fileName: file.name,\n };\n }\n }\n\n try {\n const params = {\n file,\n filePath,\n visibility: options.visibility ?? \"personal\",\n shareWith: options.shareWith,\n shareLevel: options.shareLevel,\n changeSummary: options.changeSummary,\n // Shared envelope builder — the SAME object the TUS transport encodes\n // into Upload-Metadata's `metadata_json` (parity is unit-tested).\n metadata: buildUploadMetadataEnvelope(options.metadata),\n };\n\n const upload = options.onProgress\n ? await Files_uploadFileWithProgress(params, options.onProgress, {\n requestId,\n signal: options.signal,\n // Reuse requestId as the idempotency key — single intended\n // upload from the FE perspective, single key on the BE. Backend\n // stores it in `metadata._idempotency_key` so retries don't\n // double-create version rows.\n idempotencyKey: requestId,\n })\n : await Files_uploadFile(params, {\n requestId,\n signal: options.signal,\n idempotencyKey: requestId,\n });\n\n let shareToken: string | undefined;\n let shareUrl: string | undefined;\n let directUrl: string | undefined;\n if (options.createShareLink) {\n try {\n // Always emit the direct-file URL alongside the page URL.\n // Consumers default to `directUrl` for img/video/audio/iframe\n // src; `shareUrl` is for \"click here to view file metadata\".\n ({ shareToken, shareUrl, directUrl } = await mintUploadShareLink(\n upload.data.file_id,\n options,\n ));\n } catch (linkErr) {\n // Upload succeeded; share-link creation didn't. Surface a\n // distinct error so the caller can decide whether to keep the\n // file or roll it back.\n return {\n ok: false,\n error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,\n errorCode: \"share_link_failed\",\n fileName: file.name,\n };\n }\n }\n\n return {\n ok: true,\n fileId: upload.data.file_id,\n filePath: upload.data.file_path,\n // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.\n fileSize: upload.data.size_bytes,\n versionNumber: upload.data.version_number,\n url: upload.data.url ?? null,\n shareToken,\n shareUrl,\n directUrl,\n };\n } catch (err) {\n return {\n ok: false,\n error: extractErrorMessage(err),\n errorCode: (err as { code?: string } | null)?.code,\n fileName: file.name,\n };\n }\n}\n\n// ─── Upload with Redux side effects ──────────────────────────────────────\n\n/**\n * Upload a single file. THIS is the function 99% of callers want.\n *\n * Side effects:\n * • Dispatches `trackUploadStart`/`updateUploadProgress`/`updateUploadStatus`\n * so progress bars in the UI stay in sync.\n * • Dispatches `upsertFile` on success so the file appears in the slice\n * immediately (no need to wait for a tree refresh).\n * • Dispatches `attachChildToFolder` if `parentFolderId` is known.\n *\n * Returns the unified `CloudUploadResult`. Never throws — errors come\n * back as `{ ok: false, error }` so callers always have a clear path.\n */\nexport async function cloudUpload(\n file: File,\n rawOptions: CloudUploadOptions,\n dispatch: AppDispatch,\n): Promise<CloudUploadResult> {\n // Resolved BEFORE `trackUploadStart` so a cancelled organization prompt\n // leaves no orphan progress row in the slice — cancelling must look like the\n // upload never began, because it never did.\n let options: CloudUploadOptions;\n try {\n options = await bindUploadOrganization(rawOptions);\n } catch (err) {\n return uploadOrganizationFailure(err, file.name);\n }\n const filePath = resolveFilePath(file, options);\n const requestId = newRequestId();\n\n // 1. Mark the upload as pending in the slice.\n dispatch(\n trackUploadStart({\n requestId,\n fileName: file.name,\n fileSize: file.size,\n parentFolderId: options.parentFolderId ?? null,\n }),\n );\n registerRequest({\n requestId,\n kind: \"upload\",\n resourceId: null,\n resourceType: \"file\",\n });\n\n try {\n let uploadData: {\n file_id: string;\n file_path: string;\n size_bytes: number | null;\n checksum: string | null;\n version_number: number;\n url: string | null;\n };\n\n if (resolveUploadTransport(file.size, options.transport) === \"tus\") {\n // Resumable transport — same Redux progress/tracking as buffered.\n const tusResult = await tusUploadRaw(\n file,\n filePath,\n {\n ...options,\n onProgress: (ev) => {\n dispatch(\n updateUploadProgress({ requestId, bytesUploaded: ev.loaded }),\n );\n options.onProgress?.(ev);\n },\n },\n requestId,\n );\n if (!tusResult.ok) {\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"error\",\n error: tusResult.error,\n }),\n );\n return tusResult;\n }\n uploadData = {\n file_id: tusResult.fileId,\n file_path: tusResult.filePath,\n size_bytes: tusResult.fileSize,\n checksum: null,\n version_number: tusResult.versionNumber,\n url: tusResult.url,\n };\n } else {\n const params = {\n file,\n filePath,\n visibility: options.visibility ?? \"personal\",\n shareWith: options.shareWith,\n shareLevel: options.shareLevel,\n changeSummary: options.changeSummary,\n // Shared envelope builder — identical object to the TUS transport's\n // `metadata_json` (parity is unit-tested).\n metadata: buildUploadMetadataEnvelope(options.metadata),\n };\n\n const upload = await Files_uploadFileWithProgress(\n params,\n (ev) => {\n dispatch(\n updateUploadProgress({\n requestId,\n bytesUploaded: ev.loaded,\n }),\n );\n options.onProgress?.(ev);\n },\n { requestId, signal: options.signal, idempotencyKey: requestId },\n );\n uploadData = {\n file_id: upload.data.file_id,\n file_path: upload.data.file_path,\n size_bytes: upload.data.size_bytes,\n checksum: upload.data.checksum ?? null,\n version_number: upload.data.version_number,\n url: upload.data.url ?? null,\n };\n }\n\n // 2. Slice upsert — file is now visible in the tree without\n // waiting for the realtime echo or a refetch.\n dispatch(\n upsertFile(\n apiFileRecordToCloudFile({\n id: uploadData.file_id,\n owner_id: \"\",\n file_path: uploadData.file_path,\n file_name: uploadData.file_path.split(\"/\").pop() ?? file.name,\n mime_type: file.type || null,\n // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.\n size_bytes: uploadData.size_bytes,\n checksum: uploadData.checksum,\n visibility: options.visibility ?? \"personal\",\n current_version: uploadData.version_number,\n parent_folder_id: options.parentFolderId ?? null,\n metadata: options.metadata ?? {},\n created_at: null,\n updated_at: null,\n deleted_at: null,\n }),\n ),\n );\n if (options.parentFolderId) {\n dispatch(\n attachChildToFolder({\n parentFolderId: options.parentFolderId,\n kind: \"file\",\n id: uploadData.file_id,\n }),\n );\n }\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"success\",\n fileId: uploadData.file_id,\n }),\n );\n\n // 3. Optional share link.\n let shareToken: string | undefined;\n let shareUrl: string | undefined;\n let directUrl: string | undefined;\n if (options.createShareLink) {\n try {\n // Always emit the direct-file URL alongside the page URL.\n // Consumers default to `directUrl` for img/video/audio/iframe\n // src; `shareUrl` is for \"click here to view file metadata\".\n ({ shareToken, shareUrl, directUrl } = await mintUploadShareLink(\n uploadData.file_id,\n options,\n ));\n } catch (linkErr) {\n return {\n ok: false,\n error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,\n errorCode: \"share_link_failed\",\n fileName: file.name,\n };\n }\n }\n\n return {\n ok: true,\n fileId: uploadData.file_id,\n filePath: uploadData.file_path,\n // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.\n fileSize: uploadData.size_bytes,\n versionNumber: uploadData.version_number,\n url: uploadData.url,\n shareToken,\n shareUrl,\n directUrl,\n };\n } catch (err) {\n const message = extractErrorMessage(err);\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"error\",\n error: message,\n }),\n );\n return {\n ok: false,\n error: message,\n errorCode: (err as { code?: string } | null)?.code,\n fileName: file.name,\n };\n } finally {\n releaseRequest(requestId);\n }\n}\n\n// ─── Batch upload ────────────────────────────────────────────────────────\n\nexport interface CloudUploadManyOptions extends CloudUploadOptions {\n /** Parallel ceiling. Defaults to 3. */\n concurrency?: number;\n}\n\nexport interface CloudUploadManyResult {\n successes: CloudUploadSuccess[];\n failures: CloudUploadFailure[];\n}\n\n/**\n * Upload multiple files with bounded concurrency. Returns a structured\n * result so callers can show \"3 of 5 uploaded\" UI cleanly.\n */\nexport async function cloudUploadMany(\n files: File[],\n options: CloudUploadManyOptions,\n dispatch: AppDispatch,\n): Promise<CloudUploadManyResult> {\n const limit = Math.max(1, options.concurrency ?? 3);\n const successes: CloudUploadSuccess[] = [];\n const failures: CloudUploadFailure[] = [];\n const queue = [...files];\n\n async function worker(): Promise<void> {\n while (queue.length) {\n const file = queue.shift();\n if (!file) return;\n const result = await cloudUpload(file, options, dispatch);\n if (isCloudUploadSuccess(result)) {\n successes.push(result);\n } else {\n failures.push(result);\n }\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(limit, files.length) }, worker),\n );\n\n return { successes, failures };\n}\n\n/**\n * Imperative shortcut for non-React code that still wants Redux side\n * effects. Pulls the dispatch from the store singleton.\n */\nexport async function cloudUploadImperative(\n file: File,\n options: CloudUploadOptions,\n): Promise<CloudUploadResult> {\n const { getStore } = await import(\"../host/store\");\n const store = getStore();\n if (!store) {\n return {\n ok: false,\n error: \"Redux store is not ready\",\n errorCode: \"store_not_ready\",\n fileName: file.name,\n };\n }\n return cloudUpload(file, options, store.dispatch as AppDispatch);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/files/engine/upload/cloudUpload.ts"],"sourcesContent":["/**\n * features/files/upload/cloudUpload.ts\n *\n * THE single source of truth for file uploads in this app.\n *\n * ════════════════════════════════════════════════════════════════════════\n *\n * Why this exists\n * ───────────────\n * The single underlying upload primitive. Public callers go through the\n * universal file handler (`fileHandler.upload(...)` /\n * `useFileUpload`), which calls this. Server-side routes use\n * `Api.Server.uploadAndShare` which wraps the same Python `/files/upload`\n * endpoint with a server context.\n *\n * Rule: **every upload in the app goes through `cloudUpload` (this file)\n * via the universal handler, OR through `Api.Server.uploadAndShare`.**\n * Never call `supabase.from(\"cld_*\").upsert(...)` or `ensureFolderPath`\n * from a code path whose goal is \"upload a file.\" The Python backend\n * auto-creates any missing folders when you POST a full `file_path` —\n * the browser never needs to touch `cld_folders` directly. That\n * sidesteps the known RLS recursion bug AND keeps logic centralized.\n *\n * What this module owns:\n * • Resolving a logical `file_path` from caller-supplied options.\n * • Calling the Python `/files/upload` endpoint with progress.\n * • Optionally creating a permanent share link.\n * • Dispatching Redux upserts so the slice stays in sync.\n * • Returning a uniform `{ ok: true, ... } | { ok: false, error }` shape\n * — never null, never `[object Object]`, never silent.\n *\n * What this module does NOT do:\n * • Touch `supabase.from(\"cld_*\")` directly. Reads happen via the\n * SECURITY DEFINER tree RPC (`get_user_file_tree`) and\n * supabase realtime. Writes happen via the Python backend.\n * • Block on folder lookups. The backend handles folder creation\n * atomically as part of upload.\n */\n\nimport {\n uploadFile as Files_uploadFile,\n uploadFileWithProgress as Files_uploadFileWithProgress,\n} from \"../api/files\";\nimport {\n createShareLink as createCanonicalShareLink,\n shareLinkUrl,\n} from \"../host/share-links\";\nimport { pythonShareUrl } from \"../handler/utils/python-base\";\nimport {\n newRequestId,\n type ResponseMeta,\n type UploadProgressEvent,\n} from \"../host/python-client\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\nimport {\n registerRequest,\n releaseRequest,\n} from \"../redux/request-ledger\";\nimport {\n attachChildToFolder,\n trackUploadStart,\n updateUploadProgress,\n updateUploadStatus,\n upsertFile,\n} from \"../redux/slice\";\nimport { apiFileRecordToCloudFile } from \"../redux/converters\";\n// eslint-disable-next-line no-restricted-imports -- transport sibling INSIDE the ring-fenced upload internals; cloudUpload is the ONE transport-policy chokepoint\nimport {\n buildUploadMetadataEnvelope,\n tusUploadRaw,\n} from \"./tusUpload\";\nimport type { AppDispatch } from \"../host/store\";\nimport type { PermissionLevel, Visibility } from \"../types\";\n\n// ─── Transport policy ────────────────────────────────────────────────────\n\n// The buffered-vs-TUS decision has ONE home, `@ai-matrx/data/files` (architecture step 14).\nexport {\n resolveUploadTransport,\n TUS_TRANSPORT_THRESHOLD_BYTES,\n type UploadTransport,\n} from \"@ai-matrx/data/files\";\nimport { resolveUploadTransport, type UploadTransport } from \"@ai-matrx/data/files\";\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\nexport interface CloudUploadOptions {\n /**\n * Full logical file path INCLUDING the filename. Use this when you\n * want to control the exact name (e.g. server-generated \"{uuid}.jpg\").\n */\n filePath?: string;\n /**\n * Folder path (no filename). The browser appends `file.name`\n * automatically. The Python backend auto-creates any missing\n * folders. This is the **preferred** option for almost every caller.\n *\n * Example: `folderPath: \"Images/Chat\"` → uploaded path becomes\n * `\"Images/Chat/<file.name>\"`.\n */\n folderPath?: string;\n /**\n * Existing parentFolderId — only useful when you've already loaded the\n * folder via the tree RPC or realtime. Most callers should pass\n * `folderPath` instead so the backend handles folder creation.\n */\n parentFolderId?: string | null;\n visibility?: Visibility;\n shareWith?: string[];\n shareLevel?: PermissionLevel;\n changeSummary?: string;\n metadata?: Record<string, unknown>;\n /** Progress callback (XHR upload progress). */\n onProgress?: (event: UploadProgressEvent) => void;\n signal?: AbortSignal;\n /**\n * Transport override. Default: `resolveUploadTransport` picks TUS for\n * files ≥ `TUS_TRANSPORT_THRESHOLD_BYTES`, buffered otherwise.\n */\n transport?: UploadTransport;\n /**\n * If true, also creates a permanent share link after upload. Returns\n * the `shareUrl` (`/s/:token`) and the raw `shareToken`.\n */\n createShareLink?: boolean;\n /** Share-link permission — canonical `viewer` / `editor` levels. */\n shareLinkPermissionLevel?: \"viewer\" | \"editor\";\n shareLinkExpiresAt?: string | null;\n shareLinkMaxUses?: number | null;\n}\n\nexport interface CloudUploadSuccess {\n ok: true;\n fileId: string;\n filePath: string;\n fileSize: number | null;\n versionNumber: number;\n /** Backend-provided URL (storage URL — typically requires auth or signed). */\n url: string | null;\n shareToken?: string;\n /**\n * The PUBLIC LANDING PAGE for the share link, e.g.\n * `https://app.example.com/s/<token>`. Renders an HTML page with\n * file metadata and a download button. **Do NOT** use this for\n * `<img src>`, `<video src>`, or hot-linking — the page is HTML, not\n * the file bytes.\n */\n shareUrl?: string;\n /**\n * The PUBLIC DIRECT URL for the file bytes — points at Python's\n * `{BACKEND}/share/{token}` endpoint, which serves inline-safe bytes (and may\n * 302-redirect public files to their permanent CDN URL). Embed it in\n * `<img src>`, Slack/Notion unfurls, or OG images. No Next.js hop.\n *\n * Populated whenever `shareUrl` is — they always go in pairs because\n * both are derived from the same share token.\n */\n directUrl?: string;\n}\n\nexport interface CloudUploadFailure {\n ok: false;\n error: string;\n /** Backend error code if available (e.g. \"auth_required\", \"file_too_large\"). */\n errorCode?: string;\n /** Filename for caller reference. */\n fileName: string;\n}\n\nexport type CloudUploadResult = CloudUploadSuccess | CloudUploadFailure;\n\n/**\n * Type guard — when `result.ok` discriminator narrowing isn't enough for\n * TS (e.g. inside loops or where the union is inferred indirectly),\n * call this and the compiler will narrow the branches correctly.\n */\nexport function isCloudUploadFailure(\n result: CloudUploadResult,\n): result is CloudUploadFailure {\n return result.ok === false;\n}\n\nexport function isCloudUploadSuccess(\n result: CloudUploadResult,\n): result is CloudUploadSuccess {\n return result.ok === true;\n}\n\n// ─── Helpers ─────────────────────────────────────────────────────────────\n\nfunction resolveFilePath(file: File, options: CloudUploadOptions): string {\n if (options.filePath) return options.filePath.replace(/^\\/+/, \"\");\n if (options.folderPath) {\n const folder = options.folderPath.replace(/^\\/+|\\/+$/g, \"\");\n return folder ? `${folder}/${file.name}` : file.name;\n }\n return file.name;\n}\n\n/**\n * Build the public DIRECT-FILE URL for a share token — points at Python's\n * `/share/{token}` byte endpoint. No Next.js hop. Embed in\n * `<img src>`, `<video src>`, downloads, Slack unfurls, etc.\n */\nfunction buildDirectShareUrl(token: string): string {\n return pythonShareUrl(token);\n}\n\n/**\n * Mint a canonical share link (`platform.share_links`) for a freshly\n * uploaded file. Throws on failure so both upload paths surface the\n * distinct `share_link_failed` error.\n */\nasync function mintUploadShareLink(\n fileId: string,\n options: CloudUploadOptions,\n): Promise<{ shareToken: string; shareUrl: string; directUrl: string }> {\n const created = await createCanonicalShareLink({\n resourceType: \"file\",\n resourceId: fileId,\n permissionLevel: options.shareLinkPermissionLevel ?? \"viewer\",\n expiresAt: options.shareLinkExpiresAt ?? null,\n maxUses: options.shareLinkMaxUses ?? null,\n });\n if (!created.success || !created.token) {\n throw new Error(created.error ?? \"Share link creation failed\");\n }\n return {\n shareToken: created.token,\n shareUrl: shareLinkUrl(created.token),\n directUrl: buildDirectShareUrl(created.token),\n };\n}\n\n// ─── The upload organization — resolved ONCE, for every transport ────────\n\n/**\n * Bind the owning organization to an upload before a byte moves.\n *\n * 🚨 THE BUG THIS EXISTS FOR (2026-08-30). `InlineUploadArea` — the composer's\n * attach button — sent no organization at all. The server has honoured\n * `metadata.scope.organization_id` all along (membership-checked against\n * `iam.has_org_access_for`), but with nothing supplied it fell back to the\n * uploader's PERSONAL workspace. So a screenshot attached while no organization\n * was selected silently became a personal-workspace file; a minute later the\n * person picked their team organization, and the two no longer agreed.\n *\n * Fixing that one component would have been a patch on one of several upload\n * doors. This is the choke point instead: both transports and every caller go\n * through here, so an upload that forgets its organization is not possible\n * rather than merely unusual.\n *\n * If nothing is selected, the organization gate ASKS (one dialog, then the\n * upload continues into the chosen workspace). Cancelling throws\n * `OrganizationSelectionCancelled`, which the upload paths report as an\n * ordinary cancelled result — no bytes sent, nothing written, nothing to\n * clean up.\n */\nasync function bindUploadOrganization(\n options: CloudUploadOptions,\n): Promise<CloudUploadOptions> {\n const { ensureOrganizationContext } = await import(\n \"../host/org\"\n );\n const declared = organizationIdFromUploadMetadata(options.metadata);\n const organizationId = await ensureOrganizationContext({\n organizationId: declared,\n });\n return {\n ...options,\n metadata: {\n ...(options.metadata ?? {}),\n scope: {\n ...(typeof options.metadata?.scope === \"object\" &&\n options.metadata?.scope !== null\n ? (options.metadata.scope as Record<string, unknown>)\n : {}),\n organization_id: organizationId,\n },\n },\n };\n}\n\n/**\n * Turn a failed organization binding into an ordinary upload result.\n *\n * `cloudUpload`/`cloudUploadRaw` never throw — every caller reads\n * `{ ok: false, error }` — so a cancelled prompt keeps that contract. It is\n * marked `upload_cancelled` rather than an error code so the UI can stay silent:\n * the person did not fail at anything, they declined, and declining must return\n * them exactly where they were.\n */\nfunction uploadOrganizationFailure(\n err: unknown,\n fileName: string,\n): CloudUploadResult {\n const cancelled =\n err instanceof Error && err.name === \"OrganizationSelectionCancelled\";\n return {\n ok: false,\n error: cancelled\n ? \"Upload cancelled — no workspace was selected.\"\n : extractErrorMessage(err),\n errorCode: cancelled ? \"upload_cancelled\" : \"organization_required\",\n fileName,\n };\n}\n\n/** Read an organization a caller already declared, in either accepted shape. */\nexport function organizationIdFromUploadMetadata(\n metadata: Record<string, unknown> | undefined,\n): string | undefined {\n if (!metadata) return undefined;\n const direct = metadata.organization_id;\n if (typeof direct === \"string\" && direct.length > 0) return direct;\n const scope = metadata.scope;\n if (scope && typeof scope === \"object\") {\n const nested = (scope as Record<string, unknown>).organization_id;\n if (typeof nested === \"string\" && nested.length > 0) return nested;\n }\n return undefined;\n}\n\n// ─── Upload primitive (no Redux side effects) ────────────────────────────\n\n/**\n * Pure upload — POSTs to /files/upload with a full `file_path`. Backend\n * auto-creates folders. Use this when you need raw access without Redux\n * dispatches (e.g. server-side route handlers, isolated tests).\n *\n * For browser code that wants the file to appear in the UI, use\n * `cloudUpload` (with dispatch) instead.\n */\nexport async function cloudUploadRaw(\n file: File,\n rawOptions: CloudUploadOptions = {},\n): Promise<CloudUploadResult> {\n let options: CloudUploadOptions;\n try {\n options = await bindUploadOrganization(rawOptions);\n } catch (err) {\n return uploadOrganizationFailure(err, file.name);\n }\n const filePath = resolveFilePath(file, options);\n const requestId = newRequestId();\n\n // Transport policy: large files (or an explicit override) go resumable.\n if (resolveUploadTransport(file.size, options.transport) === \"tus\") {\n const tusResult = await tusUploadRaw(file, filePath, options, requestId);\n if (!tusResult.ok || !options.createShareLink) return tusResult;\n try {\n const link = await mintUploadShareLink(tusResult.fileId, options);\n return { ...tusResult, ...link };\n } catch (linkErr) {\n return {\n ok: false,\n error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,\n errorCode: \"share_link_failed\",\n fileName: file.name,\n };\n }\n }\n\n try {\n const params = {\n file,\n filePath,\n visibility: options.visibility ?? \"personal\",\n shareWith: options.shareWith,\n shareLevel: options.shareLevel,\n changeSummary: options.changeSummary,\n // Shared envelope builder — the SAME object the TUS transport encodes\n // into Upload-Metadata's `metadata_json` (parity is unit-tested).\n metadata: buildUploadMetadataEnvelope(options.metadata),\n };\n\n const upload = options.onProgress\n ? await Files_uploadFileWithProgress(params, options.onProgress, {\n requestId,\n signal: options.signal,\n // Reuse requestId as the idempotency key — single intended\n // upload from the FE perspective, single key on the BE. Backend\n // stores it in `metadata._idempotency_key` so retries don't\n // double-create version rows.\n idempotencyKey: requestId,\n })\n : await Files_uploadFile(params, {\n requestId,\n signal: options.signal,\n idempotencyKey: requestId,\n });\n\n let shareToken: string | undefined;\n let shareUrl: string | undefined;\n let directUrl: string | undefined;\n if (options.createShareLink) {\n try {\n // Always emit the direct-file URL alongside the page URL.\n // Consumers default to `directUrl` for img/video/audio/iframe\n // src; `shareUrl` is for \"click here to view file metadata\".\n ({ shareToken, shareUrl, directUrl } = await mintUploadShareLink(\n upload.data.file_id,\n options,\n ));\n } catch (linkErr) {\n // Upload succeeded; share-link creation didn't. Surface a\n // distinct error so the caller can decide whether to keep the\n // file or roll it back.\n return {\n ok: false,\n error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,\n errorCode: \"share_link_failed\",\n fileName: file.name,\n };\n }\n }\n\n return {\n ok: true,\n fileId: upload.data.file_id,\n filePath: upload.data.file_path,\n // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.\n fileSize: upload.data.size_bytes,\n versionNumber: upload.data.version_number,\n url: upload.data.url ?? null,\n shareToken,\n shareUrl,\n directUrl,\n };\n } catch (err) {\n return {\n ok: false,\n error: extractErrorMessage(err),\n errorCode: (err as { code?: string } | null)?.code,\n fileName: file.name,\n };\n }\n}\n\n// ─── Upload with Redux side effects ──────────────────────────────────────\n\n/**\n * Upload a single file. THIS is the function 99% of callers want.\n *\n * Side effects:\n * • Dispatches `trackUploadStart`/`updateUploadProgress`/`updateUploadStatus`\n * so progress bars in the UI stay in sync.\n * • Dispatches `upsertFile` on success so the file appears in the slice\n * immediately (no need to wait for a tree refresh).\n * • Dispatches `attachChildToFolder` if `parentFolderId` is known.\n *\n * Returns the unified `CloudUploadResult`. Never throws — errors come\n * back as `{ ok: false, error }` so callers always have a clear path.\n */\nexport async function cloudUpload(\n file: File,\n rawOptions: CloudUploadOptions,\n dispatch: AppDispatch,\n): Promise<CloudUploadResult> {\n // Resolved BEFORE `trackUploadStart` so a cancelled organization prompt\n // leaves no orphan progress row in the slice — cancelling must look like the\n // upload never began, because it never did.\n let options: CloudUploadOptions;\n try {\n options = await bindUploadOrganization(rawOptions);\n } catch (err) {\n return uploadOrganizationFailure(err, file.name);\n }\n const filePath = resolveFilePath(file, options);\n const requestId = newRequestId();\n\n // 1. Mark the upload as pending in the slice.\n dispatch(\n trackUploadStart({\n requestId,\n fileName: file.name,\n fileSize: file.size,\n parentFolderId: options.parentFolderId ?? null,\n }),\n );\n registerRequest({\n requestId,\n kind: \"upload\",\n resourceId: null,\n resourceType: \"file\",\n });\n\n try {\n let uploadData: {\n file_id: string;\n file_path: string;\n size_bytes: number | null;\n checksum: string | null;\n version_number: number;\n url: string | null;\n };\n\n if (resolveUploadTransport(file.size, options.transport) === \"tus\") {\n // Resumable transport — same Redux progress/tracking as buffered.\n const tusResult = await tusUploadRaw(\n file,\n filePath,\n {\n ...options,\n onProgress: (ev) => {\n dispatch(\n updateUploadProgress({ requestId, bytesUploaded: ev.loaded }),\n );\n options.onProgress?.(ev);\n },\n },\n requestId,\n );\n if (!tusResult.ok) {\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"error\",\n error: tusResult.error,\n }),\n );\n return tusResult;\n }\n uploadData = {\n file_id: tusResult.fileId,\n file_path: tusResult.filePath,\n size_bytes: tusResult.fileSize,\n checksum: null,\n version_number: tusResult.versionNumber,\n url: tusResult.url,\n };\n } else {\n const params = {\n file,\n filePath,\n visibility: options.visibility ?? \"personal\",\n shareWith: options.shareWith,\n shareLevel: options.shareLevel,\n changeSummary: options.changeSummary,\n // Shared envelope builder — identical object to the TUS transport's\n // `metadata_json` (parity is unit-tested).\n metadata: buildUploadMetadataEnvelope(options.metadata),\n };\n\n const upload = await Files_uploadFileWithProgress(\n params,\n (ev) => {\n dispatch(\n updateUploadProgress({\n requestId,\n bytesUploaded: ev.loaded,\n }),\n );\n options.onProgress?.(ev);\n },\n { requestId, signal: options.signal, idempotencyKey: requestId },\n );\n uploadData = {\n file_id: upload.data.file_id,\n file_path: upload.data.file_path,\n size_bytes: upload.data.size_bytes,\n checksum: upload.data.checksum ?? null,\n version_number: upload.data.version_number,\n url: upload.data.url ?? null,\n };\n }\n\n // 2. Slice upsert — file is now visible in the tree without\n // waiting for the realtime echo or a refetch.\n dispatch(\n upsertFile(\n apiFileRecordToCloudFile({\n id: uploadData.file_id,\n owner_id: \"\",\n file_path: uploadData.file_path,\n file_name: uploadData.file_path.split(\"/\").pop() ?? file.name,\n mime_type: file.type || null,\n // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.\n size_bytes: uploadData.size_bytes,\n checksum: uploadData.checksum,\n visibility: options.visibility ?? \"personal\",\n current_version: uploadData.version_number,\n parent_folder_id: options.parentFolderId ?? null,\n metadata: options.metadata ?? {},\n created_at: null,\n updated_at: null,\n deleted_at: null,\n }),\n ),\n );\n if (options.parentFolderId) {\n dispatch(\n attachChildToFolder({\n parentFolderId: options.parentFolderId,\n kind: \"file\",\n id: uploadData.file_id,\n }),\n );\n }\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"success\",\n fileId: uploadData.file_id,\n }),\n );\n\n // 3. Optional share link.\n let shareToken: string | undefined;\n let shareUrl: string | undefined;\n let directUrl: string | undefined;\n if (options.createShareLink) {\n try {\n // Always emit the direct-file URL alongside the page URL.\n // Consumers default to `directUrl` for img/video/audio/iframe\n // src; `shareUrl` is for \"click here to view file metadata\".\n ({ shareToken, shareUrl, directUrl } = await mintUploadShareLink(\n uploadData.file_id,\n options,\n ));\n } catch (linkErr) {\n return {\n ok: false,\n error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,\n errorCode: \"share_link_failed\",\n fileName: file.name,\n };\n }\n }\n\n return {\n ok: true,\n fileId: uploadData.file_id,\n filePath: uploadData.file_path,\n // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.\n fileSize: uploadData.size_bytes,\n versionNumber: uploadData.version_number,\n url: uploadData.url,\n shareToken,\n shareUrl,\n directUrl,\n };\n } catch (err) {\n const message = extractErrorMessage(err);\n dispatch(\n updateUploadStatus({\n requestId,\n status: \"error\",\n error: message,\n }),\n );\n return {\n ok: false,\n error: message,\n errorCode: (err as { code?: string } | null)?.code,\n fileName: file.name,\n };\n } finally {\n releaseRequest(requestId);\n }\n}\n\n// ─── Batch upload ────────────────────────────────────────────────────────\n\nexport interface CloudUploadManyOptions extends CloudUploadOptions {\n /** Parallel ceiling. Defaults to 3. */\n concurrency?: number;\n}\n\nexport interface CloudUploadManyResult {\n successes: CloudUploadSuccess[];\n failures: CloudUploadFailure[];\n}\n\n/**\n * Upload multiple files with bounded concurrency. Returns a structured\n * result so callers can show \"3 of 5 uploaded\" UI cleanly.\n */\nexport async function cloudUploadMany(\n files: File[],\n options: CloudUploadManyOptions,\n dispatch: AppDispatch,\n): Promise<CloudUploadManyResult> {\n const limit = Math.max(1, options.concurrency ?? 3);\n const successes: CloudUploadSuccess[] = [];\n const failures: CloudUploadFailure[] = [];\n const queue = [...files];\n\n async function worker(): Promise<void> {\n while (queue.length) {\n const file = queue.shift();\n if (!file) return;\n const result = await cloudUpload(file, options, dispatch);\n if (isCloudUploadSuccess(result)) {\n successes.push(result);\n } else {\n failures.push(result);\n }\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(limit, files.length) }, worker),\n );\n\n return { successes, failures };\n}\n\n/**\n * Imperative shortcut for non-React code that still wants Redux side\n * effects. Pulls the dispatch from the store singleton.\n */\nexport async function cloudUploadImperative(\n file: File,\n options: CloudUploadOptions,\n): Promise<CloudUploadResult> {\n const { getStore } = await import(\"../host/store\");\n const store = getStore();\n if (!store) {\n return {\n ok: false,\n error: \"Redux store is not ready\",\n errorCode: \"store_not_ready\",\n fileName: file.name,\n };\n }\n return cloudUpload(file, options, store.dispatch as AppDispatch);\n}\n"],"mappings":"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","names":["resolveUploadTransport"]}
@@ -23,7 +23,7 @@
23
23
  * - Final `X-Cld-File-Id` captured from response headers (final PATCH, or the
24
24
  * completed-session HEAD/POST recovery paths) — a lost final response never
25
25
  * forces a re-upload.
26
- * - Resume URLs live in a dedicated tiny IndexedDB (`mtx-tus-urls`) — NEVER
26
+ * - Resume URLs live in a dedicated tiny browser store (`mtx-tus-urls`, owned by @ai-matrx/data/files/tus) — NEVER
27
27
  * the recorder chunk journal DB.
28
28
  *
29
29
  * NOT LIVE-TESTED against production: the server side (CORS, metadata parity,
@@ -31,70 +31,25 @@
31
31
  * see features/files/handler/FEATURE.md § Transport policy for the pending
32
32
  * E2E note. The client is unit-tested with an injected HttpStack.
33
33
  */
34
- import * as tus from "tus-js-client";
34
+ import { type TusHttpStack, type TusFileReader, type TusUrlStorage } from "@ai-matrx/data/files/tus";
35
35
  import type { CloudUploadOptions, CloudUploadResult } from "./cloudUpload.js";
36
- /** Server-bounded chunk size (8–32 MiB allowed): 16 MiB. */
37
- export declare const TUS_CHUNK_SIZE_BYTES: number;
38
- export declare const TUS_UPLOAD_PATH = "/files/upload/tus";
39
- /**
40
- * The ONE builder for the upload metadata JSON object. The buffered path
41
- * serializes this into the `metadata_json` form field
42
- * (`features/files/api/files.ts`); the TUS path serializes the SAME object
43
- * into the `metadata_json` Upload-Metadata key. One builder ⇒ the two
44
- * transports can never drift (parity is unit-tested).
45
- *
46
- * 🚨 `scope.organization_id` is the OWNING WORKSPACE of the uploaded file, and
47
- * it is not decoration. The server reads it (`SyncEngine._metadata_organization_id`,
48
- * membership-checked against `iam.has_org_access_for`) and stamps
49
- * `files.files.organization_id` from it. Omit it and the server falls back to
50
- * the uploader's OWN organization — which is how, on 2026-08-30, a screenshot
51
- * attached from the composer landed in a own organization nobody had chosen
52
- * and then disagreed with the team organization the person actually picked.
53
- *
54
- * Callers never set it by hand: `bindUploadOrganization` in `cloudUpload.ts`
55
- * resolves it for every upload before a byte moves, so it is already present in
56
- * `metadata` by the time this builder runs. This function keeps the shape
57
- * canonical and is where the contract is written down.
58
- */
59
- export declare function buildUploadMetadataEnvelope(metadata: Record<string, unknown> | undefined): Record<string, unknown>;
60
- /** tus-js-client UrlStorage backed by the dedicated `mtx-tus-urls` DB —
61
- * deliberately SEPARATE from the recorder chunk journal. */
62
- export declare function createTusUrlStorage(): NonNullable<ConstructorParameters<typeof tus.Upload>[1]["urlStorage"]>;
63
- /** Read-only summary of a stored TUS resume session (diagnostics surfaces —
64
- * /camera's resume-pending indicator, /camera/admin). */
65
- export interface StoredTusUploadSummary {
66
- urlStorageKey: string;
67
- fingerprint: string;
68
- size: number | null;
69
- metadata: Record<string, string>;
70
- creationTime: string;
71
- uploadUrl: string | null;
72
- }
73
- /**
74
- * List every stored TUS resume-URL entry (the `mtx-tus-urls` DB). Read-only —
75
- * resuming still happens inside `tusUploadRaw` via the UrlStorage; this only
76
- * lets management surfaces SHOW that a resumable session is pending. Returns
77
- * [] where IndexedDB is unavailable (never throws — diagnostics must not
78
- * break a page).
79
- */
80
- export declare function listStoredTusUploads(): Promise<StoredTusUploadSummary[]>;
36
+ export { buildUploadMetadataEnvelope, createTusUrlStorage, listStoredTusUploads, TUS_CHUNK_SIZE_BYTES, TUS_UPLOAD_PATH, type StoredTusUploadSummary, } from "@ai-matrx/data/files/tus";
81
37
  export interface TusUploadDeps {
82
38
  /** Injected HttpStack for unit tests (mock wire, no XHR). */
83
- httpStack?: tus.HttpStack;
39
+ httpStack?: TusHttpStack;
84
40
  /** Injected file reader for unit tests (Node builds of tus-js-client
85
41
  * cannot slice browser Files). */
86
- fileReader?: ConstructorParameters<typeof tus.Upload>[1]["fileReader"];
42
+ fileReader?: TusFileReader;
87
43
  /** Override the endpoint (tests). */
88
44
  endpointOverride?: string;
89
45
  /** Override URL storage (tests). */
90
- urlStorage?: ReturnType<typeof createTusUrlStorage>;
46
+ urlStorage?: TusUrlStorage;
91
47
  }
92
48
  /**
93
49
  * Resumable upload of one file via TUS. Same result contract as
94
50
  * `cloudUploadRaw` — `{ ok: true, fileId, ... } | { ok: false, error }`,
95
- * never throws. Automatically resumes a previous session for the same file
96
- * (custom UrlStorage + server HEAD), including the completed-session
97
- * recovery: when the server reports the session already finished and hands
98
- * back `X-Cld-File-Id`, the upload is skipped and the file resolves directly.
51
+ * never throws. The transfer is data's `tusTransfer`; this binds fresh
52
+ * host headers (mandatory-org, fail-closed `buildHeaders`) to every request
53
+ * and hydrates the canonical row so the result matches the buffered contract.
99
54
  */
100
55
  export declare function tusUploadRaw(file: File, filePath: string, options: CloudUploadOptions, idempotencyKey: string, deps?: TusUploadDeps): Promise<CloudUploadResult>;