@ai-matrx/media 0.7.16 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -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 +30 -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/dist/files.cjs +1298 -0
- package/dist/files.cjs.map +1 -0
- package/dist/files.d.cts +1171 -0
- package/dist/files.d.ts +1171 -0
- package/dist/files.js +1275 -0
- package/dist/files.js.map +1 -0
- package/package.json +404 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/handler/upload.ts"],"sourcesContent":["/**\n * features/files/handler/upload.ts\n *\n * The single write path. Coerces a `FileSource` into a `File`, then runs\n * it through `cloudUpload` (which dispatches optimistic Redux updates,\n * calls Python's `/files/upload`, and optionally creates a share link in\n * the same round-trip). The returned `NormalizedFile` reflects the new\n * `cld_files` row and carries the share-link URL when requested.\n *\n * Org-scope routing is visibility-aware: public/shared uploads inherit\n * active organization / project / task ids by default; personal uploads\n * remain independent of ambient app scope unless explicitly opted in.\n */\n\nimport type { RootState, AppDispatch } from \"../host/store\";\nimport { getStoreSingleton } from \"../host/store\";\nimport {\n cloudUpload,\n isCloudUploadFailure,\n} from \"../upload/cloudUpload\";\nimport * as Files from \"../api/files\";\nimport {\n uploadAsset,\n uploadAssetWithProgress,\n} from \"../api/assets\";\nimport { pythonShareUrl } from \"./utils/python-base\";\nimport { apiFileRecordToCloudFile } from \"../redux/converters\";\nimport {\n selectOrganizationId,\n selectProjectId,\n selectTaskId,\n} from \"../host/store\";\nimport { FileUploadError, UploadCancelledError } from \"../../../files\";\nimport { fromCloudFile } from \"./input/normalize\";\nimport { setCached } from \"../hooks/blob-cache\";\nimport type {\n FileSource,\n NormalizedFile,\n UploadedNormalizedFile,\n UploadOpts,\n} from \"./types\";\n\nconst DEFAULT_FOLDER = \"Inbox\";\n\n/**\n * Personal files belong to the individual, independent of whichever\n * organization, project, or task happens to be active in the UI.\n */\nexport function shouldInheritActiveScope(\n visibility: NonNullable<UploadOpts[\"visibility\"]>,\n override?: boolean,\n folderPath?: string,\n): boolean {\n if (override !== undefined) return override;\n\n // Shared/Private Assets are the user's long-lived library namespaces.\n // Their existing folder rows belong to the personal organization even\n // when individual files are public. Letting an unrelated active org leak\n // into these paths makes the backend correctly reject the owner+path\n // collision as a cross-organization write.\n const normalizedFolder = folderPath?.replace(/^\\/+|\\/+$/g, \"\") ?? \"\";\n if (\n normalizedFolder === \"Shared Assets\" ||\n normalizedFolder.startsWith(\"Shared Assets/\") ||\n normalizedFolder === \"Private Assets\" ||\n normalizedFolder.startsWith(\"Private Assets/\")\n ) {\n return false;\n }\n\n return visibility !== \"personal\";\n}\n\nexport async function uploadInternal(\n source: FileSource,\n opts: UploadOpts,\n): Promise<UploadedNormalizedFile> {\n const file = await sourceToFile(source, opts.fileName);\n if (!file) {\n throw new FileUploadError(\n \"Cannot upload: source did not produce raw bytes (use the input shape that already has a fileId instead)\",\n );\n }\n\n const store = getStoreSingleton();\n if (!store) {\n throw new FileUploadError(\"Cannot upload: redux store not yet initialized\");\n }\n const dispatch = store.dispatch as AppDispatch;\n\n const folderPath = opts.folderPath ?? defaultFolderForSource(source);\n const visibility = opts.visibility ?? (opts.preset ? \"public\" : \"personal\");\n const metadata = stampScope(\n opts.metadata ?? {},\n shouldInheritActiveScope(visibility, opts.inheritActiveScope, folderPath),\n opts.organizationId,\n );\n\n // Asset-pipeline branch — when `preset` is set the upload routes through\n // `POST /assets`, which renders preset variants server-side and returns\n // the canonical Asset envelope. The handler stitches the envelope onto\n // the returned NormalizedFile so consumers can read every variant URL\n // (og_url, thumbnail_url, etc.) without a second round-trip.\n if (opts.preset) {\n const params = {\n file,\n preset: opts.preset,\n folder: folderPath.replace(/^\\/+|\\/+$/g, \"\"),\n visibility,\n customVariants: opts.customVariants,\n shareWith: opts.shareWith,\n shareLevel: opts.shareLevel,\n metadata,\n };\n const { data: asset } = opts.onProgress\n ? await uploadAssetWithProgress(params, (event) =>\n opts.onProgress!(event.loaded, event.total),\n )\n : await uploadAsset(params);\n // The bytes that just crossed the upload boundary are already the\n // authoritative contents for this durable file ID. Seed the shared\n // authenticated blob cache before callers replace their local object URL\n // with that ID, so every thumbnail keeps its pixels through the handoff.\n setCached(asset.file_id, file, URL.createObjectURL(file), {\n mimeType: file.type,\n });\n // Hydrate the cloud-files row so the cloudFiles slice sees the new\n // master record and downstream readers (selectors, realtime) get it.\n const { data: full } = await Files.getFile(asset.file_id);\n const cloudFile = apiFileRecordToCloudFile(full);\n const normalized = preserveUploadedIdentity(\n fromCloudFile(cloudFile, source),\n asset.file_id,\n \"asset upload\",\n );\n return {\n ...normalized,\n asset,\n url: asset.primary_url ?? normalized.url,\n };\n }\n\n const result = await cloudUpload(\n file,\n {\n // filePath wins when set — used by version-replace callers (e.g. the\n // image editor \"Save\" path that overwrites the existing cloud file).\n ...(opts.filePath\n ? { filePath: opts.filePath.replace(/^\\/+/, \"\") }\n : { folderPath: folderPath.replace(/^\\/+|\\/+$/g, \"\") }),\n visibility,\n shareWith: opts.shareWith,\n shareLevel: opts.shareLevel,\n changeSummary: opts.changeSummary,\n metadata,\n onProgress: opts.onProgress\n ? (event) => opts.onProgress!(event.loaded, event.total)\n : undefined,\n signal: opts.signal,\n transport: opts.transport,\n createShareLink: opts.createShareLink,\n shareLinkPermissionLevel: opts.shareLinkPermissionLevel,\n shareLinkExpiresAt: opts.shareLinkExpiresAt,\n shareLinkMaxUses: opts.shareLinkMaxUses,\n },\n dispatch,\n );\n\n if (isCloudUploadFailure(result)) {\n // Declining the workspace question is an answer, not a failure — it must\n // not reach an error toast or the Error Inspector.\n if (result.errorCode === \"upload_cancelled\") {\n throw new UploadCancelledError(result.error);\n }\n throw new FileUploadError(result.error);\n }\n\n // Preserve the already-loaded bytes under their new durable identity.\n // setCached owns (and eventually revokes) this object URL.\n setCached(result.fileId, file, URL.createObjectURL(file), {\n mimeType: file.type,\n });\n\n // cloudUpload returns slim metadata. Fetch the full row so the\n // returned NormalizedFile reflects every column the cloudFiles slice\n // will see (visibility/owner/permissions/checksum/...).\n const { data: full } = await Files.getFile(result.fileId);\n const cloudFile = apiFileRecordToCloudFile(full);\n // The upload response is the authoritative creation acknowledgement. A\n // follow-up hydration may enrich metadata, but it must never erase or replace\n // the id that the server just committed (an observed production response\n // adapter did exactly that, leaving chat with only a temporary/share URL).\n const normalized = preserveUploadedIdentity(\n fromCloudFile(cloudFile, source),\n result.fileId,\n );\n\n // Stitch on the share-link fields — cloudUpload created them in the\n // same round-trip as the upload. URL precedence is deliberate: a PUBLIC\n // file's `normalized.url` is the permanent CDN URL (set by fromCloudFile)\n // and must win over `directUrl` — otherwise we hand out a share-token URL\n // as if it were a permanent CDN URL. Private/shared files take the\n // share-token URLs: a caller that asked for a share link wants a URL any\n // recipient can open, and the durable authenticated URL that\n // `fromCloudFile` now seeds on `normalized.url` requires OUR auth.\n if (result.shareToken) {\n const appShareUrl = opts.appOrigin\n ? `${opts.appOrigin.replace(/\\/$/, \"\")}/s/${result.shareToken}`\n : undefined;\n const publicUrl =\n cloudFile.visibility === \"public\" ? normalized.url : undefined;\n const url =\n publicUrl ||\n result.directUrl ||\n appShareUrl ||\n result.shareUrl ||\n pythonShareUrl(result.shareToken);\n return {\n ...normalized,\n shareToken: result.shareToken,\n url,\n };\n }\n\n return normalized;\n}\n\n/**\n * Merge metadata from a post-upload hydration without letting that secondary\n * read erase or replace the identity acknowledged by the creation response.\n * This is shared by ordinary and asset uploads because both are the same\n * two-step contract: create → hydrate.\n */\nexport function preserveUploadedIdentity(\n hydrated: NormalizedFile,\n authoritativeFileId: string,\n label = \"upload\",\n): UploadedNormalizedFile {\n if (hydrated.fileId !== authoritativeFileId) {\n console.error(\n `[file-handler] hydrated ${label} lost or changed its durable identity ` +\n `(upload=${authoritativeFileId}, hydrated=${hydrated.fileId ?? \"<missing>\"}); ` +\n `preserving the upload acknowledgement.`,\n );\n }\n const uploaded = {\n ...hydrated,\n fileId: authoritativeFileId,\n };\n assertUploadedIdentity(uploaded);\n return uploaded;\n}\n\nfunction assertUploadedIdentity(\n file: NormalizedFile,\n): asserts file is UploadedNormalizedFile {\n if (!file.fileId) {\n throw new FileUploadError(\n \"Upload completed without a durable file identity; refusing to attach a temporary URL\",\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Source → File coercion. Anything that already carries server identity\n// (cloud_file, file_id, share_link, ...) is rejected — those are not\n// \"uploads\", they're already persisted. The handler's resolve() path is\n// the right tool for those.\n// ---------------------------------------------------------------------------\n\nasync function sourceToFile(\n source: FileSource,\n overrideName?: string,\n): Promise<File | null> {\n switch (source.kind) {\n case \"file\":\n return overrideName ? renameFile(source.file, overrideName) : source.file;\n\n case \"blob\": {\n const name =\n overrideName ?? source.fileName ?? guessFilename(source.blob.type);\n return new File([source.blob], name, {\n type: source.mime ?? source.blob.type ?? \"application/octet-stream\",\n });\n }\n\n case \"buffer\": {\n const blob = bufferToBlob(source.buffer, source.mime);\n const name =\n overrideName ?? source.fileName ?? guessFilename(source.mime);\n return new File([blob], name, { type: source.mime });\n }\n\n case \"stream\": {\n const reader = source.stream.getReader();\n const parts: BlobPart[] = [];\n while (true) {\n const { value, done } = await reader.read();\n if (done) break;\n if (value) {\n const copy = new Uint8Array(value.byteLength);\n copy.set(value);\n parts.push(copy.buffer as ArrayBuffer);\n }\n }\n const blob = new Blob(parts, { type: source.mime });\n const name =\n overrideName ?? source.fileName ?? guessFilename(source.mime);\n return new File([blob], name, { type: source.mime });\n }\n\n case \"data_uri\":\n case \"base64\": {\n const dataUri =\n source.kind === \"data_uri\"\n ? source.dataUri\n : `data:${source.mime};base64,${source.base64}`;\n const blob = await dataUriToBlob(dataUri);\n const fallbackName =\n (\"fileName\" in source && source.fileName) || guessFilename(blob.type);\n const name = overrideName ?? fallbackName;\n return new File([blob], name, { type: blob.type });\n }\n\n case \"external_url\": {\n const res = await fetch(source.url);\n if (!res.ok) {\n throw new FileUploadError(\n `Failed to fetch external URL for upload (${res.status})`,\n );\n }\n const blob = await res.blob();\n const name =\n overrideName ?? filenameFromUrl(source.url) ?? guessFilename(blob.type);\n return new File([blob], name, { type: source.mime ?? blob.type });\n }\n\n case \"youtube\":\n throw new FileUploadError(\n \"YouTube URLs cannot be uploaded — pass them as a source directly\",\n );\n\n default:\n return null;\n }\n}\n\nfunction bufferToBlob(\n buffer: ArrayBuffer | Uint8Array | SharedArrayBuffer,\n mime: string,\n): Blob {\n if (buffer instanceof Uint8Array) {\n const copy = new Uint8Array(buffer.byteLength);\n copy.set(buffer);\n return new Blob([copy.buffer], { type: mime });\n }\n if (buffer instanceof ArrayBuffer) return new Blob([buffer], { type: mime });\n const view = new Uint8Array(buffer);\n const copy = new Uint8Array(view.byteLength);\n copy.set(view);\n return new Blob([copy.buffer], { type: mime });\n}\n\nfunction dataUriToBlob(dataUri: string): Promise<Blob> {\n return fetch(dataUri).then((r) => r.blob());\n}\n\nfunction renameFile(file: File, name: string): File {\n return new File([file], name, {\n type: file.type,\n lastModified: file.lastModified,\n });\n}\n\nfunction filenameFromUrl(url: string): string | undefined {\n try {\n const path = new URL(url).pathname;\n const last = path.split(\"/\").pop();\n return last && last.length > 0 ? last : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction guessFilename(mime: string): string {\n if (!mime) return \"upload.bin\";\n const ext = mime.split(\"/\")[1]?.split(\";\")[0] ?? \"bin\";\n return `upload-${Date.now()}.${ext}`;\n}\n\nexport function stampScope(\n metadata: Record<string, unknown>,\n inherit: boolean,\n organizationId?: string,\n activeScope?: {\n organizationId?: string | null;\n projectId?: string | null;\n taskId?: string | null;\n },\n): Record<string, unknown> {\n if (!inherit && !organizationId) return metadata;\n let resolvedActiveScope = activeScope;\n if (inherit && !resolvedActiveScope) {\n const store = getStoreSingleton();\n if (store) {\n const state = store.getState() as RootState;\n resolvedActiveScope = {\n organizationId: selectOrganizationId(state),\n projectId: selectProjectId(state),\n taskId: selectTaskId(state),\n };\n }\n }\n const activeOrganizationId = resolvedActiveScope?.organizationId;\n const projectId = resolvedActiveScope?.projectId;\n const taskId = resolvedActiveScope?.taskId;\n if (!organizationId && !activeOrganizationId && !projectId && !taskId) {\n return metadata;\n }\n const existing =\n (metadata.scope as Record<string, unknown> | undefined) ?? {};\n return {\n ...metadata,\n scope: {\n ...existing,\n ...(activeOrganizationId\n ? { organization_id: activeOrganizationId }\n : {}),\n ...(projectId ? { project_id: projectId } : {}),\n ...(taskId ? { task_id: taskId } : {}),\n // An explicit owner is a record-bound write. It must never be replaced\n // by whatever organization happens to be active in the shell.\n ...(organizationId ? { organization_id: organizationId } : {}),\n },\n };\n}\n\nfunction defaultFolderForSource(source: FileSource): string {\n switch (source.kind) {\n case \"file\":\n case \"blob\":\n return DEFAULT_FOLDER;\n case \"data_uri\":\n case \"base64\":\n return `${DEFAULT_FOLDER}/Pasted`;\n case \"external_url\":\n return `${DEFAULT_FOLDER}/Imported`;\n default:\n return DEFAULT_FOLDER;\n }\n}\n"],"mappings":"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","names":["full","cloudFile","normalized","copy"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/handler/utils/classify.ts
|
|
3
|
+
*
|
|
4
|
+
* Wrap the existing FILE_TYPES registry to produce a `FileMeta` from any
|
|
5
|
+
* (filename, mime) tuple. This is the only place the handler talks to
|
|
6
|
+
* the classifier — keeps imports tidy and mocks easy.
|
|
7
|
+
*/
|
|
8
|
+
import type { FileMeta } from "../types.js";
|
|
9
|
+
export declare function classify(opts: {
|
|
10
|
+
fileName?: string;
|
|
11
|
+
mime?: string;
|
|
12
|
+
sizeBytes?: number;
|
|
13
|
+
checksum?: string;
|
|
14
|
+
}): FileMeta;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getFilePreviewProfile } from "../../utils/file-types.js";
|
|
2
|
+
function classify(opts) {
|
|
3
|
+
const fallbackName = opts.fileName ?? guessNameFromMime(opts.mime);
|
|
4
|
+
const profile = getFilePreviewProfile(
|
|
5
|
+
fallbackName,
|
|
6
|
+
opts.mime ?? null,
|
|
7
|
+
opts.sizeBytes ?? null
|
|
8
|
+
);
|
|
9
|
+
return {
|
|
10
|
+
fileName: opts.fileName,
|
|
11
|
+
mime: profile.mime,
|
|
12
|
+
sizeBytes: opts.sizeBytes,
|
|
13
|
+
checksum: opts.checksum,
|
|
14
|
+
category: profile.details.category,
|
|
15
|
+
previewKind: profile.previewKind,
|
|
16
|
+
thumbnailStrategy: profile.thumbnailStrategy
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function guessNameFromMime(mime) {
|
|
20
|
+
if (!mime) return "file";
|
|
21
|
+
const ext = mime.split("/")[1]?.split(";")[0] ?? "bin";
|
|
22
|
+
return `file.${ext}`;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
classify
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=classify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/files/engine/handler/utils/classify.ts"],"sourcesContent":["/**\n * features/files/handler/utils/classify.ts\n *\n * Wrap the existing FILE_TYPES registry to produce a `FileMeta` from any\n * (filename, mime) tuple. This is the only place the handler talks to\n * the classifier — keeps imports tidy and mocks easy.\n */\n\nimport { getFilePreviewProfile } from \"../../utils/file-types\";\nimport type { FileMeta } from \"../types\";\n\nexport function classify(opts: {\n fileName?: string;\n mime?: string;\n sizeBytes?: number;\n checksum?: string;\n}): FileMeta {\n const fallbackName = opts.fileName ?? guessNameFromMime(opts.mime);\n const profile = getFilePreviewProfile(\n fallbackName,\n opts.mime ?? null,\n opts.sizeBytes ?? null,\n );\n return {\n fileName: opts.fileName,\n mime: profile.mime,\n sizeBytes: opts.sizeBytes,\n checksum: opts.checksum,\n category: profile.details.category,\n previewKind: profile.previewKind,\n thumbnailStrategy: profile.thumbnailStrategy,\n };\n}\n\nfunction guessNameFromMime(mime?: string): string {\n if (!mime) return \"file\";\n const ext = mime.split(\"/\")[1]?.split(\";\")[0] ?? \"bin\";\n return `file.${ext}`;\n}\n"],"mappings":"AAQA,SAAS,6BAA6B;AAG/B,SAAS,SAAS,MAKZ;AACX,QAAM,eAAe,KAAK,YAAY,kBAAkB,KAAK,IAAI;AACjE,QAAM,UAAU;AAAA,IACd;AAAA,IACA,KAAK,QAAQ;AAAA,IACb,KAAK,aAAa;AAAA,EACpB;AACA,SAAO;AAAA,IACL,UAAU,KAAK;AAAA,IACf,MAAM,QAAQ;AAAA,IACd,WAAW,KAAK;AAAA,IAChB,UAAU,KAAK;AAAA,IACf,UAAU,QAAQ,QAAQ;AAAA,IAC1B,aAAa,QAAQ;AAAA,IACrB,mBAAmB,QAAQ;AAAA,EAC7B;AACF;AAEA,SAAS,kBAAkB,MAAuB;AAChD,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG,MAAM,GAAG,EAAE,CAAC,KAAK;AACjD,SAAO,QAAQ,GAAG;AACpB;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/handler/utils/prefer-locator.ts
|
|
3
|
+
*
|
|
4
|
+
* "Pick the best identifier" helpers used by every output adapter. The
|
|
5
|
+
* priority order is FIXED by the Python backend's MediaRef resolution
|
|
6
|
+
* rules: file_id > url > base64.
|
|
7
|
+
*/
|
|
8
|
+
import type { NormalizedFile } from "../types.js";
|
|
9
|
+
export declare function preferIdentityLocator(file: NormalizedFile): {
|
|
10
|
+
file_id?: string;
|
|
11
|
+
url?: string;
|
|
12
|
+
base64_data?: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Pick the best URL for a `<img src>` / `<video src>` / `<audio src>`
|
|
16
|
+
* consumer. Order:
|
|
17
|
+
* 1. Public CDN URL (permanent, CDN-cached) — already on `file.url`
|
|
18
|
+
* when the file is public AND server has CDN enabled.
|
|
19
|
+
* 2. Signed S3 URL we already minted — already on `file.url`.
|
|
20
|
+
* 3. Python share resolver — for share-link sources.
|
|
21
|
+
* 4. data: URI — last resort.
|
|
22
|
+
*
|
|
23
|
+
* No Next.js hops. The browser talks to Python (or CDN) directly.
|
|
24
|
+
*/
|
|
25
|
+
export declare function preferDisplayUrl(file: NormalizedFile): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Pick the best URL for a `fetch()` consumer. Signed S3 URLs are CORS-
|
|
28
|
+
* blocked, so when `transportSafeForFetch` is false we route through
|
|
29
|
+
* Python's authenticated download endpoint — same Python backend the
|
|
30
|
+
* cloud-files REST client already uses, no Next.js involvement.
|
|
31
|
+
*/
|
|
32
|
+
export declare function preferFetchableUrl(file: NormalizedFile): string | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pythonFileDownloadUrl,
|
|
3
|
+
pythonShareUrl
|
|
4
|
+
} from "./python-base.js";
|
|
5
|
+
function preferIdentityLocator(file) {
|
|
6
|
+
if (file.fileId) return { file_id: file.fileId };
|
|
7
|
+
if (file.url) return { url: file.url };
|
|
8
|
+
if (file.base64) return { base64_data: file.base64 };
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
function preferDisplayUrl(file) {
|
|
12
|
+
if (file.url) return file.url;
|
|
13
|
+
if (file.shareToken) return pythonShareUrl(file.shareToken);
|
|
14
|
+
if (file.base64 && file.meta.mime) {
|
|
15
|
+
return `data:${file.meta.mime};base64,${file.base64}`;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function preferFetchableUrl(file) {
|
|
20
|
+
if (file.capabilities.transportSafeForFetch && file.url) return file.url;
|
|
21
|
+
if (file.fileId) return pythonFileDownloadUrl(file.fileId);
|
|
22
|
+
if (file.shareToken) return pythonShareUrl(file.shareToken);
|
|
23
|
+
if (file.url) return file.url;
|
|
24
|
+
if (file.base64 && file.meta.mime) {
|
|
25
|
+
return `data:${file.meta.mime};base64,${file.base64}`;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
preferDisplayUrl,
|
|
31
|
+
preferFetchableUrl,
|
|
32
|
+
preferIdentityLocator
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=prefer-locator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/files/engine/handler/utils/prefer-locator.ts"],"sourcesContent":["/**\n * features/files/handler/utils/prefer-locator.ts\n *\n * \"Pick the best identifier\" helpers used by every output adapter. The\n * priority order is FIXED by the Python backend's MediaRef resolution\n * rules: file_id > url > base64.\n */\n\nimport type { NormalizedFile } from \"../types\";\nimport {\n pythonFileDownloadUrl,\n pythonShareUrl,\n} from \"./python-base\";\n\nexport function preferIdentityLocator(\n file: NormalizedFile,\n): { file_id?: string; url?: string; base64_data?: string } {\n if (file.fileId) return { file_id: file.fileId };\n if (file.url) return { url: file.url };\n if (file.base64) return { base64_data: file.base64 };\n return {};\n}\n\n/**\n * Pick the best URL for a `<img src>` / `<video src>` / `<audio src>`\n * consumer. Order:\n * 1. Public CDN URL (permanent, CDN-cached) — already on `file.url`\n * when the file is public AND server has CDN enabled.\n * 2. Signed S3 URL we already minted — already on `file.url`.\n * 3. Python share resolver — for share-link sources.\n * 4. data: URI — last resort.\n *\n * No Next.js hops. The browser talks to Python (or CDN) directly.\n */\nexport function preferDisplayUrl(file: NormalizedFile): string | null {\n if (file.url) return file.url;\n if (file.shareToken) return pythonShareUrl(file.shareToken);\n if (file.base64 && file.meta.mime) {\n return `data:${file.meta.mime};base64,${file.base64}`;\n }\n return null;\n}\n\n/**\n * Pick the best URL for a `fetch()` consumer. Signed S3 URLs are CORS-\n * blocked, so when `transportSafeForFetch` is false we route through\n * Python's authenticated download endpoint — same Python backend the\n * cloud-files REST client already uses, no Next.js involvement.\n */\nexport function preferFetchableUrl(file: NormalizedFile): string | null {\n if (file.capabilities.transportSafeForFetch && file.url) return file.url;\n if (file.fileId) return pythonFileDownloadUrl(file.fileId);\n if (file.shareToken) return pythonShareUrl(file.shareToken);\n if (file.url) return file.url;\n if (file.base64 && file.meta.mime) {\n return `data:${file.meta.mime};base64,${file.base64}`;\n }\n return null;\n}\n"],"mappings":"AASA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAEA,SAAS,sBACd,MAC0D;AAC1D,MAAI,KAAK,OAAQ,QAAO,EAAE,SAAS,KAAK,OAAO;AAC/C,MAAI,KAAK,IAAK,QAAO,EAAE,KAAK,KAAK,IAAI;AACrC,MAAI,KAAK,OAAQ,QAAO,EAAE,aAAa,KAAK,OAAO;AACnD,SAAO,CAAC;AACV;AAaO,SAAS,iBAAiB,MAAqC;AACpE,MAAI,KAAK,IAAK,QAAO,KAAK;AAC1B,MAAI,KAAK,WAAY,QAAO,eAAe,KAAK,UAAU;AAC1D,MAAI,KAAK,UAAU,KAAK,KAAK,MAAM;AACjC,WAAO,QAAQ,KAAK,KAAK,IAAI,WAAW,KAAK,MAAM;AAAA,EACrD;AACA,SAAO;AACT;AAQO,SAAS,mBAAmB,MAAqC;AACtE,MAAI,KAAK,aAAa,yBAAyB,KAAK,IAAK,QAAO,KAAK;AACrE,MAAI,KAAK,OAAQ,QAAO,sBAAsB,KAAK,MAAM;AACzD,MAAI,KAAK,WAAY,QAAO,eAAe,KAAK,UAAU;AAC1D,MAAI,KAAK,IAAK,QAAO,KAAK;AAC1B,MAAI,KAAK,UAAU,KAAK,KAAK,MAAM;AACjC,WAAO,QAAQ,KAAK,KAAK,IAAI,WAAW,KAAK,MAAM;AAAA,EACrD;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/handler/utils/python-base.ts
|
|
3
|
+
*
|
|
4
|
+
* Resolve Python backend URLs for direct browser → Python file traffic.
|
|
5
|
+
* The handler emits URLs that point at Python, never at Next.js — there
|
|
6
|
+
* is no server-side file handler in this app and no `/app/api/*` proxy
|
|
7
|
+
* involvement.
|
|
8
|
+
*
|
|
9
|
+
* Design note — "all variants in one object":
|
|
10
|
+
* Python exposes the same bytes via slightly-different URLs (inline vs
|
|
11
|
+
* attachment) and the FE has a share landing page. Rather than add a new
|
|
12
|
+
* function for every future tweak, the canonical builders here
|
|
13
|
+
* (`shareUrls`, `fileUrls`) return ALL the variants up front and let the
|
|
14
|
+
* caller pick. Single spelling for "everything you can do with a
|
|
15
|
+
* token / file id."
|
|
16
|
+
*
|
|
17
|
+
* Single-string helpers (`pythonShareUrl`, `pythonFileDownloadUrl`,
|
|
18
|
+
* `pythonFileInlineUrl`) are kept as thin wrappers so existing callers
|
|
19
|
+
* don't churn. Prefer the object form in new code.
|
|
20
|
+
*
|
|
21
|
+
* Tokens are CANONICAL share-link tokens (`platform.share_links`), minted via
|
|
22
|
+
* `createShareLink` in `@/utils/permissions/shareLinks` — the landing page is
|
|
23
|
+
* `/s/{token}` and the clean byte endpoint is Python's `/share/{token}`.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pythonBaseUrl(): string;
|
|
26
|
+
export interface ShareUrls {
|
|
27
|
+
/**
|
|
28
|
+
* Canonical clean public URL with `inline` Content-Disposition (the default).
|
|
29
|
+
* Works as `<img src>`, `<video src>`, `<audio src>`, PDF preview, or a
|
|
30
|
+
* normal browser link a recipient can paste anywhere.
|
|
31
|
+
*
|
|
32
|
+
* Image / video / audio / PDF render inline. Dangerous types
|
|
33
|
+
* (HTML, SVG, JS) are forced to `attachment` server-side.
|
|
34
|
+
*/
|
|
35
|
+
public: string;
|
|
36
|
+
/**
|
|
37
|
+
* Explicit download endpoint with `?inline=false` — forces
|
|
38
|
+
* `Content-Disposition: attachment` so the browser triggers a
|
|
39
|
+
* download dialog instead of rendering inline. Use for "Save As…"
|
|
40
|
+
* style affordances on images / PDFs.
|
|
41
|
+
*/
|
|
42
|
+
attachment: string;
|
|
43
|
+
/**
|
|
44
|
+
* The canonical share landing page on the FE — `/s/{token}` (metadata via
|
|
45
|
+
* the `resolve_share_token` RPC + preview + download button). Useful as a
|
|
46
|
+
* clickable link in chat / email / docs; NOT a media src.
|
|
47
|
+
*/
|
|
48
|
+
page: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build every URL variant for a share token in one call. Cheap —
|
|
52
|
+
* just string concatenation, no I/O.
|
|
53
|
+
*/
|
|
54
|
+
export declare function shareUrls(token: string, opts?: {
|
|
55
|
+
appOrigin?: string;
|
|
56
|
+
}): ShareUrls;
|
|
57
|
+
/**
|
|
58
|
+
* Byte URLs for ONE child file of a shared record — a shared chat's
|
|
59
|
+
* attachment — reached with the parent's share token (access ladder T-19b).
|
|
60
|
+
* The files service answers `/share/{token}/files/{fileId}` only when the file
|
|
61
|
+
* is a child of the record that token shares; nothing else is reachable.
|
|
62
|
+
*/
|
|
63
|
+
export declare function shareChildFileUrls(token: string, fileId: string, opts?: {
|
|
64
|
+
baseUrl?: string;
|
|
65
|
+
}): {
|
|
66
|
+
inline: string;
|
|
67
|
+
attachment: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Python's clean public byte-streaming share endpoint. Convenience wrapper
|
|
71
|
+
* around `shareUrls(token).public` for callers that just want the
|
|
72
|
+
* single canonical embeddable URL.
|
|
73
|
+
*/
|
|
74
|
+
export declare function pythonShareUrl(token: string): string;
|
|
75
|
+
export interface FileUrls {
|
|
76
|
+
/**
|
|
77
|
+
* Authenticated direct-download endpoint. CORS-safe for `fetch()`.
|
|
78
|
+
* Returns bytes with `Content-Disposition: attachment` so a `<a>`
|
|
79
|
+
* click triggers a download.
|
|
80
|
+
*/
|
|
81
|
+
download: string;
|
|
82
|
+
/**
|
|
83
|
+
* Authenticated inline endpoint — same bytes, `Content-Disposition:
|
|
84
|
+
* inline` so the URL works as `<img src>`, `<video src>`, etc.
|
|
85
|
+
*/
|
|
86
|
+
inline: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build every URL variant for a cld_files file id in one call. Cheap —
|
|
90
|
+
* just string concatenation. These are DURABLE URLs: they never expire and
|
|
91
|
+
* carry only the file id. Auth is either an Authorization header on
|
|
92
|
+
* `fetch()` (the python-client attaches it) or the `mx_files_session`
|
|
93
|
+
* cookie for plain `<img>`/`<video>` bindings (see `../session.ts`).
|
|
94
|
+
* `?inline=1` matches the exact spelling the backend emits on
|
|
95
|
+
* `FileRecord.url`, keeping cache keys identical either way.
|
|
96
|
+
*/
|
|
97
|
+
export declare function fileUrls(fileId: string): FileUrls;
|
|
98
|
+
/**
|
|
99
|
+
* True for a durable `/files/{id}/download` URL (either host, either
|
|
100
|
+
* spelling) — bytes that answer 401 unless the request carries the person's
|
|
101
|
+
* Authorization header or the `mx_files_session` cookie. A consumer that
|
|
102
|
+
* loads bytes with its own fetch (PDF.js) must never send one bare.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isAuthenticatedFileBytesUrl(url: string | null | undefined): boolean;
|
|
105
|
+
/** Single-URL convenience wrapper around `fileUrls(fileId).download`. */
|
|
106
|
+
export declare function pythonFileDownloadUrl(fileId: string): string;
|
|
107
|
+
/** Single-URL convenience wrapper around `fileUrls(fileId).inline`. */
|
|
108
|
+
export declare function pythonFileInlineUrl(fileId: string): string;
|
|
109
|
+
/**
|
|
110
|
+
* Extract the share token from any URL we recognize. Returns `null` for
|
|
111
|
+
* URLs that aren't ours (opaque external and third-party URLs).
|
|
112
|
+
*/
|
|
113
|
+
export declare function tokenFromShareUrl(url: string): string | null;
|
|
114
|
+
/**
|
|
115
|
+
* "Best-effort viewer URL" for any image / file URL we might have stored
|
|
116
|
+
* in the database. The intent is what the user clicks when they want to
|
|
117
|
+
* SEE the file (not download it):
|
|
118
|
+
*
|
|
119
|
+
* - Our bare `/share/{token}` (or legacy `/share/{token}/download`) URLs → the
|
|
120
|
+
* canonical FE landing page at `${origin}/s/{token}` (metadata + preview
|
|
121
|
+
* + download button). This is what an admin wants when they click a
|
|
122
|
+
* screenshot in the feedback dialog.
|
|
123
|
+
* - Anything else (external URLs) →
|
|
124
|
+
* returned unchanged. Browsers render image bytes inline regardless,
|
|
125
|
+
* and we have no viewer page for them anyway.
|
|
126
|
+
*/
|
|
127
|
+
export declare function imageViewUrl(url: string, opts?: {
|
|
128
|
+
appOrigin?: string;
|
|
129
|
+
}): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { fileUrls as packageFileUrls, shareUrl as packageShareUrl } from "@ai-matrx/data/files";
|
|
2
|
+
import { resolveFilesBaseUrl } from "../../host/python-client.js";
|
|
3
|
+
import { shareLinkUrl } from "../../host/share-links.js";
|
|
4
|
+
function pythonBaseUrl() {
|
|
5
|
+
return resolveFilesBaseUrl();
|
|
6
|
+
}
|
|
7
|
+
function shareUrls(token, opts) {
|
|
8
|
+
const t = encodeURIComponent(token);
|
|
9
|
+
const base = packageShareUrl(pythonBaseUrl(), token);
|
|
10
|
+
return {
|
|
11
|
+
public: base,
|
|
12
|
+
attachment: `${base}/download?inline=false`,
|
|
13
|
+
page: opts?.appOrigin ? `${opts.appOrigin.replace(/\/$/, "")}/s/${t}` : shareLinkUrl(t)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function shareChildFileUrls(token, fileId, opts) {
|
|
17
|
+
const backend = (opts?.baseUrl ?? pythonBaseUrl()).replace(/\/$/, "");
|
|
18
|
+
const base = `${backend}/share/${encodeURIComponent(token)}/files/${encodeURIComponent(fileId)}`;
|
|
19
|
+
return { inline: base, attachment: `${base}?inline=false` };
|
|
20
|
+
}
|
|
21
|
+
function pythonShareUrl(token) {
|
|
22
|
+
return shareUrls(token).public;
|
|
23
|
+
}
|
|
24
|
+
function fileUrls(fileId) {
|
|
25
|
+
return packageFileUrls(pythonBaseUrl(), fileId);
|
|
26
|
+
}
|
|
27
|
+
const AUTHENTICATED_FILE_BYTES_RE = /\/files\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/download(?:[?#]|$)/i;
|
|
28
|
+
function isAuthenticatedFileBytesUrl(url) {
|
|
29
|
+
return !!url && AUTHENTICATED_FILE_BYTES_RE.test(url);
|
|
30
|
+
}
|
|
31
|
+
function pythonFileDownloadUrl(fileId) {
|
|
32
|
+
return fileUrls(fileId).download;
|
|
33
|
+
}
|
|
34
|
+
function pythonFileInlineUrl(fileId) {
|
|
35
|
+
return fileUrls(fileId).inline;
|
|
36
|
+
}
|
|
37
|
+
const SHARE_TOKEN_RE = /\/share\/([0-9a-f-]{8,})(?:\/download)?(?:[/?#]|$)/i;
|
|
38
|
+
function tokenFromShareUrl(url) {
|
|
39
|
+
if (!url) return null;
|
|
40
|
+
const match = url.match(SHARE_TOKEN_RE);
|
|
41
|
+
return match ? match[1] : null;
|
|
42
|
+
}
|
|
43
|
+
function imageViewUrl(url, opts) {
|
|
44
|
+
const token = tokenFromShareUrl(url);
|
|
45
|
+
if (!token) return url;
|
|
46
|
+
return shareUrls(token, opts).page;
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
fileUrls,
|
|
50
|
+
imageViewUrl,
|
|
51
|
+
isAuthenticatedFileBytesUrl,
|
|
52
|
+
pythonBaseUrl,
|
|
53
|
+
pythonFileDownloadUrl,
|
|
54
|
+
pythonFileInlineUrl,
|
|
55
|
+
pythonShareUrl,
|
|
56
|
+
shareChildFileUrls,
|
|
57
|
+
shareUrls,
|
|
58
|
+
tokenFromShareUrl
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=python-base.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../src/files/engine/handler/utils/python-base.ts"],"sourcesContent":["/**\n * features/files/handler/utils/python-base.ts\n *\n * Resolve Python backend URLs for direct browser → Python file traffic.\n * The handler emits URLs that point at Python, never at Next.js — there\n * is no server-side file handler in this app and no `/app/api/*` proxy\n * involvement.\n *\n * Design note — \"all variants in one object\":\n * Python exposes the same bytes via slightly-different URLs (inline vs\n * attachment) and the FE has a share landing page. Rather than add a new\n * function for every future tweak, the canonical builders here\n * (`shareUrls`, `fileUrls`) return ALL the variants up front and let the\n * caller pick. Single spelling for \"everything you can do with a\n * token / file id.\"\n *\n * Single-string helpers (`pythonShareUrl`, `pythonFileDownloadUrl`,\n * `pythonFileInlineUrl`) are kept as thin wrappers so existing callers\n * don't churn. Prefer the object form in new code.\n *\n * Tokens are CANONICAL share-link tokens (`platform.share_links`), minted via\n * `createShareLink` in `@/utils/permissions/shareLinks` — the landing page is\n * `/s/{token}` and the clean byte endpoint is Python's `/share/{token}`.\n */\n\nimport { fileUrls as packageFileUrls, shareUrl as packageShareUrl } from \"@ai-matrx/data/files\";\nimport { resolveFilesBaseUrl } from \"../../host/python-client\";\nimport { shareLinkUrl } from \"../../host/share-links\";\n\nexport function pythonBaseUrl(): string {\n return resolveFilesBaseUrl();\n}\n\n// ---------------------------------------------------------------------------\n// Share-link URLs (token-based, public, no auth)\n// ---------------------------------------------------------------------------\n\nexport interface ShareUrls {\n /**\n * Canonical clean public URL with `inline` Content-Disposition (the default).\n * Works as `<img src>`, `<video src>`, `<audio src>`, PDF preview, or a\n * normal browser link a recipient can paste anywhere.\n *\n * Image / video / audio / PDF render inline. Dangerous types\n * (HTML, SVG, JS) are forced to `attachment` server-side.\n */\n public: string;\n /**\n * Explicit download endpoint with `?inline=false` — forces\n * `Content-Disposition: attachment` so the browser triggers a\n * download dialog instead of rendering inline. Use for \"Save As…\"\n * style affordances on images / PDFs.\n */\n attachment: string;\n /**\n * The canonical share landing page on the FE — `/s/{token}` (metadata via\n * the `resolve_share_token` RPC + preview + download button). Useful as a\n * clickable link in chat / email / docs; NOT a media src.\n */\n page: string;\n}\n\n/**\n * Build every URL variant for a share token in one call. Cheap —\n * just string concatenation, no I/O.\n */\nexport function shareUrls(\n token: string,\n opts?: { appOrigin?: string },\n): ShareUrls {\n const t = encodeURIComponent(token);\n // The URL grammar is @ai-matrx/data/files'; the host only binds the base.\n const base = packageShareUrl(pythonBaseUrl(), token);\n return {\n public: base,\n attachment: `${base}/download?inline=false`,\n page: opts?.appOrigin\n ? `${opts.appOrigin.replace(/\\/$/, \"\")}/s/${t}`\n : shareLinkUrl(t),\n };\n}\n\n/**\n * Byte URLs for ONE child file of a shared record — a shared chat's\n * attachment — reached with the parent's share token (access ladder T-19b).\n * The files service answers `/share/{token}/files/{fileId}` only when the file\n * is a child of the record that token shares; nothing else is reachable.\n */\nexport function shareChildFileUrls(\n token: string,\n fileId: string,\n opts?: { baseUrl?: string },\n): { inline: string; attachment: string } {\n const backend = (opts?.baseUrl ?? pythonBaseUrl()).replace(/\\/$/, \"\");\n const base = `${backend}/share/${encodeURIComponent(token)}/files/${encodeURIComponent(fileId)}`;\n return { inline: base, attachment: `${base}?inline=false` };\n}\n\n/**\n * Python's clean public byte-streaming share endpoint. Convenience wrapper\n * around `shareUrls(token).public` for callers that just want the\n * single canonical embeddable URL.\n */\nexport function pythonShareUrl(token: string): string {\n return shareUrls(token).public;\n}\n\n// ---------------------------------------------------------------------------\n// Authenticated file URLs (file-id, owner / shared-permission required)\n// ---------------------------------------------------------------------------\n\nexport interface FileUrls {\n /**\n * Authenticated direct-download endpoint. CORS-safe for `fetch()`.\n * Returns bytes with `Content-Disposition: attachment` so a `<a>`\n * click triggers a download.\n */\n download: string;\n /**\n * Authenticated inline endpoint — same bytes, `Content-Disposition:\n * inline` so the URL works as `<img src>`, `<video src>`, etc.\n */\n inline: string;\n}\n\n/**\n * Build every URL variant for a cld_files file id in one call. Cheap —\n * just string concatenation. These are DURABLE URLs: they never expire and\n * carry only the file id. Auth is either an Authorization header on\n * `fetch()` (the python-client attaches it) or the `mx_files_session`\n * cookie for plain `<img>`/`<video>` bindings (see `../session.ts`).\n * `?inline=1` matches the exact spelling the backend emits on\n * `FileRecord.url`, keeping cache keys identical either way.\n */\nexport function fileUrls(fileId: string): FileUrls {\n // The URL grammar is @ai-matrx/data/files'; the host only binds the base.\n return packageFileUrls(pythonBaseUrl(), fileId);\n}\n\nconst AUTHENTICATED_FILE_BYTES_RE =\n /\\/files\\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\/download(?:[?#]|$)/i;\n\n/**\n * True for a durable `/files/{id}/download` URL (either host, either\n * spelling) — bytes that answer 401 unless the request carries the person's\n * Authorization header or the `mx_files_session` cookie. A consumer that\n * loads bytes with its own fetch (PDF.js) must never send one bare.\n */\nexport function isAuthenticatedFileBytesUrl(url: string | null | undefined): boolean {\n return !!url && AUTHENTICATED_FILE_BYTES_RE.test(url);\n}\n\n/** Single-URL convenience wrapper around `fileUrls(fileId).download`. */\nexport function pythonFileDownloadUrl(fileId: string): string {\n return fileUrls(fileId).download;\n}\n\n/** Single-URL convenience wrapper around `fileUrls(fileId).inline`. */\nexport function pythonFileInlineUrl(fileId: string): string {\n return fileUrls(fileId).inline;\n}\n\n// ---------------------------------------------------------------------------\n// URL classification + viewer-page derivation\n// ---------------------------------------------------------------------------\n\n/**\n * Match anything that ends in `/share/{token}` or `/share/{token}/download`\n * (with optional query string / hash) where `{token}` is a hex-ish string of\n * at least 8 chars (covers both UUIDs and shorter custom tokens). Used to\n * recognize URLs that we ourselves emitted via `shareUrls()` so we can\n * round-trip them back into a viewer URL.\n */\nconst SHARE_TOKEN_RE = /\\/share\\/([0-9a-f-]{8,})(?:\\/download)?(?:[/?#]|$)/i;\n\n/**\n * Extract the share token from any URL we recognize. Returns `null` for\n * URLs that aren't ours (opaque external and third-party URLs).\n */\nexport function tokenFromShareUrl(url: string): string | null {\n if (!url) return null;\n const match = url.match(SHARE_TOKEN_RE);\n return match ? match[1] : null;\n}\n\n/**\n * \"Best-effort viewer URL\" for any image / file URL we might have stored\n * in the database. The intent is what the user clicks when they want to\n * SEE the file (not download it):\n *\n * - Our bare `/share/{token}` (or legacy `/share/{token}/download`) URLs → the\n * canonical FE landing page at `${origin}/s/{token}` (metadata + preview\n * + download button). This is what an admin wants when they click a\n * screenshot in the feedback dialog.\n * - Anything else (external URLs) →\n * returned unchanged. Browsers render image bytes inline regardless,\n * and we have no viewer page for them anyway.\n */\nexport function imageViewUrl(\n url: string,\n opts?: { appOrigin?: string },\n): string {\n const token = tokenFromShareUrl(url);\n if (!token) return url;\n return shareUrls(token, opts).page;\n}\n"],"mappings":"AAyBA,SAAS,YAAY,iBAAiB,YAAY,uBAAuB;AACzE,SAAS,2BAA2B;AACpC,SAAS,oBAAoB;AAEtB,SAAS,gBAAwB;AACtC,SAAO,oBAAoB;AAC7B;AAmCO,SAAS,UACd,OACA,MACW;AACX,QAAM,IAAI,mBAAmB,KAAK;AAElC,QAAM,OAAO,gBAAgB,cAAc,GAAG,KAAK;AACnD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,YAAY,GAAG,IAAI;AAAA,IACnB,MAAM,MAAM,YACR,GAAG,KAAK,UAAU,QAAQ,OAAO,EAAE,CAAC,MAAM,CAAC,KAC3C,aAAa,CAAC;AAAA,EACpB;AACF;AAQO,SAAS,mBACd,OACA,QACA,MACwC;AACxC,QAAM,WAAW,MAAM,WAAW,cAAc,GAAG,QAAQ,OAAO,EAAE;AACpE,QAAM,OAAO,GAAG,OAAO,UAAU,mBAAmB,KAAK,CAAC,UAAU,mBAAmB,MAAM,CAAC;AAC9F,SAAO,EAAE,QAAQ,MAAM,YAAY,GAAG,IAAI,gBAAgB;AAC5D;AAOO,SAAS,eAAe,OAAuB;AACpD,SAAO,UAAU,KAAK,EAAE;AAC1B;AA6BO,SAAS,SAAS,QAA0B;AAEjD,SAAO,gBAAgB,cAAc,GAAG,MAAM;AAChD;AAEA,MAAM,8BACJ;AAQK,SAAS,4BAA4B,KAAyC;AACnF,SAAO,CAAC,CAAC,OAAO,4BAA4B,KAAK,GAAG;AACtD;AAGO,SAAS,sBAAsB,QAAwB;AAC5D,SAAO,SAAS,MAAM,EAAE;AAC1B;AAGO,SAAS,oBAAoB,QAAwB;AAC1D,SAAO,SAAS,MAAM,EAAE;AAC1B;AAaA,MAAM,iBAAiB;AAMhB,SAAS,kBAAkB,KAA4B;AAC5D,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,QAAQ,IAAI,MAAM,cAAc;AACtC,SAAO,QAAQ,MAAM,CAAC,IAAI;AAC5B;AAeO,SAAS,aACd,KACA,MACQ;AACR,QAAM,QAAQ,kBAAkB,GAAG;AACnC,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,UAAU,OAAO,IAAI,EAAE;AAChC;","names":[]}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/hooks/blob-cache.ts
|
|
3
|
+
*
|
|
4
|
+
* Layer 1 (in-memory LRU) of the 3-tier byte cache. Survives React mount /
|
|
5
|
+
* unmount cycles within a session. Without it, closing and reopening a
|
|
6
|
+
* 10MB PDF re-fetches the whole blob over the wire — useFileBlob keeps
|
|
7
|
+
* its blob in component state, so unmount = `URL.revokeObjectURL` +
|
|
8
|
+
* lost bytes.
|
|
9
|
+
*
|
|
10
|
+
* 3-tier read path (see features/files/cache/):
|
|
11
|
+
* 1. In-memory LRU (this module) — synchronous, session
|
|
12
|
+
* 2. IndexedDB (features/files/cache/idb-store) — persistent, async
|
|
13
|
+
* 3. Network — Python /files/{id}/download
|
|
14
|
+
*
|
|
15
|
+
* Writes populate both tiers when the MIME policy permits IDB persistence
|
|
16
|
+
* (PDFs, ≤50 MB video, ≤100 MB audio, images, etc. — see cache/policy.ts).
|
|
17
|
+
*
|
|
18
|
+
* Design:
|
|
19
|
+
* - Single `Map<fileId, CacheEntry>`. Map iteration order = insertion
|
|
20
|
+
* order, so re-inserting on a cache hit gives us LRU for free.
|
|
21
|
+
* - Capped by total bytes (default 250 MB). When inserts push past
|
|
22
|
+
* the cap, oldest entries are evicted and their object URLs revoked.
|
|
23
|
+
* - The cache OWNS the object URL. `useFileBlob` reads `entry.url`
|
|
24
|
+
* directly and does NOT revoke on unmount — only the cache itself
|
|
25
|
+
* revokes, on eviction or explicit invalidation.
|
|
26
|
+
*
|
|
27
|
+
* Invalidation hooks:
|
|
28
|
+
* - `invalidate(fileId)` — single file. Used by upload-version /
|
|
29
|
+
* restore-version / delete thunks + realtime middleware on
|
|
30
|
+
* cross-device version inserts. Drops from BOTH tiers.
|
|
31
|
+
* - `invalidateAll()` — for sign-out / identity swap. Drops in-memory
|
|
32
|
+
* immediately; pass a userId to also wipe IDB for that user.
|
|
33
|
+
*
|
|
34
|
+
* Why not Redux: Blobs are not serialisable; storing them in the slice
|
|
35
|
+
* would trigger RTK serializableCheck warnings on every dispatch and
|
|
36
|
+
* the full blob would be passed through every middleware. A module-
|
|
37
|
+
* level Map is the right primitive for this.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Stamp every subsequent put/invalidate with the current user. Called
|
|
41
|
+
* from the sign-in flow (after Supabase session resolves). Without this
|
|
42
|
+
* IDB entries are written under an empty userId — readable but not
|
|
43
|
+
* scoped, which would leak across user swaps. Memory LRU is unaffected.
|
|
44
|
+
*/
|
|
45
|
+
export declare function setBlobCacheIdentity(userId: string | null): void;
|
|
46
|
+
interface CacheEntry {
|
|
47
|
+
blob: Blob;
|
|
48
|
+
/** `blob:`-scheme URL — owned by the cache; revoked on eviction. */
|
|
49
|
+
url: string;
|
|
50
|
+
/** `blob.size` cached so eviction math doesn't need to re-read. */
|
|
51
|
+
bytes: number;
|
|
52
|
+
/**
|
|
53
|
+
* The file version these bytes belong to; null when it was not known at
|
|
54
|
+
* fetch time (stamped by the first read that knows it). A read for a
|
|
55
|
+
* different version is a miss — the bytes are refetched only when the
|
|
56
|
+
* version changes.
|
|
57
|
+
*/
|
|
58
|
+
version: number | null;
|
|
59
|
+
/** Last-access timestamp (ms). Updated on cache hits. */
|
|
60
|
+
lastAccessed: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Read a cached entry. Touches the LRU position so re-accesses keep
|
|
64
|
+
* the entry alive. Returns `null` on miss — and when `version` is given and
|
|
65
|
+
* the entry holds a different version's bytes.
|
|
66
|
+
*/
|
|
67
|
+
export declare function getCached(fileId: string, version?: number | null): CacheEntry | null;
|
|
68
|
+
/**
|
|
69
|
+
* Hydrate the in-memory tier from IndexedDB for a single fileId. Returns
|
|
70
|
+
* the hydrated entry on hit, `null` on miss or when IDB is unavailable.
|
|
71
|
+
*
|
|
72
|
+
* Use this in `useFileBlob`'s effect when the in-memory LRU misses but
|
|
73
|
+
* we want to avoid a network round-trip if the bytes persisted from a
|
|
74
|
+
* previous session.
|
|
75
|
+
*/
|
|
76
|
+
export declare function hydrateFromIdb(fileId: string, version?: number | null): Promise<CacheEntry | null>;
|
|
77
|
+
/**
|
|
78
|
+
* Insert (or overwrite) a cached entry. The cache takes ownership of
|
|
79
|
+
* `blob` + `url` — callers should NOT revoke the URL themselves.
|
|
80
|
+
*
|
|
81
|
+
* Writes the entry into the in-memory LRU AND (when the MIME policy
|
|
82
|
+
* permits — see `features/files/cache/policy.ts`) into IndexedDB so the
|
|
83
|
+
* bytes survive page reloads.
|
|
84
|
+
*
|
|
85
|
+
* If a previous entry for the same fileId exists, its URL is revoked
|
|
86
|
+
* and the entry is replaced. After insert, the cache evicts oldest
|
|
87
|
+
* entries until the total byte size is under the budget.
|
|
88
|
+
*/
|
|
89
|
+
export declare function setCached(fileId: string, blob: Blob, url: string, meta?: {
|
|
90
|
+
mimeType?: string;
|
|
91
|
+
version?: number | null;
|
|
92
|
+
checksum?: string | null;
|
|
93
|
+
}): void;
|
|
94
|
+
/**
|
|
95
|
+
* Drop a single file's cached entry from both tiers. Use this from any
|
|
96
|
+
* code path that makes the cached bytes stale: new-version upload,
|
|
97
|
+
* restore-version, hard-delete, realtime-broadcast cross-device version
|
|
98
|
+
* insert.
|
|
99
|
+
*/
|
|
100
|
+
export declare function invalidate(fileId: string): void;
|
|
101
|
+
/** The version the cached bytes belong to (null: unknown or not cached). No LRU touch. */
|
|
102
|
+
export declare function cachedVersion(fileId: string): number | null;
|
|
103
|
+
/**
|
|
104
|
+
* A save wrote `blob` as version `version` of this file: drop every older
|
|
105
|
+
* copy (all tiers) and cache the saved bytes as that version, so every
|
|
106
|
+
* viewer shows the new version without downloading what this tab just
|
|
107
|
+
* uploaded.
|
|
108
|
+
*/
|
|
109
|
+
export declare function replaceWithSavedBytes(fileId: string, blob: Blob, version: number): void;
|
|
110
|
+
/**
|
|
111
|
+
* Drop every in-memory entry. Use on sign-out / identity swap so the
|
|
112
|
+
* next user can never see the previous user's blob URLs lingering in
|
|
113
|
+
* memory. When `userId` is provided, also clears IDB for that user.
|
|
114
|
+
*/
|
|
115
|
+
export declare function invalidateAll(userId?: string): void;
|
|
116
|
+
/**
|
|
117
|
+
* Test/debug helper. Useful for diagnostic pages that want to render
|
|
118
|
+
* "blob cache: 3 files, 47 MB / 250 MB".
|
|
119
|
+
*/
|
|
120
|
+
export declare function getCacheStats(): {
|
|
121
|
+
entryCount: number;
|
|
122
|
+
totalBytes: number;
|
|
123
|
+
budgetBytes: number;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Dev/test only — change the size budget at runtime. The default is
|
|
127
|
+
* 250 MB (DEFAULT_BUDGET_BYTES).
|
|
128
|
+
*/
|
|
129
|
+
export declare function setBudgetBytes(bytes: number): void;
|
|
130
|
+
export {};
|