@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.
- package/CHANGELOG.md +21 -0
- package/dist/files/engine/api/assets.d.ts +223 -0
- package/dist/files/engine/api/assets.js +124 -0
- package/dist/files/engine/api/assets.js.map +1 -0
- package/dist/files/engine/api/direct.d.ts +43 -0
- package/dist/files/engine/api/direct.js +73 -0
- package/dist/files/engine/api/direct.js.map +1 -0
- package/dist/files/engine/api/fileOrganization.d.ts +34 -0
- package/dist/files/engine/api/fileOrganization.js +38 -0
- package/dist/files/engine/api/fileOrganization.js.map +1 -0
- package/dist/files/engine/api/files.d.ts +249 -0
- package/dist/files/engine/api/files.js +231 -0
- package/dist/files/engine/api/files.js.map +1 -0
- package/dist/files/engine/api/folders.d.ts +63 -0
- package/dist/files/engine/api/folders.js +56 -0
- package/dist/files/engine/api/folders.js.map +1 -0
- package/dist/files/engine/api/office.d.ts +27 -0
- package/dist/files/engine/api/office.js +19 -0
- package/dist/files/engine/api/office.js.map +1 -0
- package/dist/files/engine/api/permissions.d.ts +37 -0
- package/dist/files/engine/api/permissions.js +109 -0
- package/dist/files/engine/api/permissions.js.map +1 -0
- package/dist/files/engine/api/versions.d.ts +26 -0
- package/dist/files/engine/api/versions.js +37 -0
- package/dist/files/engine/api/versions.js.map +1 -0
- package/dist/files/engine/cache/idb-store.d.ts +95 -0
- package/dist/files/engine/cache/idb-store.js +172 -0
- package/dist/files/engine/cache/idb-store.js.map +1 -0
- package/dist/files/engine/cache/policy.d.ts +14 -0
- package/dist/files/engine/cache/policy.js +40 -0
- package/dist/files/engine/cache/policy.js.map +1 -0
- package/dist/files/engine/cache/register-service-worker.d.ts +57 -0
- package/dist/files/engine/cache/register-service-worker.js +80 -0
- package/dist/files/engine/cache/register-service-worker.js.map +1 -0
- package/dist/files/engine/db-types.d.ts +48865 -0
- package/dist/files/engine/db-types.js +1 -0
- package/dist/files/engine/db-types.js.map +1 -0
- package/dist/files/engine/filesDb.d.ts +1913 -0
- package/dist/files/engine/filesDb.js +28 -0
- package/dist/files/engine/filesDb.js.map +1 -0
- package/dist/files/engine/handler/handler.d.ts +82 -0
- package/dist/files/engine/handler/handler.js +124 -0
- package/dist/files/engine/handler/handler.js.map +1 -0
- package/dist/files/engine/handler/hooks/useFile.d.ts +17 -0
- package/dist/files/engine/handler/hooks/useFile.js +75 -0
- package/dist/files/engine/handler/hooks/useFile.js.map +1 -0
- package/dist/files/engine/handler/hooks/useFileUpload.d.ts +76 -0
- package/dist/files/engine/handler/hooks/useFileUpload.js +67 -0
- package/dist/files/engine/handler/hooks/useFileUpload.js.map +1 -0
- package/dist/files/engine/handler/input/normalize.d.ts +14 -0
- package/dist/files/engine/handler/input/normalize.js +365 -0
- package/dist/files/engine/handler/input/normalize.js.map +1 -0
- package/dist/files/engine/handler/intelligence/access.d.ts +35 -0
- package/dist/files/engine/handler/intelligence/access.js +85 -0
- package/dist/files/engine/handler/intelligence/access.js.map +1 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.d.ts +21 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.js +67 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.js.map +1 -0
- package/dist/files/engine/handler/output/target.d.ts +19 -0
- package/dist/files/engine/handler/output/target.js +222 -0
- package/dist/files/engine/handler/output/target.js.map +1 -0
- package/dist/files/engine/handler/resolver.d.ts +34 -0
- package/dist/files/engine/handler/resolver.js +133 -0
- package/dist/files/engine/handler/resolver.js.map +1 -0
- package/dist/files/engine/handler/types.d.ts +367 -0
- package/dist/files/engine/handler/types.js +1 -0
- package/dist/files/engine/handler/types.js.map +1 -0
- package/dist/files/engine/handler/upload.d.ts +32 -0
- package/dist/files/engine/handler/upload.js +295 -0
- package/dist/files/engine/handler/upload.js.map +1 -0
- package/dist/files/engine/handler/utils/classify.d.ts +14 -0
- package/dist/files/engine/handler/utils/classify.js +27 -0
- package/dist/files/engine/handler/utils/classify.js.map +1 -0
- package/dist/files/engine/handler/utils/prefer-locator.d.ts +32 -0
- package/dist/files/engine/handler/utils/prefer-locator.js +34 -0
- package/dist/files/engine/handler/utils/prefer-locator.js.map +1 -0
- package/dist/files/engine/handler/utils/python-base.d.ts +129 -0
- package/dist/files/engine/handler/utils/python-base.js +60 -0
- package/dist/files/engine/handler/utils/python-base.js.map +1 -0
- package/dist/files/engine/hooks/blob-cache.d.ts +130 -0
- package/dist/files/engine/hooks/blob-cache.js +162 -0
- package/dist/files/engine/hooks/blob-cache.js.map +1 -0
- package/dist/files/engine/hooks/office-extraction-cache.d.ts +28 -0
- package/dist/files/engine/hooks/office-extraction-cache.js +67 -0
- package/dist/files/engine/hooks/office-extraction-cache.js.map +1 -0
- package/dist/files/engine/host/configure.d.ts +166 -0
- package/dist/files/engine/host/configure.js +31 -0
- package/dist/files/engine/host/configure.js.map +1 -0
- package/dist/files/engine/host/org.d.ts +8 -0
- package/dist/files/engine/host/org.js +16 -0
- package/dist/files/engine/host/org.js.map +1 -0
- package/dist/files/engine/host/python-client.d.ts +60 -0
- package/dist/files/engine/host/python-client.js +46 -0
- package/dist/files/engine/host/python-client.js.map +1 -0
- package/dist/files/engine/host/share-links.d.ts +20 -0
- package/dist/files/engine/host/share-links.js +32 -0
- package/dist/files/engine/host/share-links.js.map +1 -0
- package/dist/files/engine/host/store.d.ts +24 -0
- package/dist/files/engine/host/store.js +23 -0
- package/dist/files/engine/host/store.js.map +1 -0
- package/dist/files/engine/host/supabase.d.ts +3808 -0
- package/dist/files/engine/host/supabase.js +33 -0
- package/dist/files/engine/host/supabase.js.map +1 -0
- package/dist/files/engine/host/toast.d.ts +14 -0
- package/dist/files/engine/host/toast.js +20 -0
- package/dist/files/engine/host/toast.js.map +1 -0
- package/dist/files/engine/host/typed-client.d.ts +136 -0
- package/dist/files/engine/host/typed-client.js +66 -0
- package/dist/files/engine/host/typed-client.js.map +1 -0
- package/dist/files/engine/index.d.ts +22 -0
- package/dist/files/engine/index.js +49 -0
- package/dist/files/engine/index.js.map +1 -0
- package/dist/files/engine/media/our-file-sources.d.ts +39 -0
- package/dist/files/engine/media/our-file-sources.js +83 -0
- package/dist/files/engine/media/our-file-sources.js.map +1 -0
- package/dist/files/engine/media/signed-url.d.ts +1 -0
- package/dist/files/engine/media/signed-url.js +6 -0
- package/dist/files/engine/media/signed-url.js.map +1 -0
- package/dist/files/engine/redux/converters.d.ts +73 -0
- package/dist/files/engine/redux/converters.js +286 -0
- package/dist/files/engine/redux/converters.js.map +1 -0
- package/dist/files/engine/redux/file-hydration.d.ts +15 -0
- package/dist/files/engine/redux/file-hydration.js +46 -0
- package/dist/files/engine/redux/file-hydration.js.map +1 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.d.ts +5 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.js +28 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.js.map +1 -0
- package/dist/files/engine/redux/file-tree-timeout.d.ts +32 -0
- package/dist/files/engine/redux/file-tree-timeout.js +73 -0
- package/dist/files/engine/redux/file-tree-timeout.js.map +1 -0
- package/dist/files/engine/redux/mutation-toast-middleware.d.ts +27 -0
- package/dist/files/engine/redux/mutation-toast-middleware.js +103 -0
- package/dist/files/engine/redux/mutation-toast-middleware.js.map +1 -0
- package/dist/files/engine/redux/realtime-middleware.d.ts +61 -0
- package/dist/files/engine/redux/realtime-middleware.js +431 -0
- package/dist/files/engine/redux/realtime-middleware.js.map +1 -0
- package/dist/files/engine/redux/request-ledger.d.ts +68 -0
- package/dist/files/engine/redux/request-ledger.js +66 -0
- package/dist/files/engine/redux/request-ledger.js.map +1 -0
- package/dist/files/engine/redux/selectors.d.ts +3254 -0
- package/dist/files/engine/redux/selectors.js +428 -0
- package/dist/files/engine/redux/selectors.js.map +1 -0
- package/dist/files/engine/redux/slice.d.ts +157 -0
- package/dist/files/engine/redux/slice.js +836 -0
- package/dist/files/engine/redux/slice.js.map +1 -0
- package/dist/files/engine/redux/thunks.d.ts +449 -0
- package/dist/files/engine/redux/thunks.js +1684 -0
- package/dist/files/engine/redux/thunks.js.map +1 -0
- package/dist/files/engine/redux/tree-utils.d.ts +90 -0
- package/dist/files/engine/redux/tree-utils.js +200 -0
- package/dist/files/engine/redux/tree-utils.js.map +1 -0
- package/dist/files/engine/support/claimsUser.d.ts +77 -0
- package/dist/files/engine/support/claimsUser.js +48 -0
- package/dist/files/engine/support/claimsUser.js.map +1 -0
- package/dist/files/engine/support/datetime.d.ts +6 -0
- package/dist/files/engine/support/datetime.js +15 -0
- package/dist/files/engine/support/datetime.js.map +1 -0
- package/dist/files/engine/support/document-visibility.d.ts +14 -0
- package/dist/files/engine/support/document-visibility.js +28 -0
- package/dist/files/engine/support/document-visibility.js.map +1 -0
- package/dist/files/engine/support/logger.d.ts +28 -0
- package/dist/files/engine/support/logger.js +36 -0
- package/dist/files/engine/support/logger.js.map +1 -0
- package/dist/files/engine/types.d.ts +1081 -0
- package/dist/files/engine/types.js +30 -0
- package/dist/files/engine/types.js.map +1 -0
- package/dist/files/engine/upload/cloudUpload.d.ts +182 -0
- package/dist/files/engine/upload/cloudUpload.js +418 -0
- package/dist/files/engine/upload/cloudUpload.js.map +1 -0
- package/dist/files/engine/upload/tusUpload.d.ts +100 -0
- package/dist/files/engine/upload/tusUpload.js +258 -0
- package/dist/files/engine/upload/tusUpload.js.map +1 -0
- package/dist/files/engine/upload/uploadDedupGuard.d.ts +58 -0
- package/dist/files/engine/upload/uploadDedupGuard.js +41 -0
- package/dist/files/engine/upload/uploadDedupGuard.js.map +1 -0
- package/dist/files/engine/upload/uploadGuardOpeners.d.ts +70 -0
- package/dist/files/engine/upload/uploadGuardOpeners.js +54 -0
- package/dist/files/engine/upload/uploadGuardOpeners.js.map +1 -0
- package/dist/files/engine/utils/file-types.d.ts +243 -0
- package/dist/files/engine/utils/file-types.js +1722 -0
- package/dist/files/engine/utils/file-types.js.map +1 -0
- package/dist/files/engine/utils/user-visible.d.ts +133 -0
- package/dist/files/engine/utils/user-visible.js +115 -0
- package/dist/files/engine/utils/user-visible.js.map +1 -0
- package/package.json +392 -8
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/handler/output/target.ts
|
|
3
|
+
*
|
|
4
|
+
* Render a `NormalizedFile` for a specific consumer. Each branch is the
|
|
5
|
+
* ONLY place in the codebase that knows how to build that consumer's
|
|
6
|
+
* shape — the duplication clusters in conversation/agents/Knowledge/tasks all
|
|
7
|
+
* collapse here.
|
|
8
|
+
*/
|
|
9
|
+
import type { MediaRef } from "../../types.js";
|
|
10
|
+
import type { FileTarget, NormalizedFile, RenderedFor } from "../types.js";
|
|
11
|
+
export declare function toTarget<T extends FileTarget>(file: NormalizedFile, target: T): Promise<RenderedFor<T>>;
|
|
12
|
+
/**
|
|
13
|
+
* Build a `MediaRef` from a normalized file. Exported so the agent slice
|
|
14
|
+
* (and any other sync code path) can produce the SAME shape `as` produces,
|
|
15
|
+
* without going through the async `fileHandler.use(...).as(...)` chain.
|
|
16
|
+
* Anyone duplicating this logic is the doctrine violation that produced
|
|
17
|
+
* the worst bugs in the file system; consume this helper instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare function toMediaRef(file: NormalizedFile): MediaRef;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {
|
|
2
|
+
preferDisplayUrl,
|
|
3
|
+
preferFetchableUrl,
|
|
4
|
+
preferIdentityLocator
|
|
5
|
+
} from "../utils/prefer-locator.js";
|
|
6
|
+
import { pythonShareUrl } from "../utils/python-base.js";
|
|
7
|
+
import { downloadFile } from "../../api/files.js";
|
|
8
|
+
import { isSignedUrl } from "../../media/signed-url.js";
|
|
9
|
+
async function toTarget(file, target) {
|
|
10
|
+
switch (target.kind) {
|
|
11
|
+
case "media_block":
|
|
12
|
+
return toMediaBlock(file);
|
|
13
|
+
case "media_ref":
|
|
14
|
+
return toMediaRef(file);
|
|
15
|
+
case "html_src":
|
|
16
|
+
return toHtmlSrc(file);
|
|
17
|
+
case "fetchable_url":
|
|
18
|
+
return toFetchableUrl(file);
|
|
19
|
+
case "blob":
|
|
20
|
+
return await toBlob(file);
|
|
21
|
+
case "data_uri":
|
|
22
|
+
return await toDataUri(file);
|
|
23
|
+
case "form_data_part":
|
|
24
|
+
return await toFormDataPart(file, target.field);
|
|
25
|
+
case "anchor_download":
|
|
26
|
+
return toAnchorDownload(file, target.suggestedName);
|
|
27
|
+
case "og_image":
|
|
28
|
+
return toOgImage(file);
|
|
29
|
+
case "jsonb_content_part":
|
|
30
|
+
return toJsonbContentPart(file);
|
|
31
|
+
case "rag_ingest_source":
|
|
32
|
+
return toRagIngestSource(file);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function toMediaBlock(file) {
|
|
36
|
+
if (file.youtubeUrl) {
|
|
37
|
+
const block2 = {
|
|
38
|
+
type: "media",
|
|
39
|
+
kind: "youtube",
|
|
40
|
+
url: file.youtubeUrl
|
|
41
|
+
};
|
|
42
|
+
return block2;
|
|
43
|
+
}
|
|
44
|
+
const locator = requiredRequestLocator(file);
|
|
45
|
+
const mime = file.meta.mime;
|
|
46
|
+
const category = file.meta.category;
|
|
47
|
+
if (category === "IMAGE") {
|
|
48
|
+
const block2 = { type: "media", kind: "image", ...locator };
|
|
49
|
+
if (mime) block2.mime_type = mime;
|
|
50
|
+
return block2;
|
|
51
|
+
}
|
|
52
|
+
if (category === "AUDIO") {
|
|
53
|
+
const block2 = { type: "media", kind: "audio", ...locator };
|
|
54
|
+
if (mime) block2.mime_type = mime;
|
|
55
|
+
return block2;
|
|
56
|
+
}
|
|
57
|
+
if (category === "VIDEO") {
|
|
58
|
+
const block2 = { type: "media", kind: "video", ...locator };
|
|
59
|
+
if (mime) block2.mime_type = mime;
|
|
60
|
+
return block2;
|
|
61
|
+
}
|
|
62
|
+
const block = {
|
|
63
|
+
type: "media",
|
|
64
|
+
kind: "document",
|
|
65
|
+
...locator
|
|
66
|
+
};
|
|
67
|
+
if (mime) block.mime_type = mime;
|
|
68
|
+
return block;
|
|
69
|
+
}
|
|
70
|
+
function requiredRequestLocator(file) {
|
|
71
|
+
const locator = preferIdentityLocator(file);
|
|
72
|
+
if (locator.file_id) return { file_id: locator.file_id };
|
|
73
|
+
if (locator.url) return { url: locator.url };
|
|
74
|
+
if (locator.base64_data) return { base64_data: locator.base64_data };
|
|
75
|
+
throw new Error("file-handler: media block has no locator");
|
|
76
|
+
}
|
|
77
|
+
function toMediaRef(file) {
|
|
78
|
+
const locator = preferIdentityLocator(file);
|
|
79
|
+
const ref = {};
|
|
80
|
+
if (locator.file_id) ref.file_id = locator.file_id;
|
|
81
|
+
if (locator.url) ref.url = locator.url;
|
|
82
|
+
if (file.meta.mime) ref.mime_type = file.meta.mime;
|
|
83
|
+
return ref;
|
|
84
|
+
}
|
|
85
|
+
function toHtmlSrc(file) {
|
|
86
|
+
const url = preferDisplayUrl(file);
|
|
87
|
+
if (!url) {
|
|
88
|
+
throw new Error("file-handler: no displayable URL for this file");
|
|
89
|
+
}
|
|
90
|
+
return url;
|
|
91
|
+
}
|
|
92
|
+
function toFetchableUrl(file) {
|
|
93
|
+
const url = preferFetchableUrl(file);
|
|
94
|
+
if (!url) {
|
|
95
|
+
throw new Error("file-handler: no fetchable URL for this file");
|
|
96
|
+
}
|
|
97
|
+
return url;
|
|
98
|
+
}
|
|
99
|
+
async function toBlob(file) {
|
|
100
|
+
if (file.fileId) {
|
|
101
|
+
const { blob } = await downloadFile(file.fileId, { inline: true });
|
|
102
|
+
return blob;
|
|
103
|
+
}
|
|
104
|
+
const url = preferFetchableUrl(file);
|
|
105
|
+
if (!url) {
|
|
106
|
+
throw new Error("file-handler: cannot produce a Blob for this file");
|
|
107
|
+
}
|
|
108
|
+
const res = await fetch(url);
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
throw new Error(`file-handler: blob fetch failed (${res.status})`);
|
|
111
|
+
}
|
|
112
|
+
return res.blob();
|
|
113
|
+
}
|
|
114
|
+
async function toDataUri(file) {
|
|
115
|
+
if (file.base64 && file.meta.mime) {
|
|
116
|
+
return `data:${file.meta.mime};base64,${file.base64}`;
|
|
117
|
+
}
|
|
118
|
+
const blob = await toBlob(file);
|
|
119
|
+
return blobToDataUri(blob);
|
|
120
|
+
}
|
|
121
|
+
async function toFormDataPart(file, field) {
|
|
122
|
+
const blob = await toBlob(file);
|
|
123
|
+
return {
|
|
124
|
+
value: blob,
|
|
125
|
+
filename: file.meta.fileName ?? guessFilename(field, file.meta.mime)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function toAnchorDownload(file, suggested) {
|
|
129
|
+
const url = preferDisplayUrl(file) ?? preferFetchableUrl(file);
|
|
130
|
+
if (!url) throw new Error("file-handler: no URL to download");
|
|
131
|
+
return {
|
|
132
|
+
url,
|
|
133
|
+
filename: suggested ?? file.meta.fileName ?? "download"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function toOgImage(file) {
|
|
137
|
+
if (file.url && !isSignedUrl(file.url)) return file.url;
|
|
138
|
+
if (file.shareToken) return pythonShareUrl(file.shareToken);
|
|
139
|
+
throw new Error("file-handler: no permanent URL available for OG image");
|
|
140
|
+
}
|
|
141
|
+
function toJsonbContentPart(file) {
|
|
142
|
+
if (file.youtubeUrl) {
|
|
143
|
+
const part = {
|
|
144
|
+
type: "media",
|
|
145
|
+
kind: "youtube",
|
|
146
|
+
url: file.youtubeUrl
|
|
147
|
+
};
|
|
148
|
+
return part;
|
|
149
|
+
}
|
|
150
|
+
const locator = requiredPersistedLocator(file);
|
|
151
|
+
const base = {
|
|
152
|
+
type: "media",
|
|
153
|
+
// file_id is the PREFERRED locator (preferIdentityLocator returns ONLY
|
|
154
|
+
// file_id when present — never url alongside it) and the Python MediaRef
|
|
155
|
+
// resolver prefers it. Omitting it here meant a file-id-only part (e.g. a
|
|
156
|
+
// private FastFire audio clip) shipped {url:null} — the model received an
|
|
157
|
+
// empty media block and hallucinated from context. Carry it, like
|
|
158
|
+
// toMediaBlock/toMediaRef do, and per media-durability doctrine.
|
|
159
|
+
...locator,
|
|
160
|
+
mime_type: file.meta.mime ?? null
|
|
161
|
+
};
|
|
162
|
+
switch (file.meta.category) {
|
|
163
|
+
case "IMAGE": {
|
|
164
|
+
const part = { ...base, kind: "image" };
|
|
165
|
+
return part;
|
|
166
|
+
}
|
|
167
|
+
case "AUDIO": {
|
|
168
|
+
const part = { ...base, kind: "audio" };
|
|
169
|
+
return part;
|
|
170
|
+
}
|
|
171
|
+
case "VIDEO": {
|
|
172
|
+
const part = { ...base, kind: "video" };
|
|
173
|
+
return part;
|
|
174
|
+
}
|
|
175
|
+
default: {
|
|
176
|
+
const part = { ...base, kind: "document" };
|
|
177
|
+
return part;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function requiredPersistedLocator(file) {
|
|
182
|
+
if (file.fileId) return { file_id: file.fileId };
|
|
183
|
+
if (file.url) return { url: file.url };
|
|
184
|
+
if (file.base64) {
|
|
185
|
+
return {
|
|
186
|
+
url: `data:${file.meta.mime ?? "application/octet-stream"};base64,${file.base64}`
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
throw new Error("file-handler: persisted media has no locator");
|
|
190
|
+
}
|
|
191
|
+
function toRagIngestSource(file) {
|
|
192
|
+
if (file.fileId) return { source_kind: "cld_file", source_id: file.fileId };
|
|
193
|
+
if (file.url) return { source_kind: "external_url", url: file.url };
|
|
194
|
+
if (file.base64 && file.meta.mime) {
|
|
195
|
+
const bytes = base64ToBytes(file.base64);
|
|
196
|
+
return { source_kind: "inline", inline: { mime: file.meta.mime, bytes } };
|
|
197
|
+
}
|
|
198
|
+
throw new Error("file-handler: no Knowledge-ingestable form for this file");
|
|
199
|
+
}
|
|
200
|
+
function blobToDataUri(blob) {
|
|
201
|
+
return new Promise((resolve, reject) => {
|
|
202
|
+
const reader = new FileReader();
|
|
203
|
+
reader.onerror = () => reject(reader.error);
|
|
204
|
+
reader.onload = () => resolve(reader.result);
|
|
205
|
+
reader.readAsDataURL(blob);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function base64ToBytes(b64) {
|
|
209
|
+
const binary = atob(b64);
|
|
210
|
+
const bytes = new Uint8Array(binary.length);
|
|
211
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
212
|
+
return bytes;
|
|
213
|
+
}
|
|
214
|
+
function guessFilename(field, mime) {
|
|
215
|
+
const ext = mime?.split("/")[1]?.split(";")[0] ?? "bin";
|
|
216
|
+
return `${field}.${ext}`;
|
|
217
|
+
}
|
|
218
|
+
export {
|
|
219
|
+
toMediaRef,
|
|
220
|
+
toTarget
|
|
221
|
+
};
|
|
222
|
+
//# sourceMappingURL=target.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/files/engine/handler/output/target.ts"],"sourcesContent":["/**\n * features/files/handler/output/target.ts\n *\n * Render a `NormalizedFile` for a specific consumer. Each branch is the\n * ONLY place in the codebase that knows how to build that consumer's\n * shape — the duplication clusters in conversation/agents/Knowledge/tasks all\n * collapse here.\n */\n\nimport type {\n AudioBlock,\n DocumentBlock,\n ImageBlock,\n VideoBlock,\n YouTubeVideoBlock,\n} from \"@ai-matrx/agents/message-parts\";\nimport type { MediaRef } from \"../../types\";\nimport type {\n ImageMediaPart,\n AudioMediaPart,\n VideoMediaPart,\n DocumentMediaPart,\n YouTubeMediaPart,\n MessagePart,\n} from \"@ai-matrx/agents/generated/stream-events\";\nimport {\n preferDisplayUrl,\n preferFetchableUrl,\n preferIdentityLocator,\n} from \"../utils/prefer-locator\";\nimport { pythonShareUrl } from \"../utils/python-base\";\nimport { downloadFile } from \"../../api/files\";\nimport { isSignedUrl } from \"../../media/signed-url\";\nimport type {\n FileTarget,\n MediaBlock,\n NormalizedFile,\n RagIngestSource,\n RenderedFor,\n} from \"../types\";\n\n// `unknown` because the generic specialization happens at the callsite.\nexport async function toTarget<T extends FileTarget>(\n file: NormalizedFile,\n target: T,\n): Promise<RenderedFor<T>> {\n switch (target.kind) {\n case \"media_block\":\n return toMediaBlock(file) as RenderedFor<T>;\n case \"media_ref\":\n return toMediaRef(file) as RenderedFor<T>;\n case \"html_src\":\n return toHtmlSrc(file) as RenderedFor<T>;\n case \"fetchable_url\":\n return toFetchableUrl(file) as RenderedFor<T>;\n case \"blob\":\n return (await toBlob(file)) as RenderedFor<T>;\n case \"data_uri\":\n return (await toDataUri(file)) as RenderedFor<T>;\n case \"form_data_part\":\n return (await toFormDataPart(file, target.field)) as RenderedFor<T>;\n case \"anchor_download\":\n return toAnchorDownload(file, target.suggestedName) as RenderedFor<T>;\n case \"og_image\":\n return toOgImage(file) as RenderedFor<T>;\n case \"jsonb_content_part\":\n return toJsonbContentPart(file) as RenderedFor<T>;\n case \"rag_ingest_source\":\n return toRagIngestSource(file) as RenderedFor<T>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// AI media blocks (outbound)\n// ---------------------------------------------------------------------------\n\nfunction toMediaBlock(file: NormalizedFile): MediaBlock {\n if (file.youtubeUrl) {\n const block: YouTubeVideoBlock = {\n type: \"media\",\n kind: \"youtube\",\n url: file.youtubeUrl,\n };\n return block;\n }\n\n const locator = requiredRequestLocator(file);\n const mime = file.meta.mime;\n const category = file.meta.category;\n\n if (category === \"IMAGE\") {\n const block: ImageBlock = { type: \"media\", kind: \"image\", ...locator };\n if (mime) block.mime_type = mime;\n return block;\n }\n if (category === \"AUDIO\") {\n const block: AudioBlock = { type: \"media\", kind: \"audio\", ...locator };\n if (mime) block.mime_type = mime;\n return block;\n }\n if (category === \"VIDEO\") {\n const block: VideoBlock = { type: \"media\", kind: \"video\", ...locator };\n if (mime) block.mime_type = mime;\n return block;\n }\n const block: DocumentBlock = {\n type: \"media\",\n kind: \"document\",\n ...locator,\n };\n if (mime) block.mime_type = mime;\n return block;\n}\n\nfunction requiredRequestLocator(file: NormalizedFile) {\n const locator = preferIdentityLocator(file);\n if (locator.file_id) return { file_id: locator.file_id };\n if (locator.url) return { url: locator.url };\n if (locator.base64_data) return { base64_data: locator.base64_data };\n throw new Error(\"file-handler: media block has no locator\");\n}\n\n/**\n * Build a `MediaRef` from a normalized file. Exported so the agent slice\n * (and any other sync code path) can produce the SAME shape `as` produces,\n * without going through the async `fileHandler.use(...).as(...)` chain.\n * Anyone duplicating this logic is the doctrine violation that produced\n * the worst bugs in the file system; consume this helper instead.\n */\nexport function toMediaRef(file: NormalizedFile): MediaRef {\n const locator = preferIdentityLocator(file);\n const ref: MediaRef = {};\n if (locator.file_id) ref.file_id = locator.file_id;\n if (locator.url) ref.url = locator.url;\n if (file.meta.mime) ref.mime_type = file.meta.mime;\n return ref;\n}\n\n// ---------------------------------------------------------------------------\n// Browser-facing URLs\n// ---------------------------------------------------------------------------\n\nfunction toHtmlSrc(file: NormalizedFile): string {\n const url = preferDisplayUrl(file);\n if (!url) {\n throw new Error(\"file-handler: no displayable URL for this file\");\n }\n return url;\n}\n\nfunction toFetchableUrl(file: NormalizedFile): string {\n const url = preferFetchableUrl(file);\n if (!url) {\n throw new Error(\"file-handler: no fetchable URL for this file\");\n }\n return url;\n}\n\nasync function toBlob(file: NormalizedFile): Promise<Blob> {\n // AN OWNED FILE IS READ BY ID, THROUGH THE AUTHENTICATED CLIENT.\n //\n // `preferFetchableUrl` correctly resolves an owned file to Python's\n // `/files/{id}/download` (FEATURE.md \"Locator preference\" — S3 URLs are\n // CORS-blocked, so `transportSafeForFetch` is false for them). That endpoint\n // is AUTHENTICATED: a bare `fetch` of it sends no credentials and every owned\n // private file came back 401, breaking `blob`, `data_uri`, `form_data_part`\n // and `anchor_download` for exactly the files the handler exists to serve.\n // `downloadFile` is the same endpoint through the typed client that attaches\n // auth — so the id, which is all we ever need, is all we use.\n if (file.fileId) {\n const { blob } = await downloadFile(file.fileId, { inline: true });\n return blob;\n }\n // No id: a foreign/base64/share source. Those genuinely are plain fetches.\n const url = preferFetchableUrl(file);\n if (!url) {\n throw new Error(\"file-handler: cannot produce a Blob for this file\");\n }\n const res = await fetch(url);\n if (!res.ok) {\n throw new Error(`file-handler: blob fetch failed (${res.status})`);\n }\n return res.blob();\n}\n\nasync function toDataUri(file: NormalizedFile): Promise<string> {\n if (file.base64 && file.meta.mime) {\n return `data:${file.meta.mime};base64,${file.base64}`;\n }\n const blob = await toBlob(file);\n return blobToDataUri(blob);\n}\n\nasync function toFormDataPart(\n file: NormalizedFile,\n field: string,\n): Promise<{ value: Blob; filename: string }> {\n const blob = await toBlob(file);\n return {\n value: blob,\n filename: file.meta.fileName ?? guessFilename(field, file.meta.mime),\n };\n}\n\nfunction toAnchorDownload(\n file: NormalizedFile,\n suggested?: string,\n): { url: string; filename: string } {\n const url = preferDisplayUrl(file) ?? preferFetchableUrl(file);\n if (!url) throw new Error(\"file-handler: no URL to download\");\n return {\n url,\n filename: suggested ?? file.meta.fileName ?? \"download\",\n };\n}\n\nfunction toOgImage(file: NormalizedFile): string {\n // OG previews are scraped offline by social platforms — never use a\n // signed URL (it'll expire before the scrape) and never use a same-origin\n // proxy (auth-required). Prefer permanent CDN > share-link bytes endpoint.\n if (file.url && !isSignedUrl(file.url)) return file.url;\n if (file.shareToken) return pythonShareUrl(file.shareToken);\n throw new Error(\"file-handler: no permanent URL available for OG image\");\n}\n\n// ---------------------------------------------------------------------------\n// Persistence (cx_message.content[])\n// ---------------------------------------------------------------------------\n\nfunction toJsonbContentPart(file: NormalizedFile): MessagePart {\n if (file.youtubeUrl) {\n const part: YouTubeMediaPart = {\n type: \"media\",\n kind: \"youtube\",\n url: file.youtubeUrl,\n };\n return part;\n }\n\n const locator = requiredPersistedLocator(file);\n const base = {\n type: \"media\" as const,\n // file_id is the PREFERRED locator (preferIdentityLocator returns ONLY\n // file_id when present — never url alongside it) and the Python MediaRef\n // resolver prefers it. Omitting it here meant a file-id-only part (e.g. a\n // private FastFire audio clip) shipped {url:null} — the model received an\n // empty media block and hallucinated from context. Carry it, like\n // toMediaBlock/toMediaRef do, and per media-durability doctrine.\n ...locator,\n mime_type: file.meta.mime ?? null,\n };\n switch (file.meta.category) {\n case \"IMAGE\": {\n const part: ImageMediaPart = { ...base, kind: \"image\" };\n return part;\n }\n case \"AUDIO\": {\n const part: AudioMediaPart = { ...base, kind: \"audio\" };\n return part;\n }\n case \"VIDEO\": {\n const part: VideoMediaPart = { ...base, kind: \"video\" };\n return part;\n }\n default: {\n const part: DocumentMediaPart = { ...base, kind: \"document\" };\n return part;\n }\n }\n}\n\nfunction requiredPersistedLocator(file: NormalizedFile) {\n if (file.fileId) return { file_id: file.fileId };\n if (file.url) return { url: file.url };\n if (file.base64) {\n return {\n url: `data:${file.meta.mime ?? \"application/octet-stream\"};base64,${file.base64}`,\n };\n }\n throw new Error(\"file-handler: persisted media has no locator\");\n}\n\n// ---------------------------------------------------------------------------\n// Knowledge ingest\n// ---------------------------------------------------------------------------\n\nfunction toRagIngestSource(file: NormalizedFile): RagIngestSource {\n if (file.fileId) return { source_kind: \"cld_file\", source_id: file.fileId };\n if (file.url) return { source_kind: \"external_url\", url: file.url };\n if (file.base64 && file.meta.mime) {\n const bytes = base64ToBytes(file.base64);\n return { source_kind: \"inline\", inline: { mime: file.meta.mime, bytes } };\n }\n throw new Error(\"file-handler: no Knowledge-ingestable form for this file\");\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction blobToDataUri(blob: Blob): Promise<string> {\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onerror = () => reject(reader.error);\n reader.onload = () => resolve(reader.result as string);\n reader.readAsDataURL(blob);\n });\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n const binary = atob(b64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\nfunction guessFilename(field: string, mime?: string): string {\n const ext = mime?.split(\"/\")[1]?.split(\";\")[0] ?? \"bin\";\n return `${field}.${ext}`;\n}\n"],"mappings":"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","names":["block"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/handler/resolver.ts
|
|
3
|
+
*
|
|
4
|
+
* The intelligence layer. Takes a freshly-normalized `NormalizedFile`
|
|
5
|
+
* (output of `input/normalize.ts`) and:
|
|
6
|
+
*
|
|
7
|
+
* 1. Hydrates from Redux when a fileId is known
|
|
8
|
+
* 2. Fetches `/files/{id}` if missing from the slice
|
|
9
|
+
* 3. Decides origin + capabilities (owned/shared/public/external)
|
|
10
|
+
* 4. Binds the durable render URL (synchronous — durable URLs are a
|
|
11
|
+
* pure function of the file id and never expire)
|
|
12
|
+
* 5. Sniffs MIME from magic bytes when unknown
|
|
13
|
+
* 6. Translates backend failure modes into typed errors
|
|
14
|
+
*
|
|
15
|
+
* Pure(ish) — uses the imported store directly so callsites don't need
|
|
16
|
+
* to hand it in. Hooks read the store's RootState; thunks operate via
|
|
17
|
+
* dispatch. The resolver is a one-way function: given the same input,
|
|
18
|
+
* the same Redux state, and the same network result, it produces the
|
|
19
|
+
* same NormalizedFile.
|
|
20
|
+
*/
|
|
21
|
+
import type { NormalizedFile } from "./types.js";
|
|
22
|
+
interface ResolveOpts {
|
|
23
|
+
/** When true, the resolver binds the durable URL if owned and missing one. */
|
|
24
|
+
needsUrl?: boolean;
|
|
25
|
+
/** When true, the resolver will fetch bytes to sniff MIME if missing. */
|
|
26
|
+
sniffMime?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Take a `NormalizedFile` produced by `normalize()` and finish the job:
|
|
30
|
+
* hydrate, decide access, bind the durable URL. Idempotent — calling
|
|
31
|
+
* twice with the same input returns equivalent results.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolve(file: NormalizedFile, opts?: ResolveOpts): Promise<NormalizedFile>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { getStoreSingleton } from "../host/store.js";
|
|
2
|
+
import * as Files from "../api/files.js";
|
|
3
|
+
import {
|
|
4
|
+
apiFileRecordToCloudFile,
|
|
5
|
+
dbRowToCloudFile
|
|
6
|
+
} from "../redux/converters.js";
|
|
7
|
+
import { readFileRowById } from "../filesDb.js";
|
|
8
|
+
import { supabase } from "../host/supabase.js";
|
|
9
|
+
import {
|
|
10
|
+
selectFileById,
|
|
11
|
+
selectPermissionsForResource
|
|
12
|
+
} from "../redux/selectors.js";
|
|
13
|
+
import {
|
|
14
|
+
FileAccessDeniedError,
|
|
15
|
+
FileDeletedError,
|
|
16
|
+
FileNotFoundError
|
|
17
|
+
} from "../../../files.js";
|
|
18
|
+
import { decideForOwnedFile } from "./intelligence/access.js";
|
|
19
|
+
import { sniffMimeFromBlob } from "./intelligence/magic-bytes.js";
|
|
20
|
+
import { pythonFileInlineUrl } from "./utils/python-base.js";
|
|
21
|
+
import { fromCloudFile } from "./input/normalize.js";
|
|
22
|
+
import { classify } from "./utils/classify.js";
|
|
23
|
+
async function resolve(file, opts = {}) {
|
|
24
|
+
let result = file;
|
|
25
|
+
if (result.fileId) {
|
|
26
|
+
result = await hydrateFromFileId(result);
|
|
27
|
+
}
|
|
28
|
+
if (opts.needsUrl && result.fileId && !result.url) {
|
|
29
|
+
result = ensureDurableUrl(result);
|
|
30
|
+
}
|
|
31
|
+
if (opts.sniffMime && !result.meta.mime && !result.fileId) {
|
|
32
|
+
result = await sniffIfPossible(result);
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
async function hydrateFromFileId(file) {
|
|
37
|
+
if (!file.fileId) return file;
|
|
38
|
+
const store = getStoreSingleton();
|
|
39
|
+
if (!store) throw new Error("file-handler: redux store not yet initialized");
|
|
40
|
+
const state = store.getState();
|
|
41
|
+
const cached = selectFileById(state, file.fileId);
|
|
42
|
+
let cloudFile = cached ?? null;
|
|
43
|
+
if (!cloudFile?.filePath) {
|
|
44
|
+
try {
|
|
45
|
+
const { data } = await Files.getFile(file.fileId);
|
|
46
|
+
cloudFile = apiFileRecordToCloudFile(data);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const status = err?.status;
|
|
49
|
+
if (status === 404) {
|
|
50
|
+
throw new FileNotFoundError(void 0, { fileId: file.fileId });
|
|
51
|
+
}
|
|
52
|
+
if (status === 403) {
|
|
53
|
+
throw new FileAccessDeniedError(void 0, { fileId: file.fileId });
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!cloudFile.organizationId) {
|
|
59
|
+
const row = await readFileRowById(supabase, file.fileId);
|
|
60
|
+
if (!row) {
|
|
61
|
+
throw new FileNotFoundError(void 0, { fileId: file.fileId });
|
|
62
|
+
}
|
|
63
|
+
const canonical = dbRowToCloudFile(row);
|
|
64
|
+
cloudFile = {
|
|
65
|
+
...canonical,
|
|
66
|
+
publicUrl: cloudFile.publicUrl ?? canonical.publicUrl,
|
|
67
|
+
url: cloudFile.url ?? canonical.url,
|
|
68
|
+
cdnUrl: cloudFile.cdnUrl ?? canonical.cdnUrl,
|
|
69
|
+
downloadUrl: cloudFile.downloadUrl ?? canonical.downloadUrl,
|
|
70
|
+
thumbnailUrl: cloudFile.thumbnailUrl ?? canonical.thumbnailUrl
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (cloudFile.deletedAt) {
|
|
74
|
+
throw new FileDeletedError(void 0, { fileId: cloudFile.id });
|
|
75
|
+
}
|
|
76
|
+
const hydrated = fromCloudFile(cloudFile, file.__source);
|
|
77
|
+
const permissions = selectPermissionsForResource(state, cloudFile.id);
|
|
78
|
+
const decision = decideForOwnedFile(cloudFile, state, permissions);
|
|
79
|
+
return {
|
|
80
|
+
...hydrated,
|
|
81
|
+
origin: decision.origin,
|
|
82
|
+
capabilities: decision.capabilities,
|
|
83
|
+
url: file.url ?? hydrated.url,
|
|
84
|
+
base64: file.base64,
|
|
85
|
+
shareToken: file.shareToken,
|
|
86
|
+
lifecycle: {
|
|
87
|
+
...hydrated.lifecycle,
|
|
88
|
+
expiresAt: file.lifecycle.expiresAt,
|
|
89
|
+
lastVerifiedAt: Date.now()
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function ensureDurableUrl(file) {
|
|
94
|
+
if (!file.fileId || file.url) return file;
|
|
95
|
+
return {
|
|
96
|
+
...file,
|
|
97
|
+
url: pythonFileInlineUrl(file.fileId),
|
|
98
|
+
lifecycle: {
|
|
99
|
+
...file.lifecycle,
|
|
100
|
+
lastVerifiedAt: Date.now()
|
|
101
|
+
},
|
|
102
|
+
capabilities: {
|
|
103
|
+
...file.capabilities,
|
|
104
|
+
transportSafeForFetch: true
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function sniffIfPossible(file) {
|
|
109
|
+
if (!file.url || file.url.startsWith("data:")) return file;
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(file.url, {
|
|
112
|
+
method: "GET",
|
|
113
|
+
headers: { Range: "bytes=0-31" }
|
|
114
|
+
});
|
|
115
|
+
if (!res.ok) return file;
|
|
116
|
+
const blob = await res.blob();
|
|
117
|
+
const sniffed = await sniffMimeFromBlob(blob);
|
|
118
|
+
if (!sniffed) return file;
|
|
119
|
+
return {
|
|
120
|
+
...file,
|
|
121
|
+
meta: classify({
|
|
122
|
+
...file.meta,
|
|
123
|
+
mime: sniffed
|
|
124
|
+
})
|
|
125
|
+
};
|
|
126
|
+
} catch {
|
|
127
|
+
return file;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
resolve
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/handler/resolver.ts"],"sourcesContent":["/**\n * features/files/handler/resolver.ts\n *\n * The intelligence layer. Takes a freshly-normalized `NormalizedFile`\n * (output of `input/normalize.ts`) and:\n *\n * 1. Hydrates from Redux when a fileId is known\n * 2. Fetches `/files/{id}` if missing from the slice\n * 3. Decides origin + capabilities (owned/shared/public/external)\n * 4. Binds the durable render URL (synchronous — durable URLs are a\n * pure function of the file id and never expire)\n * 5. Sniffs MIME from magic bytes when unknown\n * 6. Translates backend failure modes into typed errors\n *\n * Pure(ish) — uses the imported store directly so callsites don't need\n * to hand it in. Hooks read the store's RootState; thunks operate via\n * dispatch. The resolver is a one-way function: given the same input,\n * the same Redux state, and the same network result, it produces the\n * same NormalizedFile.\n */\n\nimport type { RootState } from \"../host/store\";\nimport { getStoreSingleton } from \"../host/store\";\nimport * as Files from \"../api/files\";\nimport {\n apiFileRecordToCloudFile,\n dbRowToCloudFile,\n} from \"../redux/converters\";\nimport { readFileRowById } from \"../filesDb\";\nimport { supabase } from \"../host/supabase\";\nimport {\n selectFileById,\n selectPermissionsForResource,\n} from \"../redux/selectors\";\nimport {\n FileAccessDeniedError,\n FileDeletedError,\n FileNotFoundError,\n} from \"../../../files\";\nimport { decideForOwnedFile } from \"./intelligence/access\";\nimport { sniffMimeFromBlob } from \"./intelligence/magic-bytes\";\nimport { pythonFileInlineUrl } from \"./utils/python-base\";\nimport { fromCloudFile } from \"./input/normalize\";\nimport { classify } from \"./utils/classify\";\nimport type { NormalizedFile } from \"./types\";\n\ninterface ResolveOpts {\n /** When true, the resolver binds the durable URL if owned and missing one. */\n needsUrl?: boolean;\n /** When true, the resolver will fetch bytes to sniff MIME if missing. */\n sniffMime?: boolean;\n}\n\n/**\n * Take a `NormalizedFile` produced by `normalize()` and finish the job:\n * hydrate, decide access, bind the durable URL. Idempotent — calling\n * twice with the same input returns equivalent results.\n */\nexport async function resolve(\n file: NormalizedFile,\n opts: ResolveOpts = {},\n): Promise<NormalizedFile> {\n let result = file;\n\n if (result.fileId) {\n result = await hydrateFromFileId(result);\n }\n\n if (opts.needsUrl && result.fileId && !result.url) {\n result = ensureDurableUrl(result);\n }\n\n if (opts.sniffMime && !result.meta.mime && !result.fileId) {\n result = await sniffIfPossible(result);\n }\n\n return result;\n}\n\n// ---------------------------------------------------------------------------\n// Hydration\n// ---------------------------------------------------------------------------\n\nasync function hydrateFromFileId(\n file: NormalizedFile,\n): Promise<NormalizedFile> {\n if (!file.fileId) return file;\n\n const store = getStoreSingleton();\n if (!store) throw new Error(\"file-handler: redux store not yet initialized\");\n const state = store.getState() as RootState;\n const cached = selectFileById(state, file.fileId);\n\n // `selectFileById` already returns a fully-typed `CloudFileRecord`\n // (`extends CloudFile`) straight from our own reducer-typed state — no\n // re-validation needed. `undefined` (not cached) normalizes to `null`.\n let cloudFile: import(\"../types\").CloudFile | null =\n cached ?? null;\n\n // Newly-created files can reach a consumer before the file-tree hydration\n // fills the placeholder's durable path. An id-bearing placeholder is not a\n // complete cache hit: versioned writes require the canonical path so the\n // backend updates the existing row instead of creating a sibling.\n if (!cloudFile?.filePath) {\n try {\n const { data } = await Files.getFile(file.fileId);\n cloudFile = apiFileRecordToCloudFile(data);\n } catch (err) {\n const status = (err as { status?: number })?.status;\n if (status === 404) {\n throw new FileNotFoundError(undefined, { fileId: file.fileId });\n }\n if (status === 403) {\n throw new FileAccessDeniedError(undefined, { fileId: file.fileId });\n }\n throw err;\n }\n }\n\n // REST FileRecord is optimized for rendering and older deployed servers do\n // not expose `organization_id`. A version write cannot infer ownership from\n // ambient UI context: the existing row is authoritative. Hydrate the one\n // canonical DB row whenever organization identity is absent, retaining any\n // server-computed URL fields already present on the REST/cache envelope.\n if (!cloudFile.organizationId) {\n const row = await readFileRowById(supabase, file.fileId);\n if (!row) {\n throw new FileNotFoundError(undefined, { fileId: file.fileId });\n }\n const canonical = dbRowToCloudFile(row);\n cloudFile = {\n ...canonical,\n publicUrl: cloudFile.publicUrl ?? canonical.publicUrl,\n url: cloudFile.url ?? canonical.url,\n cdnUrl: cloudFile.cdnUrl ?? canonical.cdnUrl,\n downloadUrl: cloudFile.downloadUrl ?? canonical.downloadUrl,\n thumbnailUrl: cloudFile.thumbnailUrl ?? canonical.thumbnailUrl,\n };\n }\n\n if (cloudFile.deletedAt) {\n throw new FileDeletedError(undefined, { fileId: cloudFile.id });\n }\n\n const hydrated = fromCloudFile(cloudFile, file.__source);\n const permissions = selectPermissionsForResource(state, cloudFile.id);\n const decision = decideForOwnedFile(cloudFile, state, permissions);\n\n return {\n ...hydrated,\n origin: decision.origin,\n capabilities: decision.capabilities,\n url: file.url ?? hydrated.url,\n base64: file.base64,\n shareToken: file.shareToken,\n lifecycle: {\n ...hydrated.lifecycle,\n expiresAt: file.lifecycle.expiresAt,\n lastVerifiedAt: Date.now(),\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Durable URL binding (synchronous — a pure function of the file id)\n// ---------------------------------------------------------------------------\n\nfunction ensureDurableUrl(file: NormalizedFile): NormalizedFile {\n if (!file.fileId || file.url) return file;\n // The durable inline URL never expires; a plain `<img>` binding\n // authenticates via the `mx_files_session` cookie, and `fetch()`es via\n // the python-client attach Authorization headers — so it is fetch-safe.\n return {\n ...file,\n url: pythonFileInlineUrl(file.fileId),\n lifecycle: {\n ...file.lifecycle,\n lastVerifiedAt: Date.now(),\n },\n capabilities: {\n ...file.capabilities,\n transportSafeForFetch: true,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// MIME sniffing\n// ---------------------------------------------------------------------------\n\nasync function sniffIfPossible(file: NormalizedFile): Promise<NormalizedFile> {\n if (!file.url || file.url.startsWith(\"data:\")) return file;\n try {\n const res = await fetch(file.url, {\n method: \"GET\",\n headers: { Range: \"bytes=0-31\" },\n });\n if (!res.ok) return file;\n const blob = await res.blob();\n const sniffed = await sniffMimeFromBlob(blob);\n if (!sniffed) return file;\n return {\n ...file,\n meta: classify({\n ...file.meta,\n mime: sniffed,\n }),\n };\n } catch {\n return file;\n }\n}\n"],"mappings":"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","names":[]}
|