@ai-matrx/media 0.8.0 → 0.9.1

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.
Files changed (185) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/files/engine/api/assets.d.ts +223 -0
  3. package/dist/files/engine/api/assets.js +124 -0
  4. package/dist/files/engine/api/assets.js.map +1 -0
  5. package/dist/files/engine/api/direct.d.ts +43 -0
  6. package/dist/files/engine/api/direct.js +73 -0
  7. package/dist/files/engine/api/direct.js.map +1 -0
  8. package/dist/files/engine/api/fileOrganization.d.ts +34 -0
  9. package/dist/files/engine/api/fileOrganization.js +38 -0
  10. package/dist/files/engine/api/fileOrganization.js.map +1 -0
  11. package/dist/files/engine/api/files.d.ts +249 -0
  12. package/dist/files/engine/api/files.js +231 -0
  13. package/dist/files/engine/api/files.js.map +1 -0
  14. package/dist/files/engine/api/folders.d.ts +63 -0
  15. package/dist/files/engine/api/folders.js +56 -0
  16. package/dist/files/engine/api/folders.js.map +1 -0
  17. package/dist/files/engine/api/office.d.ts +27 -0
  18. package/dist/files/engine/api/office.js +19 -0
  19. package/dist/files/engine/api/office.js.map +1 -0
  20. package/dist/files/engine/api/permissions.d.ts +37 -0
  21. package/dist/files/engine/api/permissions.js +109 -0
  22. package/dist/files/engine/api/permissions.js.map +1 -0
  23. package/dist/files/engine/api/versions.d.ts +26 -0
  24. package/dist/files/engine/api/versions.js +37 -0
  25. package/dist/files/engine/api/versions.js.map +1 -0
  26. package/dist/files/engine/cache/idb-store.d.ts +95 -0
  27. package/dist/files/engine/cache/idb-store.js +172 -0
  28. package/dist/files/engine/cache/idb-store.js.map +1 -0
  29. package/dist/files/engine/cache/policy.d.ts +14 -0
  30. package/dist/files/engine/cache/policy.js +40 -0
  31. package/dist/files/engine/cache/policy.js.map +1 -0
  32. package/dist/files/engine/cache/register-service-worker.d.ts +57 -0
  33. package/dist/files/engine/cache/register-service-worker.js +80 -0
  34. package/dist/files/engine/cache/register-service-worker.js.map +1 -0
  35. package/dist/files/engine/db-types.d.ts +48865 -0
  36. package/dist/files/engine/db-types.js +1 -0
  37. package/dist/files/engine/db-types.js.map +1 -0
  38. package/dist/files/engine/filesDb.d.ts +1913 -0
  39. package/dist/files/engine/filesDb.js +28 -0
  40. package/dist/files/engine/filesDb.js.map +1 -0
  41. package/dist/files/engine/handler/handler.d.ts +82 -0
  42. package/dist/files/engine/handler/handler.js +124 -0
  43. package/dist/files/engine/handler/handler.js.map +1 -0
  44. package/dist/files/engine/handler/hooks/useFile.d.ts +17 -0
  45. package/dist/files/engine/handler/hooks/useFile.js +75 -0
  46. package/dist/files/engine/handler/hooks/useFile.js.map +1 -0
  47. package/dist/files/engine/handler/hooks/useFileUpload.d.ts +76 -0
  48. package/dist/files/engine/handler/hooks/useFileUpload.js +67 -0
  49. package/dist/files/engine/handler/hooks/useFileUpload.js.map +1 -0
  50. package/dist/files/engine/handler/input/normalize.d.ts +14 -0
  51. package/dist/files/engine/handler/input/normalize.js +365 -0
  52. package/dist/files/engine/handler/input/normalize.js.map +1 -0
  53. package/dist/files/engine/handler/intelligence/access.d.ts +35 -0
  54. package/dist/files/engine/handler/intelligence/access.js +85 -0
  55. package/dist/files/engine/handler/intelligence/access.js.map +1 -0
  56. package/dist/files/engine/handler/intelligence/magic-bytes.d.ts +21 -0
  57. package/dist/files/engine/handler/intelligence/magic-bytes.js +67 -0
  58. package/dist/files/engine/handler/intelligence/magic-bytes.js.map +1 -0
  59. package/dist/files/engine/handler/output/target.d.ts +19 -0
  60. package/dist/files/engine/handler/output/target.js +222 -0
  61. package/dist/files/engine/handler/output/target.js.map +1 -0
  62. package/dist/files/engine/handler/resolver.d.ts +34 -0
  63. package/dist/files/engine/handler/resolver.js +133 -0
  64. package/dist/files/engine/handler/resolver.js.map +1 -0
  65. package/dist/files/engine/handler/types.d.ts +367 -0
  66. package/dist/files/engine/handler/types.js +1 -0
  67. package/dist/files/engine/handler/types.js.map +1 -0
  68. package/dist/files/engine/handler/upload.d.ts +32 -0
  69. package/dist/files/engine/handler/upload.js +295 -0
  70. package/dist/files/engine/handler/upload.js.map +1 -0
  71. package/dist/files/engine/handler/utils/classify.d.ts +14 -0
  72. package/dist/files/engine/handler/utils/classify.js +27 -0
  73. package/dist/files/engine/handler/utils/classify.js.map +1 -0
  74. package/dist/files/engine/handler/utils/prefer-locator.d.ts +32 -0
  75. package/dist/files/engine/handler/utils/prefer-locator.js +34 -0
  76. package/dist/files/engine/handler/utils/prefer-locator.js.map +1 -0
  77. package/dist/files/engine/handler/utils/python-base.d.ts +129 -0
  78. package/dist/files/engine/handler/utils/python-base.js +60 -0
  79. package/dist/files/engine/handler/utils/python-base.js.map +1 -0
  80. package/dist/files/engine/hooks/blob-cache.d.ts +130 -0
  81. package/dist/files/engine/hooks/blob-cache.js +162 -0
  82. package/dist/files/engine/hooks/blob-cache.js.map +1 -0
  83. package/dist/files/engine/hooks/office-extraction-cache.d.ts +28 -0
  84. package/dist/files/engine/hooks/office-extraction-cache.js +67 -0
  85. package/dist/files/engine/hooks/office-extraction-cache.js.map +1 -0
  86. package/dist/files/engine/host/configure.d.ts +166 -0
  87. package/dist/files/engine/host/configure.js +31 -0
  88. package/dist/files/engine/host/configure.js.map +1 -0
  89. package/dist/files/engine/host/org.d.ts +8 -0
  90. package/dist/files/engine/host/org.js +16 -0
  91. package/dist/files/engine/host/org.js.map +1 -0
  92. package/dist/files/engine/host/python-client.d.ts +60 -0
  93. package/dist/files/engine/host/python-client.js +46 -0
  94. package/dist/files/engine/host/python-client.js.map +1 -0
  95. package/dist/files/engine/host/share-links.d.ts +20 -0
  96. package/dist/files/engine/host/share-links.js +32 -0
  97. package/dist/files/engine/host/share-links.js.map +1 -0
  98. package/dist/files/engine/host/store.d.ts +24 -0
  99. package/dist/files/engine/host/store.js +23 -0
  100. package/dist/files/engine/host/store.js.map +1 -0
  101. package/dist/files/engine/host/supabase.d.ts +3808 -0
  102. package/dist/files/engine/host/supabase.js +33 -0
  103. package/dist/files/engine/host/supabase.js.map +1 -0
  104. package/dist/files/engine/host/toast.d.ts +14 -0
  105. package/dist/files/engine/host/toast.js +20 -0
  106. package/dist/files/engine/host/toast.js.map +1 -0
  107. package/dist/files/engine/host/typed-client.d.ts +136 -0
  108. package/dist/files/engine/host/typed-client.js +66 -0
  109. package/dist/files/engine/host/typed-client.js.map +1 -0
  110. package/dist/files/engine/index.d.ts +22 -0
  111. package/dist/files/engine/index.js +49 -0
  112. package/dist/files/engine/index.js.map +1 -0
  113. package/dist/files/engine/media/our-file-sources.d.ts +39 -0
  114. package/dist/files/engine/media/our-file-sources.js +83 -0
  115. package/dist/files/engine/media/our-file-sources.js.map +1 -0
  116. package/dist/files/engine/media/signed-url.d.ts +1 -0
  117. package/dist/files/engine/media/signed-url.js +6 -0
  118. package/dist/files/engine/media/signed-url.js.map +1 -0
  119. package/dist/files/engine/redux/converters.d.ts +73 -0
  120. package/dist/files/engine/redux/converters.js +286 -0
  121. package/dist/files/engine/redux/converters.js.map +1 -0
  122. package/dist/files/engine/redux/file-hydration.d.ts +15 -0
  123. package/dist/files/engine/redux/file-hydration.js +46 -0
  124. package/dist/files/engine/redux/file-hydration.js.map +1 -0
  125. package/dist/files/engine/redux/file-tree-auth-boundary.d.ts +5 -0
  126. package/dist/files/engine/redux/file-tree-auth-boundary.js +28 -0
  127. package/dist/files/engine/redux/file-tree-auth-boundary.js.map +1 -0
  128. package/dist/files/engine/redux/file-tree-timeout.d.ts +32 -0
  129. package/dist/files/engine/redux/file-tree-timeout.js +73 -0
  130. package/dist/files/engine/redux/file-tree-timeout.js.map +1 -0
  131. package/dist/files/engine/redux/mutation-toast-middleware.d.ts +27 -0
  132. package/dist/files/engine/redux/mutation-toast-middleware.js +103 -0
  133. package/dist/files/engine/redux/mutation-toast-middleware.js.map +1 -0
  134. package/dist/files/engine/redux/realtime-middleware.d.ts +61 -0
  135. package/dist/files/engine/redux/realtime-middleware.js +431 -0
  136. package/dist/files/engine/redux/realtime-middleware.js.map +1 -0
  137. package/dist/files/engine/redux/request-ledger.d.ts +68 -0
  138. package/dist/files/engine/redux/request-ledger.js +66 -0
  139. package/dist/files/engine/redux/request-ledger.js.map +1 -0
  140. package/dist/files/engine/redux/selectors.d.ts +3254 -0
  141. package/dist/files/engine/redux/selectors.js +428 -0
  142. package/dist/files/engine/redux/selectors.js.map +1 -0
  143. package/dist/files/engine/redux/slice.d.ts +157 -0
  144. package/dist/files/engine/redux/slice.js +836 -0
  145. package/dist/files/engine/redux/slice.js.map +1 -0
  146. package/dist/files/engine/redux/thunks.d.ts +449 -0
  147. package/dist/files/engine/redux/thunks.js +1684 -0
  148. package/dist/files/engine/redux/thunks.js.map +1 -0
  149. package/dist/files/engine/redux/tree-utils.d.ts +90 -0
  150. package/dist/files/engine/redux/tree-utils.js +200 -0
  151. package/dist/files/engine/redux/tree-utils.js.map +1 -0
  152. package/dist/files/engine/support/claimsUser.d.ts +77 -0
  153. package/dist/files/engine/support/claimsUser.js +48 -0
  154. package/dist/files/engine/support/claimsUser.js.map +1 -0
  155. package/dist/files/engine/support/datetime.d.ts +6 -0
  156. package/dist/files/engine/support/datetime.js +15 -0
  157. package/dist/files/engine/support/datetime.js.map +1 -0
  158. package/dist/files/engine/support/document-visibility.d.ts +14 -0
  159. package/dist/files/engine/support/document-visibility.js +28 -0
  160. package/dist/files/engine/support/document-visibility.js.map +1 -0
  161. package/dist/files/engine/support/logger.d.ts +28 -0
  162. package/dist/files/engine/support/logger.js +36 -0
  163. package/dist/files/engine/support/logger.js.map +1 -0
  164. package/dist/files/engine/types.d.ts +1081 -0
  165. package/dist/files/engine/types.js +30 -0
  166. package/dist/files/engine/types.js.map +1 -0
  167. package/dist/files/engine/upload/cloudUpload.d.ts +182 -0
  168. package/dist/files/engine/upload/cloudUpload.js +418 -0
  169. package/dist/files/engine/upload/cloudUpload.js.map +1 -0
  170. package/dist/files/engine/upload/tusUpload.d.ts +100 -0
  171. package/dist/files/engine/upload/tusUpload.js +258 -0
  172. package/dist/files/engine/upload/tusUpload.js.map +1 -0
  173. package/dist/files/engine/upload/uploadDedupGuard.d.ts +58 -0
  174. package/dist/files/engine/upload/uploadDedupGuard.js +41 -0
  175. package/dist/files/engine/upload/uploadDedupGuard.js.map +1 -0
  176. package/dist/files/engine/upload/uploadGuardOpeners.d.ts +70 -0
  177. package/dist/files/engine/upload/uploadGuardOpeners.js +54 -0
  178. package/dist/files/engine/upload/uploadGuardOpeners.js.map +1 -0
  179. package/dist/files/engine/utils/file-types.d.ts +243 -0
  180. package/dist/files/engine/utils/file-types.js +1722 -0
  181. package/dist/files/engine/utils/file-types.js.map +1 -0
  182. package/dist/files/engine/utils/user-visible.d.ts +133 -0
  183. package/dist/files/engine/utils/user-visible.js +115 -0
  184. package/dist/files/engine/utils/user-visible.js.map +1 -0
  185. package/package.json +392 -8
@@ -0,0 +1,418 @@
1
+ import {
2
+ uploadFile as Files_uploadFile,
3
+ uploadFileWithProgress as Files_uploadFileWithProgress
4
+ } from "../api/files.js";
5
+ import {
6
+ createShareLink as createCanonicalShareLink,
7
+ shareLinkUrl
8
+ } from "../host/share-links.js";
9
+ import { pythonShareUrl } from "../handler/utils/python-base.js";
10
+ import {
11
+ newRequestId
12
+ } from "../host/python-client.js";
13
+ import { extractErrorMessage } from "@ai-matrx/data/net";
14
+ import {
15
+ registerRequest,
16
+ releaseRequest
17
+ } from "../redux/request-ledger.js";
18
+ import {
19
+ attachChildToFolder,
20
+ trackUploadStart,
21
+ updateUploadProgress,
22
+ updateUploadStatus,
23
+ upsertFile
24
+ } from "../redux/slice.js";
25
+ import { apiFileRecordToCloudFile } from "../redux/converters.js";
26
+ import {
27
+ buildUploadMetadataEnvelope,
28
+ tusUploadRaw
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
+ }
35
+ function isCloudUploadFailure(result) {
36
+ return result.ok === false;
37
+ }
38
+ function isCloudUploadSuccess(result) {
39
+ return result.ok === true;
40
+ }
41
+ function resolveFilePath(file, options) {
42
+ if (options.filePath) return options.filePath.replace(/^\/+/, "");
43
+ if (options.folderPath) {
44
+ const folder = options.folderPath.replace(/^\/+|\/+$/g, "");
45
+ return folder ? `${folder}/${file.name}` : file.name;
46
+ }
47
+ return file.name;
48
+ }
49
+ function buildDirectShareUrl(token) {
50
+ return pythonShareUrl(token);
51
+ }
52
+ async function mintUploadShareLink(fileId, options) {
53
+ const created = await createCanonicalShareLink({
54
+ resourceType: "file",
55
+ resourceId: fileId,
56
+ permissionLevel: options.shareLinkPermissionLevel ?? "viewer",
57
+ expiresAt: options.shareLinkExpiresAt ?? null,
58
+ maxUses: options.shareLinkMaxUses ?? null
59
+ });
60
+ if (!created.success || !created.token) {
61
+ throw new Error(created.error ?? "Share link creation failed");
62
+ }
63
+ return {
64
+ shareToken: created.token,
65
+ shareUrl: shareLinkUrl(created.token),
66
+ directUrl: buildDirectShareUrl(created.token)
67
+ };
68
+ }
69
+ async function bindUploadOrganization(options) {
70
+ const { ensureOrganizationContext } = await import("../host/org.js");
71
+ const declared = organizationIdFromUploadMetadata(options.metadata);
72
+ const organizationId = await ensureOrganizationContext({
73
+ organizationId: declared
74
+ });
75
+ return {
76
+ ...options,
77
+ metadata: {
78
+ ...options.metadata ?? {},
79
+ scope: {
80
+ ...typeof options.metadata?.scope === "object" && options.metadata?.scope !== null ? options.metadata.scope : {},
81
+ organization_id: organizationId
82
+ }
83
+ }
84
+ };
85
+ }
86
+ function uploadOrganizationFailure(err, fileName) {
87
+ const cancelled = err instanceof Error && err.name === "OrganizationSelectionCancelled";
88
+ return {
89
+ ok: false,
90
+ error: cancelled ? "Upload cancelled \u2014 no workspace was selected." : extractErrorMessage(err),
91
+ errorCode: cancelled ? "upload_cancelled" : "organization_required",
92
+ fileName
93
+ };
94
+ }
95
+ function organizationIdFromUploadMetadata(metadata) {
96
+ if (!metadata) return void 0;
97
+ const direct = metadata.organization_id;
98
+ if (typeof direct === "string" && direct.length > 0) return direct;
99
+ const scope = metadata.scope;
100
+ if (scope && typeof scope === "object") {
101
+ const nested = scope.organization_id;
102
+ if (typeof nested === "string" && nested.length > 0) return nested;
103
+ }
104
+ return void 0;
105
+ }
106
+ async function cloudUploadRaw(file, rawOptions = {}) {
107
+ let options;
108
+ try {
109
+ options = await bindUploadOrganization(rawOptions);
110
+ } catch (err) {
111
+ return uploadOrganizationFailure(err, file.name);
112
+ }
113
+ const filePath = resolveFilePath(file, options);
114
+ const requestId = newRequestId();
115
+ if (resolveUploadTransport(file.size, options.transport) === "tus") {
116
+ const tusResult = await tusUploadRaw(file, filePath, options, requestId);
117
+ if (!tusResult.ok || !options.createShareLink) return tusResult;
118
+ try {
119
+ const link = await mintUploadShareLink(tusResult.fileId, options);
120
+ return { ...tusResult, ...link };
121
+ } catch (linkErr) {
122
+ return {
123
+ ok: false,
124
+ error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,
125
+ errorCode: "share_link_failed",
126
+ fileName: file.name
127
+ };
128
+ }
129
+ }
130
+ try {
131
+ const params = {
132
+ file,
133
+ filePath,
134
+ visibility: options.visibility ?? "personal",
135
+ shareWith: options.shareWith,
136
+ shareLevel: options.shareLevel,
137
+ changeSummary: options.changeSummary,
138
+ // Shared envelope builder — the SAME object the TUS transport encodes
139
+ // into Upload-Metadata's `metadata_json` (parity is unit-tested).
140
+ metadata: buildUploadMetadataEnvelope(options.metadata)
141
+ };
142
+ const upload = options.onProgress ? await Files_uploadFileWithProgress(params, options.onProgress, {
143
+ requestId,
144
+ signal: options.signal,
145
+ // Reuse requestId as the idempotency key — single intended
146
+ // upload from the FE perspective, single key on the BE. Backend
147
+ // stores it in `metadata._idempotency_key` so retries don't
148
+ // double-create version rows.
149
+ idempotencyKey: requestId
150
+ }) : await Files_uploadFile(params, {
151
+ requestId,
152
+ signal: options.signal,
153
+ idempotencyKey: requestId
154
+ });
155
+ let shareToken;
156
+ let shareUrl;
157
+ let directUrl;
158
+ if (options.createShareLink) {
159
+ try {
160
+ ({ shareToken, shareUrl, directUrl } = await mintUploadShareLink(
161
+ upload.data.file_id,
162
+ options
163
+ ));
164
+ } catch (linkErr) {
165
+ return {
166
+ ok: false,
167
+ error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,
168
+ errorCode: "share_link_failed",
169
+ fileName: file.name
170
+ };
171
+ }
172
+ }
173
+ return {
174
+ ok: true,
175
+ fileId: upload.data.file_id,
176
+ filePath: upload.data.file_path,
177
+ // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.
178
+ fileSize: upload.data.size_bytes,
179
+ versionNumber: upload.data.version_number,
180
+ url: upload.data.url ?? null,
181
+ shareToken,
182
+ shareUrl,
183
+ directUrl
184
+ };
185
+ } catch (err) {
186
+ return {
187
+ ok: false,
188
+ error: extractErrorMessage(err),
189
+ errorCode: err?.code,
190
+ fileName: file.name
191
+ };
192
+ }
193
+ }
194
+ async function cloudUpload(file, rawOptions, dispatch) {
195
+ let options;
196
+ try {
197
+ options = await bindUploadOrganization(rawOptions);
198
+ } catch (err) {
199
+ return uploadOrganizationFailure(err, file.name);
200
+ }
201
+ const filePath = resolveFilePath(file, options);
202
+ const requestId = newRequestId();
203
+ dispatch(
204
+ trackUploadStart({
205
+ requestId,
206
+ fileName: file.name,
207
+ fileSize: file.size,
208
+ parentFolderId: options.parentFolderId ?? null
209
+ })
210
+ );
211
+ registerRequest({
212
+ requestId,
213
+ kind: "upload",
214
+ resourceId: null,
215
+ resourceType: "file"
216
+ });
217
+ try {
218
+ let uploadData;
219
+ if (resolveUploadTransport(file.size, options.transport) === "tus") {
220
+ const tusResult = await tusUploadRaw(
221
+ file,
222
+ filePath,
223
+ {
224
+ ...options,
225
+ onProgress: (ev) => {
226
+ dispatch(
227
+ updateUploadProgress({ requestId, bytesUploaded: ev.loaded })
228
+ );
229
+ options.onProgress?.(ev);
230
+ }
231
+ },
232
+ requestId
233
+ );
234
+ if (!tusResult.ok) {
235
+ dispatch(
236
+ updateUploadStatus({
237
+ requestId,
238
+ status: "error",
239
+ error: tusResult.error
240
+ })
241
+ );
242
+ return tusResult;
243
+ }
244
+ uploadData = {
245
+ file_id: tusResult.fileId,
246
+ file_path: tusResult.filePath,
247
+ size_bytes: tusResult.fileSize,
248
+ checksum: null,
249
+ version_number: tusResult.versionNumber,
250
+ url: tusResult.url
251
+ };
252
+ } else {
253
+ const params = {
254
+ file,
255
+ filePath,
256
+ visibility: options.visibility ?? "personal",
257
+ shareWith: options.shareWith,
258
+ shareLevel: options.shareLevel,
259
+ changeSummary: options.changeSummary,
260
+ // Shared envelope builder — identical object to the TUS transport's
261
+ // `metadata_json` (parity is unit-tested).
262
+ metadata: buildUploadMetadataEnvelope(options.metadata)
263
+ };
264
+ const upload = await Files_uploadFileWithProgress(
265
+ params,
266
+ (ev) => {
267
+ dispatch(
268
+ updateUploadProgress({
269
+ requestId,
270
+ bytesUploaded: ev.loaded
271
+ })
272
+ );
273
+ options.onProgress?.(ev);
274
+ },
275
+ { requestId, signal: options.signal, idempotencyKey: requestId }
276
+ );
277
+ uploadData = {
278
+ file_id: upload.data.file_id,
279
+ file_path: upload.data.file_path,
280
+ size_bytes: upload.data.size_bytes,
281
+ checksum: upload.data.checksum ?? null,
282
+ version_number: upload.data.version_number,
283
+ url: upload.data.url ?? null
284
+ };
285
+ }
286
+ dispatch(
287
+ upsertFile(
288
+ apiFileRecordToCloudFile({
289
+ id: uploadData.file_id,
290
+ owner_id: "",
291
+ file_path: uploadData.file_path,
292
+ file_name: uploadData.file_path.split("/").pop() ?? file.name,
293
+ mime_type: file.type || null,
294
+ // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.
295
+ size_bytes: uploadData.size_bytes,
296
+ checksum: uploadData.checksum,
297
+ visibility: options.visibility ?? "personal",
298
+ current_version: uploadData.version_number,
299
+ parent_folder_id: options.parentFolderId ?? null,
300
+ metadata: options.metadata ?? {},
301
+ created_at: null,
302
+ updated_at: null,
303
+ deleted_at: null
304
+ })
305
+ )
306
+ );
307
+ if (options.parentFolderId) {
308
+ dispatch(
309
+ attachChildToFolder({
310
+ parentFolderId: options.parentFolderId,
311
+ kind: "file",
312
+ id: uploadData.file_id
313
+ })
314
+ );
315
+ }
316
+ dispatch(
317
+ updateUploadStatus({
318
+ requestId,
319
+ status: "success",
320
+ fileId: uploadData.file_id
321
+ })
322
+ );
323
+ let shareToken;
324
+ let shareUrl;
325
+ let directUrl;
326
+ if (options.createShareLink) {
327
+ try {
328
+ ({ shareToken, shareUrl, directUrl } = await mintUploadShareLink(
329
+ uploadData.file_id,
330
+ options
331
+ ));
332
+ } catch (linkErr) {
333
+ return {
334
+ ok: false,
335
+ error: `File uploaded but share link couldn't be created: ${extractErrorMessage(linkErr)}`,
336
+ errorCode: "share_link_failed",
337
+ fileName: file.name
338
+ };
339
+ }
340
+ }
341
+ return {
342
+ ok: true,
343
+ fileId: uploadData.file_id,
344
+ filePath: uploadData.file_path,
345
+ // Phase 0 rename — see docs/PYTHON_UPDATES.md §3.
346
+ fileSize: uploadData.size_bytes,
347
+ versionNumber: uploadData.version_number,
348
+ url: uploadData.url,
349
+ shareToken,
350
+ shareUrl,
351
+ directUrl
352
+ };
353
+ } catch (err) {
354
+ const message = extractErrorMessage(err);
355
+ dispatch(
356
+ updateUploadStatus({
357
+ requestId,
358
+ status: "error",
359
+ error: message
360
+ })
361
+ );
362
+ return {
363
+ ok: false,
364
+ error: message,
365
+ errorCode: err?.code,
366
+ fileName: file.name
367
+ };
368
+ } finally {
369
+ releaseRequest(requestId);
370
+ }
371
+ }
372
+ async function cloudUploadMany(files, options, dispatch) {
373
+ const limit = Math.max(1, options.concurrency ?? 3);
374
+ const successes = [];
375
+ const failures = [];
376
+ const queue = [...files];
377
+ async function worker() {
378
+ while (queue.length) {
379
+ const file = queue.shift();
380
+ if (!file) return;
381
+ const result = await cloudUpload(file, options, dispatch);
382
+ if (isCloudUploadSuccess(result)) {
383
+ successes.push(result);
384
+ } else {
385
+ failures.push(result);
386
+ }
387
+ }
388
+ }
389
+ await Promise.all(
390
+ Array.from({ length: Math.min(limit, files.length) }, worker)
391
+ );
392
+ return { successes, failures };
393
+ }
394
+ async function cloudUploadImperative(file, options) {
395
+ const { getStore } = await import("../host/store.js");
396
+ const store = getStore();
397
+ if (!store) {
398
+ return {
399
+ ok: false,
400
+ error: "Redux store is not ready",
401
+ errorCode: "store_not_ready",
402
+ fileName: file.name
403
+ };
404
+ }
405
+ return cloudUpload(file, options, store.dispatch);
406
+ }
407
+ export {
408
+ TUS_TRANSPORT_THRESHOLD_BYTES,
409
+ cloudUpload,
410
+ cloudUploadImperative,
411
+ cloudUploadMany,
412
+ cloudUploadRaw,
413
+ isCloudUploadFailure,
414
+ isCloudUploadSuccess,
415
+ organizationIdFromUploadMetadata,
416
+ resolveUploadTransport
417
+ };
418
+ //# sourceMappingURL=cloudUpload.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * features/files/upload/tusUpload.ts
3
+ *
4
+ * Resumable (TUS) upload transport for the file handler — the large-file
5
+ * sibling of the buffered multipart path in `cloudUpload.ts`. Callers never
6
+ * import this directly: `cloudUpload` routes here per the transport policy
7
+ * (size ≥ `TUS_TRANSPORT_THRESHOLD_BYTES`, or an explicit
8
+ * `transport: "tus"` override).
9
+ *
10
+ * Wire contract (common-docs/systems/media/media-capture/FEATURE.md § TUS):
11
+ * - Endpoint: `${PYTHON_BACKEND}/files/upload/tus` (resolved through the same
12
+ * base-url helper the python-client uses — server toggle respected).
13
+ * - Explicit chunk size 16 MiB (server bounds 8–32 MiB; never the client
14
+ * default).
15
+ * - `Upload-Metadata` carries `filename`, `filepath`, and ONE `metadata_json`
16
+ * key — the base64 of the SAME JSON object the buffered path sends as its
17
+ * `metadata_json` form field (tus-js-client base64-encodes values; the
18
+ * object is built by the shared `buildUploadMetadataEnvelope`). Parity with
19
+ * buffered uploads is a tested invariant.
20
+ * - FRESH Authorization per request (`onBeforeRequest` re-reads the Supabase
21
+ * session — long uploads outlive a single JWT).
22
+ * - `X-Idempotency-Key` on the creation POST only.
23
+ * - Final `X-Cld-File-Id` captured from response headers (final PATCH, or the
24
+ * completed-session HEAD/POST recovery paths) — a lost final response never
25
+ * forces a re-upload.
26
+ * - Resume URLs live in a dedicated tiny IndexedDB (`mtx-tus-urls`) — NEVER
27
+ * the recorder chunk journal DB.
28
+ *
29
+ * NOT LIVE-TESTED against production: the server side (CORS, metadata parity,
30
+ * completed-HEAD recovery) exists in aidream locally but is not deployed —
31
+ * see features/files/handler/FEATURE.md § Transport policy for the pending
32
+ * E2E note. The client is unit-tested with an injected HttpStack.
33
+ */
34
+ import * as tus from "tus-js-client";
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[]>;
81
+ export interface TusUploadDeps {
82
+ /** Injected HttpStack for unit tests (mock wire, no XHR). */
83
+ httpStack?: tus.HttpStack;
84
+ /** Injected file reader for unit tests (Node builds of tus-js-client
85
+ * cannot slice browser Files). */
86
+ fileReader?: ConstructorParameters<typeof tus.Upload>[1]["fileReader"];
87
+ /** Override the endpoint (tests). */
88
+ endpointOverride?: string;
89
+ /** Override URL storage (tests). */
90
+ urlStorage?: ReturnType<typeof createTusUrlStorage>;
91
+ }
92
+ /**
93
+ * Resumable upload of one file via TUS. Same result contract as
94
+ * `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.
99
+ */
100
+ export declare function tusUploadRaw(file: File, filePath: string, options: CloudUploadOptions, idempotencyKey: string, deps?: TusUploadDeps): Promise<CloudUploadResult>;