@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,30 @@
|
|
|
1
|
+
const DEFAULT_VISIBLE_COLUMNS = {
|
|
2
|
+
name: true,
|
|
3
|
+
type: true,
|
|
4
|
+
extension: false,
|
|
5
|
+
mime: false,
|
|
6
|
+
path: false,
|
|
7
|
+
owner: true,
|
|
8
|
+
size: true,
|
|
9
|
+
version: false,
|
|
10
|
+
updated_at: true,
|
|
11
|
+
created_at: false,
|
|
12
|
+
access: true,
|
|
13
|
+
rag_status: false,
|
|
14
|
+
// ON by default on purpose: context drives Knowledge/NER and everything
|
|
15
|
+
// downstream — its presence (or amber absence) must be impossible to miss.
|
|
16
|
+
// One bulk query per visible page populates it (no per-row probes).
|
|
17
|
+
context: true
|
|
18
|
+
};
|
|
19
|
+
function isCloudTreeFileRow(row) {
|
|
20
|
+
return row.kind === "file";
|
|
21
|
+
}
|
|
22
|
+
function isCloudTreeFolderRow(row) {
|
|
23
|
+
return row.kind === "folder";
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
DEFAULT_VISIBLE_COLUMNS,
|
|
27
|
+
isCloudTreeFileRow,
|
|
28
|
+
isCloudTreeFolderRow
|
|
29
|
+
};
|
|
30
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/files/engine/types.ts"],"sourcesContent":["/**\n * features/files/types.ts\n *\n * Single source of truth for all cloud-files types. Import from\n * `@/features/files/types` — never duplicate types in feature subfolders.\n *\n * LAYERS\n * ------\n * 1. Domain types — camelCase, what components and Redux work with.\n * 2. DB row types — snake_case, derived from Supabase-generated `Database`.\n * 3. API types — from Python OpenAPI-generated `components[\"schemas\"]`.\n * 4. Runtime records — domain types + dirty/loading/error metadata for Redux.\n * 5. Tree & UI types — normalized structure + UI state shapes.\n * 6. Upload types — upload orchestrator state.\n * 7. Error types — re-export of BackendApiError, plus files-specific codes.\n *\n * Do NOT duplicate or re-declare any type from here in feature subfolders.\n */\n\nimport type { components } from \"@ai-matrx/agents/generated/api-types\";\nimport type { Database } from \"./db-types\";\nimport type { FieldFlags } from \"@ai-matrx/agents/field-flags\";\nimport type { readFileRowById } from \"./filesDb\";\n\n// ---------------------------------------------------------------------------\n// 1. Enums (backend contract — copied verbatim from cld_files_frontend.md §7)\n// ---------------------------------------------------------------------------\n\n/**\n * THE canonical `platform.visibility` enum, in enum order:\n * `personal < internal < link < public`. Identical on the server\n * (`matrx_utils.visibility.VisibilityLiteral`) and in the DB. One vocabulary,\n * no per-domain dialect.\n *\n * Two bugs are fossilized here; do not reintroduce either.\n *\n * 1. `internal` was folded into `personal` on read until 2026-07-26, so a file\n * readable by an entire organization was labelled \"Only you\" everywhere.\n * Never collapse a level to shrink this union — they are different facts.\n *\n * 2. This domain used to call `link` \"shared\". That is NOT a synonym: `shared`\n * was RETIRED from the DB enum on 2026-07-21, and the server's legacy map\n * reconciles it to `personal`. So the old code read `link` as `\"shared\"`\n * and, on any write-back, silently DOWNGRADED the file to `personal`.\n * Never send `shared` or `private` to the server.\n */\n// Visibility, PermissionLevel, MediaRef and FileIdentityHint live in `@ai-matrx/media/files` (P16f).\nimport type { Visibility, PermissionLevel, MediaRef, FileIdentityHint } from \"../../files\";\nexport type { Visibility, PermissionLevel, MediaRef, FileIdentityHint };\nexport type ResourceType = \"file\" | \"folder\";\n/**\n * Who a grant targets. Mirrors the canonical `iam.permissions` three-way\n * mutual exclusion (CHECK `user_or_org_or_public`): exactly one of a user, an\n * organization, or the public sentinel per row.\n * - `\"user\"` — `granted_to_user_id` set; `granteeId` is that user id.\n * - `\"group\"` — `granted_to_organization_id` set; `granteeId` is that org id.\n * - `\"public\"` — `is_public = true`; there is NO grantee id, so `granteeId`\n * carries the permission row's own id (never `\"\"`). A public\n * grant is \"anyone with access\", not a member — member/avatar\n * UIs must exclude it, and it is revoked by flipping the\n * resource's visibility (see `features/sharing/FEATURE.md`),\n * not via the by-grantee-id REST endpoint.\n */\nexport type GranteeType = \"user\" | \"group\" | \"public\";\n\n// ---------------------------------------------------------------------------\n// 1b. MediaRef — canonical reference shape for AI API content blocks\n// ---------------------------------------------------------------------------\n//\n// Mirrors the Python backend's `MediaRef` schema verbatim. Every outbound\n// `image` / `audio` / `video` / `document` content block on the AI APIs\n// (`/ai/agents/{id}`, `/ai/conversations/{id}`, `/ai/chat`, `/ai/manual`,\n// etc.) carries one of these as the file identifier.\n//\n// Backend Pydantic:\n// class MediaRef(BaseModel):\n// file_id: str | None = None # cld_files UUID — preferred\n// url: str | None = None # any URL we issued OR external https://\n// mime_type: str | None = None\n// metadata: dict[str, Any] = Field(default_factory=dict)\n//\n// Exactly ONE of `file_id` / `url` SHOULD be set, in that preference order.\n// Sending both is allowed but the backend resolves them in the same\n// priority — `file_id` wins, then `url`. Native storage locations\n// (`s3://...`) are server-only and never appear on the client.\n//\n// Use the builders in [redux/converters.ts](./redux/converters.ts):\n// - `cloudFileToMediaRef(file)` — for an in-store CloudFile\n// - `fileIdToMediaRef(id, mime?)` — when only the id is known\n// - `urlToMediaRef(url, mime?)` — for external public URLs\n//\n// **Don't hand-build MediaRefs at callsites.** The builders make sure we\n// never accidentally drift from this contract.\n// (MediaRef: see the import above.)\n\n/**\n * Metadata a caller may already know when all it has is a durable file id.\n * These values seed the canonical Redux record before field hydration runs;\n * omitted keys remain genuinely unloaded and are fetched on demand.\n */\n// (FileIdentityHint: see the import above.)\n\n// ---------------------------------------------------------------------------\n// 2. DB row types — straight from Supabase-generated Database type\n// ---------------------------------------------------------------------------\n//\n// These are the authoritative shapes for reads via supabase-js. Always use\n// these (not hand-rolled shapes) to track schema changes automatically.\n//\n// Note on table naming: The Python team's doc uses `cld_file_share_links`;\n// the canonical DB table is `platform.share_links` (see common-docs\n// systems/files/file-service/WIRE_CONTRACT.md).\n\n// Cloud-files tables live in the dedicated `files` schema (the `cld_` prefix\n// was dropped in the 2026 DB restructure). Permissions are the exception —\n// they live in the canonical `public.permissions` grant store.\ntype FilesTables = Database[\"files\"][\"Tables\"];\ntype IamTables = Database[\"iam\"][\"Tables\"];\n\nexport type CloudFileRow = FilesTables[\"files\"][\"Row\"];\nexport type CloudFileInsert = FilesTables[\"files\"][\"Insert\"];\nexport type CloudFileUpdate = FilesTables[\"files\"][\"Update\"];\n\n/**\n * What the client is actually ALLOWED to read from `files.files` — DERIVED from\n * `FILES_TABLE_COLUMNS` (the ONE canonical select, in [filesDb.ts](./filesDb.ts))\n * rather than declared beside it, so the two can never disagree.\n *\n * `files.files` grants `authenticated` NO table-level SELECT: every readable\n * column carries its own column grant, which is how the server-only native\n * storage location stays server-only. So a column ADDED to the table is\n * readable by nobody until a migration grants it, and a column list that names\n * an ungranted column fails the WHOLE read with \"permission denied for table\n * files\". Subtracting one name from the generated Row type therefore described\n * a row the client cannot actually read: on 2026-09-13 the table gained\n * `origin_device_id` and `client_modified_at` (folder-sync 028, no client\n * grant then) and every selected row stopped matching this type. Deriving from\n * the query makes the select list the single truth. (2026-09-24: `authenticated`\n * now holds SELECT on `origin_device_id` and `artifact_kind`, verified live, and\n * both are in the select list — Recents needs the first.)\n *\n * Same deal for `file_versions` via `FILE_VERSIONS_TABLE_COLUMNS`.\n */\nexport type CloudFileReadRow = NonNullable<\n Awaited<ReturnType<typeof readFileRowById>>\n>;\nexport type CloudFileVersionReadRow = Omit<\n FilesTables[\"file_versions\"][\"Row\"],\n // storage_uri is server-only (grant revoked). custom_fields is not granted\n // to the client either — selecting it 403s the whole version read. A column\n // the select list does not name is not part of the row the client reads.\n \"storage_uri\" | \"custom_fields\"\n>;\n\nexport type CloudFolderRow = FilesTables[\"folders\"][\"Row\"];\nexport type CloudFolderInsert = FilesTables[\"folders\"][\"Insert\"];\nexport type CloudFolderUpdate = FilesTables[\"folders\"][\"Update\"];\n\nexport type CloudFileVersionRow = FilesTables[\"file_versions\"][\"Row\"];\n/**\n * File-permission grants live in the CANONICAL grant store `iam.permissions`\n * (resource_type='file'), NOT in the legacy cld_ file-permission duplicate\n * (deprecated in the 2026 DB cutover — see docs/db_rebuild/03-app-agent-cutover-instructions.md §1a).\n */\nexport type CloudFilePermissionRow = IamTables[\"permissions\"][\"Row\"];\n\n/**\n * Summary JSON returned by `iam.fn_list_resource_permissions` and\n * `iam.fn_grant_resource_permission` — NOT a full `iam.permissions` row.\n * Maps grantee/org columns into `grantee_id` + `grantee_type` and file-level\n * `read|write|admin` permission strings.\n */\nexport interface IamResourcePermissionRpcRow {\n resource_id: string;\n resource_type: string;\n grantee_id: string;\n grantee_type: string;\n permission_level: string;\n granted_by: string | null;\n expires_at: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// 3. API (REST) types — from Python OpenAPI schemas\n// ---------------------------------------------------------------------------\n\nexport type FileRecordApi = components[\"schemas\"][\"FileRecord\"];\nexport type FileUploadResponse = components[\"schemas\"][\"FileUploadResponse\"];\nexport type FilePatchRequest = components[\"schemas\"][\"FilePatchRequest\"];\nexport type GrantPermissionRequest =\n components[\"schemas\"][\"GrantPermissionRequest\"];\n\n// ---------------------------------------------------------------------------\n// 4. Domain types (camelCase) — what the app works with\n// ---------------------------------------------------------------------------\n//\n// These are converted from CloudFileRow / CloudFolderRow in\n// [features/files/redux/converters.ts](./redux/converters.ts).\n\n/**\n * Discriminator on every cloud-file record. Real records are bytes in S3;\n * virtual records are Postgres rows surfaced by a `VirtualSourceAdapter`\n * (Notes, Agent Apps, Tool UIs, code-files snippets, etc.) — see\n * [features/files/virtual-sources/types.ts](./virtual-sources/types.ts).\n *\n * The `source` field defaults to `{ kind: \"real\" }` everywhere so existing\n * callers compile unchanged. Synthetic ids of shape\n * `vfs:<adapterId>:<virtualId>[:<fieldId>]` keep the cloud-files Redux\n * `filesById` / `foldersById` maps a single keyspace.\n */\nexport type FileSource =\n | { kind: \"real\" }\n | {\n kind: \"virtual\";\n adapterId: string;\n virtualId: string;\n fieldId?: string;\n };\n\nexport interface CloudFile {\n id: string;\n ownerId: string;\n /** Canonical owning workspace from `files.files.organization_id`. */\n organizationId?: string | null;\n filePath: string;\n fileName: string;\n mimeType: string | null;\n fileSize: number | null;\n checksum: string | null;\n visibility: Visibility;\n currentVersion: number;\n parentFolderId: string | null;\n metadata: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n deletedAt: string | null;\n /**\n * Permanent CDN URL (Cloudflare-fronted) when the file is public AND\n * the server has the CDN feature enabled. ``null`` otherwise — callers\n * should fall back to ``useFileSrc({ kind: \"file_id\", fileId })`` for the durable URL.\n *\n * Carries a ``?v=<checksum[:8]>`` cache-buster so a content change\n * invalidates the cache instantly. **Do not strip the query string.**\n *\n * Populated by the API converter (``apiFileRecordToCloudFile``);\n * always ``null`` for rows that came in via the direct DB read path\n * because the DB has no ``public_url`` column — it's computed\n * server-side from visibility + storage location + checksum. For DB-sourced\n * rows, fall back to ``useFileSrc({ kind: \"file_id\", fileId })`` to fetch the canonical\n * URL (which the server returns as a CDN URL when applicable).\n */\n publicUrl: string | null;\n /**\n * The DURABLE URL envelope the REST `FileRecord` carries. `url` is the\n * server's canonical always-renderable pick (CDN for public, the durable\n * `/files/{id}/download?inline=1` route for private — authenticated by\n * the `mx_files_session` cookie). `cdnUrl` is public-only and permanent.\n * `downloadUrl` carries attachment disposition. None of them expire.\n *\n * These are populated by `apiFileRecordToCloudFile` from the REST\n * response. They are `null` on the direct-DB read path (the `cld_files`\n * table has no computed-URL columns) — DB-sourced rows build the durable\n * URL from the file id via the resolver.\n */\n url: string | null;\n cdnUrl: string | null;\n downloadUrl: string | null;\n /**\n * Backend-rendered thumbnail URL (Phase 1b universal thumbnails). Set\n * for **every** uploaded file regardless of MIME — Python now renders\n * SOCIAL_BASELINE variants (og_url / thumbnail_url / tiny_url) for\n * images, PDFs (page 1), videos (10%-mark frame), audio (waveform),\n * and even archives / text / unknown mimes (mime-family icon PNGs).\n *\n * Populated by `apiFileRecordToCloudFile` from `FileRecord.thumbnail_url`\n * — the REST response field. The server resolves this from the\n * variants store at request time (post-Phase-1b the legacy\n * `cld_files.thumbnail_url` column is dropped).\n *\n * `null` for rows coming in via the direct Supabase read path — the\n * row doesn't carry resolved URLs. Those callers should fall back to\n * `useFileAsset(fileId)` and read `asset.variants[\"thumbnail_url\"].url`,\n * or `MediaThumbnail` will fall back to the category icon.\n *\n * Phase 1c: PDFs additionally get `Asset.variants[\"page1_url\"]`\n * (page 1 at 150 DPI ~1200×1700) for full-page detail views, and\n * videos get `Asset.variants[\"poster_url\"]` (native-res frame) for\n * the HTML5 `<video poster>` attribute. Those are separate from this\n * `thumbnailUrl` field and require the asset fetch.\n */\n thumbnailUrl: string | null;\n /** Real S3-backed bytes vs. virtual Postgres-backed adapter row. */\n source: FileSource;\n /**\n * Binary lineage — points to the cld_files row this one was derived\n * from (e.g. \"extracted text from this PDF\" or \"page range 5–10 of\n * the parent PDF\"). Set by Phase 4A migration `0006_cld_files_lineage`.\n * Null when the file was uploaded directly with no derivation.\n * Optional on the FE because it is null for nearly every existing\n * file and was added to the API after the initial schema landed.\n */\n parentFileId?: string | null;\n /**\n * Free-form classifier set by the deriving system: \"pdf_text_extract\",\n * \"page_range_5_10\", \"ocr_re_run\", \"merge\", … Used by lineage chips\n * to label *how* the parent relates to this file.\n */\n derivationKind?: string | null;\n derivationMetadata?: Record<string, unknown> | null;\n /**\n * The registered device (`public.app_instances.id`) that wrote this row — a\n * desktop sync client — or null for an in-app write. A device's write is the\n * person's file but never their recent activity: Recents keys on this via\n * `isRecentActivityFile` (mirror of `files.is_recent_activity`).\n */\n originDeviceId?: string | null;\n /**\n * When set, this row is a deliberate parallel copy of `duplicateOfFileId`\n * (the keeper). Set by:\n * - the dedup consolidation script (soft-deletes the duplicate row +\n * stamps the keeper's id here so refs to the dup still resolve), or\n * - `intent: \"force_new_copy\"` uploads (user explicitly asked for a\n * second copy of identical content).\n * Null for every freshly-uploaded file.\n *\n * UI: surface a \"duplicate of <keeper>\" chip on rows where this is set.\n * Refs that hit a `deletedAt != null` row should `useFile(duplicateOfFileId)`\n * to follow the chain to the live keeper.\n */\n duplicateOfFileId?: string | null;\n /**\n * Points at the \"official\" `processed_documents.id` for this file —\n * the canonical text-extraction row out of potentially many re_extract /\n * re_clean variants. UIs that show \"extracted text\" or feed Knowledge should\n * use this column to find THE extract, not the freshest one.\n */\n canonicalProcessedDocumentId?: string | null;\n}\n\nexport interface CloudFolder {\n id: string;\n ownerId: string;\n folderPath: string;\n folderName: string;\n parentId: string | null;\n visibility: Visibility;\n metadata: Record<string, unknown>;\n createdAt: string;\n updatedAt: string;\n deletedAt: string | null;\n /** Real cloud-folder vs. virtual adapter root / nested adapter folder. */\n source: FileSource;\n}\n\nexport interface CloudFileVersion {\n id: string;\n fileId: string;\n versionNumber: number;\n fileSize: number | null;\n checksum: string | null;\n createdBy: string | null;\n createdAt: string;\n changeSummary: string | null;\n}\n\nexport interface CloudFilePermission {\n id: string;\n resourceId: string;\n resourceType: ResourceType;\n granteeId: string;\n granteeType: GranteeType;\n permissionLevel: PermissionLevel;\n grantedBy: string | null;\n grantedAt: string;\n expiresAt: string | null;\n}\n\n/**\n * A canonical share link (`platform.share_links`) scoped to a file or folder.\n * Minted/listed/revoked via the canonical RPC family (`create_share_link` /\n * `list_share_links` / `revoke_share_link` — see `utils/permissions/shareLinks.ts`).\n */\nexport interface CloudShareLink {\n id: string;\n resourceId: string;\n resourceType: ResourceType;\n shareToken: string;\n permissionLevel: \"viewer\" | \"editor\";\n label: string | null;\n createdAt: string | null;\n expiresAt: string | null;\n maxUses: number | null;\n useCount: number;\n isActive: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// 5. Tree RPC types — get_user_file_tree\n// ---------------------------------------------------------------------------\n//\n// The Supabase-generated type is `Json` — we hand-type the expected shape\n// tolerantly (converters accept unknown and narrow). If the Python team\n// updates the shape, only [redux/converters.ts](./redux/converters.ts) needs\n// to change.\n//\n// Open question on the exact schema: common-docs systems/files/file-service/HANDOFF.md.\n\nexport interface CloudTreeFileRow {\n kind: \"file\";\n id: string;\n file_path: string;\n file_name: string;\n parent_folder_id: string | null;\n mime_type: string | null;\n /**\n * File size in bytes. Renamed from `file_size` in Phase 0 (see\n * docs/PYTHON_UPDATES.md §3). The RPC `tree_for_owner` (and friends)\n * now returns `size_bytes`; converters read both names defensively\n * during the transition. Consumers should always read `size_bytes`.\n */\n size_bytes: number | null;\n visibility: Visibility;\n current_version: number;\n effective_permission: PermissionLevel | null;\n owner_id: string;\n created_at: string;\n updated_at: string;\n deleted_at: string | null;\n /** Device that wrote the row (desktop sync), null for an in-app write. */\n origin_device_id: string | null;\n /**\n * The row's metadata as the RPC returns it — carries `system_artifact`, the\n * marker every file list reads through `isListedFile` (2026-09-29; the tree\n * used to drop it, so system files could not be told apart in the store).\n */\n metadata: Record<string, unknown>;\n}\n\nexport interface CloudTreeFolderRow {\n kind: \"folder\";\n id: string;\n folder_path: string;\n folder_name: string;\n parent_id: string | null;\n visibility: Visibility;\n effective_permission: PermissionLevel | null;\n owner_id: string;\n created_at: string;\n updated_at: string;\n deleted_at: string | null;\n metadata: Record<string, unknown>;\n}\n\nexport type CloudTreeRow = CloudTreeFileRow | CloudTreeFolderRow;\n\n// ---------------------------------------------------------------------------\n// 6. Runtime records — what lives in Redux state\n// ---------------------------------------------------------------------------\n//\n// Pattern copied from features/agents/redux/agent-shortcuts (see\n// slice.ts::makeEmptyRecord). Every user-editable field gets tracked in\n// _dirtyFields + _fieldHistory so optimistic updates can roll back on error.\n// _pendingRequestIds holds requestIds in flight; the realtime middleware\n// checks this set to dedup its own echoes.\n\nexport interface RuntimeMetadata<K extends string> {\n _dirty: boolean;\n _dirtyFields: FieldFlags<K>;\n _loadedFields: FieldFlags<K>;\n _loading: boolean;\n _error: string | null;\n _pendingRequestIds: string[];\n}\n\nexport type CloudFileFieldSnapshot = Partial<\n Pick<\n CloudFile,\n | \"fileName\"\n | \"filePath\"\n | \"visibility\"\n | \"parentFolderId\"\n | \"metadata\"\n | \"deletedAt\"\n >\n>;\n\nexport interface CloudFileRecord\n extends CloudFile, RuntimeMetadata<keyof CloudFile> {\n _fieldHistory: CloudFileFieldSnapshot;\n}\n\nexport type CloudFolderFieldSnapshot = Partial<\n Pick<\n CloudFolder,\n \"folderName\" | \"folderPath\" | \"parentId\" | \"visibility\" | \"metadata\"\n >\n>;\n\nexport interface CloudFolderRecord\n extends CloudFolder, RuntimeMetadata<keyof CloudFolder> {\n _fieldHistory: CloudFolderFieldSnapshot;\n}\n\n// ---------------------------------------------------------------------------\n// 7. Tree & UI state\n// ---------------------------------------------------------------------------\n\nexport interface TreeChildren {\n folderIds: string[];\n fileIds: string[];\n}\n\nexport interface TreeState {\n rootFolderIds: string[];\n rootFileIds: string[];\n childrenByFolderId: Record<string, TreeChildren>;\n fullyLoadedFolderIds: Record<string, true>;\n status: \"idle\" | \"loading\" | \"loaded\" | \"error\";\n error: string | null;\n lastReconciledAt: number | null;\n}\n\nexport type ViewMode = \"list\" | \"grid\" | \"columns\";\n/**\n * Sticky filter chips above the file table. Mirrors the `FilterChips`\n * component's union — re-declared here (not imported) so the slice\n * stays component-cycle-free. Keep in sync with\n * `features/files/components/surfaces/desktop/FilterChips.tsx`.\n */\nexport type ChipFilter = \"recents\" | \"starred\";\n\n/**\n * Column sort keys. Mirror the file-table columns so users can sort by any\n * column they're looking at. Folders always group ahead of files regardless\n * of the active key (Box / Drive / Dropbox convention) — see `compareNodes`.\n */\nexport type SortBy =\n | \"name\"\n | \"type\"\n | \"extension\"\n | \"mime\"\n | \"path\"\n | \"owner\"\n | \"size\"\n | \"version\"\n | \"updated_at\"\n | \"created_at\";\nexport type SortDirection = \"asc\" | \"desc\";\n/** Whether the file table shows files only, folders only, or both. */\nexport type KindFilter = \"all\" | \"files\" | \"folders\";\n/** Optional details surface (extension, mime, dimensions, etc.) on rows. */\nexport type DetailsLevel = \"compact\" | \"extended\";\n\n/** Modified-date preset filter — \"today\", \"week\" = last 7d, \"month\" = last 30d. */\nexport type ModifiedFilter = \"any\" | \"today\" | \"week\" | \"month\";\n/** Size preset filter — buckets familiar to users. */\nexport type SizeFilter = \"any\" | \"small\" | \"medium\" | \"large\" | \"huge\";\n/**\n * Access (visibility) filter. One option per `Visibility` level plus \"any\" —\n * a level with no option is a level whose rows can never be filtered to.\n */\nexport type AccessFilter = \"any\" | Visibility;\n/**\n * Type filter — multi-select set of file categories (CODE, DOCUMENT, IMAGE,\n * VIDEO, …). Empty array = \"any type\". Modeled as `string[]` (not the\n * `FileCategory` enum from `utils/file-types.ts`) to keep the slice type\n * import-cycle-free; the selectors / pickers cast at the boundary.\n */\nexport type TypeFilter = string[];\n/** Owner filter — multi-select set of owner user ids. Empty = \"any owner\". */\nexport type OwnerFilter = string[];\n\n/**\n * Per-file Knowledge indexing status.\n *\n * - `indexed` — backend has a `processed_documents` row for this file\n * - `not_indexed` — file exists but no doc row (`/files/{id}/document` 404)\n * - `pending` — request is in flight\n * - `unknown` — endpoint returned a transient error or hasn't been\n * called yet for this file\n *\n * The pending state matters because the user toggles \"Show Knowledge status\"\n * across hundreds of files at once — the column needs to show \"Checking…\"\n * for the rows still in flight rather than flickering \"Not indexed\".\n */\nexport type RagStatus = \"indexed\" | \"not_indexed\" | \"pending\" | \"unknown\";\n\n/**\n * Knowledge filter — multi-select of statuses. Empty array = \"any status\".\n * Modeled as `string[]` (not `RagStatus[]`) to mirror `TypeFilter` /\n * `OwnerFilter` and keep the slice type import-cycle-free.\n */\nexport type RagFilter = string[];\n\n/** Per-column filters surfaced through the column-header dropdowns. */\nexport interface ColumnFilters {\n /** Name \"contains\" — column-scoped text filter, distinct from the\n * global search box. */\n name: string;\n /** File category multi-select (Image / Video / Code / …). */\n type: TypeFilter;\n /** Extension \"contains\" — e.g. \"pdf\", \"jp\" matches jpg & jpeg. */\n extension: string;\n /** MIME \"contains\" — e.g. \"image/\" matches every image. */\n mime: string;\n /** Folder path \"contains\" — useful in tree-wide search results. */\n path: string;\n /** Owner user-id multi-select. */\n owner: OwnerFilter;\n modified: ModifiedFilter;\n /** Same preset semantics as `modified`, applied to `createdAt`. */\n created: ModifiedFilter;\n size: SizeFilter;\n access: AccessFilter;\n /**\n * Knowledge indexing status multi-select. Only meaningful when the user has\n * fetched Knowledge statuses (via the Knowledge column toggle in column-settings or\n * the column-header refresh button) — folders never pass this filter\n * since Knowledge indexing is a file-only concept.\n */\n rag: RagFilter;\n}\n\n/**\n * Stable ids for every optional / required column rendered in the file\n * table. Hidden vs. visible columns are tracked in\n * `UiState.visibleColumns` — a Box.com / Google-Drive-style \"Choose\n * columns\" panel toggles each on/off. `name` and `access` are conceptually\n * always present but are still included here so the type remains the\n * single source of truth.\n */\nexport type ColumnId =\n | \"name\"\n | \"type\"\n | \"extension\"\n | \"mime\"\n | \"path\"\n | \"owner\"\n | \"size\"\n | \"version\"\n | \"updated_at\"\n | \"created_at\"\n | \"access\"\n | \"rag_status\"\n | \"context\";\n\nexport type VisibleColumns = Record<ColumnId, boolean>;\n\n/**\n * Default column set on first load. Tuned to match what users coming from\n * Box.com / Google Drive expect to see by default — Type is shown\n * because \"what kind of file is this\" is the second-most-important\n * question after \"what's its name\", and the previous shipping default\n * (just Name / Modified / Size / Access) silently hid that signal.\n *\n * `rag_status` is OFF by default because populating it requires a per-file\n * network probe that the user explicitly opts into.\n */\nexport const DEFAULT_VISIBLE_COLUMNS: VisibleColumns = {\n name: true,\n type: true,\n extension: false,\n mime: false,\n path: false,\n owner: true,\n size: true,\n version: false,\n updated_at: true,\n created_at: false,\n access: true,\n rag_status: false,\n // ON by default on purpose: context drives Knowledge/NER and everything\n // downstream — its presence (or amber absence) must be impossible to miss.\n // One bulk query per visible page populates it (no per-row probes).\n context: true,\n};\n\nexport interface UiState {\n viewMode: ViewMode;\n sortBy: SortBy;\n sortDir: SortDirection;\n /** Files-only / folders-only / both. Default = \"all\". */\n kindFilter: KindFilter;\n /** Whether to show extra detail columns (Extension, Type, Owner, …). */\n detailsLevel: DetailsLevel;\n /** Per-column filter values driven by the column-header dropdowns. */\n columnFilters: ColumnFilters;\n /** Which optional columns are mounted in the file table. */\n visibleColumns: VisibleColumns;\n /**\n * Tree-wide search box value. Lives in Redux (not local component state)\n * so the URL-sync layer can reflect it as `?q=…` and so cross-component\n * reads (e.g. clearing search from a chip) don't need callbacks.\n */\n searchQuery: string;\n /**\n * Sticky filter chip currently active (Recents / Starred). Independent\n * of `kindFilter` — chips apply preset semantics on top of any column\n * filters. `null` = no chip.\n */\n chipFilter: ChipFilter | null;\n activeFileId: string | null;\n activeFolderId: string | null;\n /**\n * The single item (file or folder id) that has \"keyboard/visual focus\" — the\n * highlighted row in the Google Drive sense. Set after create/upload so the\n * newly-created item is immediately highlighted and scrolled into view.\n * Clicking any row also moves focus to that row.\n */\n focusedId: string | null;\n}\n\nexport interface SelectionState {\n selectedIds: string[];\n anchorId: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// 8. Upload orchestrator state\n// ---------------------------------------------------------------------------\n\nexport type UploadStatus =\n \"pending\" | \"uploading\" | \"success\" | \"error\" | \"cancelled\";\n\nexport interface UploadState {\n requestId: string;\n fileName: string;\n fileSize: number;\n parentFolderId: string | null;\n /**\n * The logical `folderPath` this upload targeted (`UploadFilesArg.folderPath`),\n * verbatim — `null` when the caller used `parentFolderId` instead.\n *\n * This is the ONE correlation channel a container-scoped surface has for\n * finding its own failed uploads: `state.uploads` is a flat, app-wide map\n * (every uploader shares it — the Files page, chat attachments, the\n * Rulebook Resources card…), so a surface that wants \"MY failed uploads,\n * not anyone else's\" gives itself a folder path nothing else uses and reads\n * back entries matching it exactly (see\n * `RulebookSourcesPanel.tsx`'s `sourcesFolderPath`).\n */\n folderPath: string | null;\n status: UploadStatus;\n bytesUploaded: number;\n startedAt: number;\n completedAt: number | null;\n error: string | null;\n retries: number;\n /** Populated on success; null until then. */\n fileId: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// 9. Slice state shape\n// ---------------------------------------------------------------------------\n//\n// Registered under key `cloudFiles` in lib/redux/rootReducer.ts (Phase 2).\n\n/**\n * Per-file Knowledge indexing status, hydrated lazily by the\n * `prefetchRagStatusesForFiles` thunk. The thunk de-duplicates against\n * `byFileId` so toggling the Knowledge column on/off doesn't re-fetch already\n * known answers (use the column header's refresh action to force).\n */\nexport interface RagStatusState {\n byFileId: Record<string, RagStatus>;\n /** True while a batch fetch is in flight. */\n isFetching: boolean;\n /** ms timestamp of the most-recent successful batch (any source). */\n lastFetchedAt: number | null;\n}\n\nexport interface CloudFilesState {\n filesById: Record<string, CloudFileRecord>;\n foldersById: Record<string, CloudFolderRecord>;\n versionsByFileId: Record<string, CloudFileVersion[]>;\n permissionsByResourceId: Record<string, CloudFilePermission[]>;\n shareLinksByResourceId: Record<string, CloudShareLink[]>;\n\n tree: TreeState;\n selection: SelectionState;\n ui: UiState;\n uploads: Record<string, UploadState>;\n\n /** Per-file Knowledge indexing status, populated on demand. */\n ragStatus: RagStatusState;\n\n /**\n * Realtime attachment status. Mirrors the supabase Channel lifecycle.\n */\n realtime: {\n status: \"detached\" | \"connecting\" | \"subscribed\" | \"errored\" | \"closed\";\n userId: string | null;\n lastEventAt: number | null;\n error: string | null;\n };\n}\n\n// ---------------------------------------------------------------------------\n// 10. Thunk argument shapes (Phase 2 will dispatch these)\n// ---------------------------------------------------------------------------\n\nexport interface CreateFolderArg {\n folderName: string;\n parentId: string | null;\n visibility?: Visibility;\n metadata?: Record<string, unknown>;\n}\n\nexport interface DeleteFolderArg {\n folderId: string;\n}\n\nexport interface EnsureFolderPathArg {\n /**\n * A folder path like \"Images/2026/Q1\". Each segment is created if missing.\n * Returns the leaf folder's id.\n */\n folderPath: string;\n visibility?: Visibility;\n}\n\nexport interface UploadFilesArg {\n files: File[];\n /**\n * Existing folder id (rare — only when you've already created/loaded the\n * folder via realtime or the tree RPC). Prefer `folderPath` for new\n * uploads — the backend auto-creates folders so the browser doesn't\n * need to query `cld_folders` (which can recurse on RLS until the\n * SECURITY DEFINER policy fix lands; see HANDOFF.md).\n */\n parentFolderId?: string | null;\n /**\n * Logical folder path (e.g. \"Images/Chat\" or \"Debug Uploads\"). The\n * Python backend creates any missing folders during upload. This is\n * the recommended option for new uploads — it doesn't trigger any\n * supabase-js queries on `cld_folders` from the browser.\n */\n folderPath?: string | null;\n visibility?: Visibility;\n shareWith?: string[];\n shareLevel?: PermissionLevel;\n changeSummary?: string;\n metadata?: Record<string, unknown>;\n /**\n * Per-upload options forwarded to the backend `options_json` field.\n * Notably `rag.trigger_now` to run Knowledge immediately on upload instead of\n * waiting for the scheduled auto-Knowledge sweep. Only menu/explicit uploads set\n * this — drag-drop leaves it unset (scheduled sweep still runs).\n */\n options?: { rag?: { trigger_now?: boolean } };\n /** Parallel upload ceiling. Defaults to 3. */\n concurrency?: number;\n /**\n * Per-file path overrides. Keyed by index into `files` (string\n * because Records use string keys). When set for a file, the\n * upload uses this exact path (relative to the parent prefix)\n * instead of the file's own name — i.e. it can target an EXISTING\n * file's path so the backend version-bumps it.\n *\n * The auto \" (1)\" / \" (2)\" rename in `uploadFiles` is bypassed\n * for any index that has an override, since the override is the\n * user's explicit choice (typically \"Overwrite\" from the\n * duplicate-upload dialog).\n *\n * Indices not present here use the default name resolution.\n */\n filenameOverrides?: Record<number, string>;\n /**\n * Indices to drop from the upload entirely. Used by the\n * duplicate-upload dialog's \"Skip\" action — we want to keep the\n * batch shape stable for telemetry, but not actually upload\n * those files. Indices reference the original `files` array.\n */\n skipIndices?: number[];\n /**\n * Indices whose duplicate-dialog decision was explicitly \"Make a copy\".\n * The upload keeps its unique display name and sends the backend's strict\n * `force_new_copy` intent so checksum-identical bytes create a distinct,\n * durable row linked through `duplicate_of_file_id`.\n */\n forceNewCopyIndices?: number[];\n}\n\nexport interface RenameFileArg {\n fileId: string;\n newName: string;\n}\n\nexport interface MoveFileArg {\n fileId: string;\n newParentFolderId: string | null;\n}\n\nexport interface UpdateFileMetadataArg {\n fileId: string;\n patch: {\n visibility?: Visibility;\n metadata?: Record<string, unknown>;\n };\n}\n\nexport interface DeleteFileArg {\n fileId: string;\n}\n\n/**\n * Save new content as the next version of an EXISTING file (edit-in-place).\n * `content` is the whole new body; the file keeps its id, name and folder.\n */\nexport interface SaveFileNewVersionArg {\n fileId: string;\n content: string | Blob;\n changeSummary?: string;\n}\n\n/** What a successful save wrote: the same file id at its new version. */\nexport interface SaveFileNewVersionResult {\n fileId: string;\n versionNumber: number;\n}\n\nexport interface RestoreVersionArg {\n fileId: string;\n versionNumber: number;\n}\n\n/**\n * Grant/revoke via the by-grantee REST path. `granteeType` here is a `user` or\n * `group` grant only — `\"public\"` is not a by-grantee grant (it is toggled on\n * the resource's visibility), and the grant/revoke thunks throw if it is passed\n * (see `requireByGranteeType` in `redux/thunks.ts`). Defaults to `\"user\"`.\n */\nexport interface GrantPermissionArg {\n resourceId: string;\n resourceType: ResourceType;\n granteeId: string;\n granteeType?: GranteeType;\n level: PermissionLevel;\n expiresAt?: string;\n}\n\nexport interface RevokePermissionArg {\n resourceId: string;\n resourceType: ResourceType;\n granteeId: string;\n granteeType?: GranteeType;\n}\n\nexport interface CreateShareLinkArg {\n resourceId: string;\n resourceType: ResourceType;\n permissionLevel: \"viewer\" | \"editor\";\n expiresAt?: string;\n maxUses?: number;\n}\n\nexport interface RevokeShareLinkArg {\n /** `platform.share_links.id` — the canonical revoke key. */\n linkId: string;\n}\n\n// ---------------------------------------------------------------------------\n// 10b. Folder CRUD requests (Python P-6 contract)\n// ---------------------------------------------------------------------------\n\n/**\n * Request body for `POST /folders` — create a folder. The Python team\n * accepts a logical path (e.g. \"Images/Chat\") OR an explicit name + parentId.\n * Path-style is preferred because the backend creates intermediate folders\n * atomically and idempotently, matching upload's auto-create semantics.\n */\n/**\n * Body for `POST /folders`. Path-style ONLY — the backend creates any missing\n * segments and REJECTS `{folder_name, parent_id}` (`validation_error`).\n * DERIVED from the contract so the phantom name/parent fields can't be sent.\n */\nexport type CreateFolderRequest = components[\"schemas\"][\"CreateFolderRequest\"];\n\n/**\n * Body for `PATCH /folders/{id}`. Rename AND move are expressed as the target\n * `folder_path` — the server renames, reparents, and cascades descendants from\n * it. The backend SILENTLY IGNORES `folder_name`/`parent_id` (they are not on\n * the model), so sending them made rename/move no-op server-side. DERIVED from\n * the contract to make that mistake a compile error.\n */\nexport type FolderPatchRequest = components[\"schemas\"][\"PatchFolderRequest\"];\n\n// ---------------------------------------------------------------------------\n// 10c. Bulk operations (Python P-7 contract)\n// ---------------------------------------------------------------------------\n\n/** Body for `DELETE /files/bulk`. */\nexport interface BulkDeleteFilesRequest {\n file_ids: string[];\n}\n\n/** Body for `POST /files/bulk/move`. */\nexport interface BulkMoveFilesRequest {\n file_ids: string[];\n /** Target parent folder id, or null to move to root. */\n new_parent_folder_id: string | null;\n}\n\n/** Body for `POST /folders/bulk/move`. */\nexport interface BulkMoveFoldersRequest {\n folder_ids: string[];\n /** Target parent folder id, or null to move to root. */\n new_parent_id: string | null;\n}\n\n/**\n * Per-item outcome inside a bulk response. Matches the backend\n * `BulkResultItem` shape: `{ id, ok, error }` per item plus the\n * aggregate counters returned alongside.\n */\nexport interface BulkResultItem {\n id: string;\n ok: boolean;\n /** Error code/message string when `ok` is false; null on success. */\n error: string | null;\n}\n\n/**\n * Aggregate envelope returned by every bulk endpoint:\n * - `DELETE /files/bulk`\n * - `POST /files/bulk/move`\n * - `POST /folders/bulk/move`\n *\n * The aggregate `succeeded` and `failed` are NUMBERS (counts), not\n * arrays. Per-item detail lives in `results`.\n */\nexport interface BulkResponse {\n results: BulkResultItem[];\n succeeded: number;\n failed: number;\n}\n\n// ---------------------------------------------------------------------------\n// 10e. Storage usage / quotas / tier (GET /files/usage)\n// ---------------------------------------------------------------------------\n\n/**\n * Response body of `GET /files/usage`. Drives the storage indicator,\n * the tier badge, and feature gating in the UI. `null` on a numeric\n * field means \"no cap\" (typically Enterprise tier).\n */\nexport interface StorageUsageResponse {\n tier_id: string;\n tier_name: string;\n is_blocked: boolean;\n blocked_reason: string | null;\n bytes_used: number;\n files_count: number;\n daily_upload_count: number;\n daily_upload_bytes: number;\n max_storage_bytes: number | null;\n max_file_size_bytes: number | null;\n max_files: number | null;\n max_versions_per_file: number | null;\n max_daily_uploads: number | null;\n max_daily_upload_bytes: number | null;\n max_bulk_items: number | null;\n rate_limit_uploads_per_min: number | null;\n rate_limit_downloads_per_min: number | null;\n features: Record<string, unknown>;\n /**\n * TRUE when `files.user_storage_usage` actually holds a row for this user.\n *\n * `get_usage_status` synthesizes `{bytes_used: 0, files_count: 0, …}` when\n * the ledger row is ABSENT, which on screen is indistinguishable from a\n * genuinely empty account — so the UI would say \"0 bytes of 5 GB\" about an\n * account whose usage nobody has measured. The flattener detects the\n * synthesized shape (it carries no `user_id`) and says so here, and the\n * meter renders \"usage being recalculated\" instead of a number it does not\n * have. folder-sync SPEC-SERVER §8: metering only just landed on the\n * standalone service, and FS-L6 still has to rebuild and re-grain the\n * ledger — an unbuilt row is the expected state, not an error.\n */\n ledger_measured: boolean;\n /** When the ledger row was last written, or null when there is no row. */\n ledger_measured_at: string | null;\n}\n\n// ---------------------------------------------------------------------------\n// 10f. Trash + restore (GET /files/trash, POST /files/{id}/restore)\n// ---------------------------------------------------------------------------\n\n/**\n * Response body of `GET /files/trash`. Soft-deleted files + folders for\n * the authenticated user (or guest fingerprint).\n */\nexport interface TrashListResponse {\n files: FileRecordApi[];\n folders: CloudFolderRow[];\n}\n\n// ---------------------------------------------------------------------------\n// 10g. Search (GET /files/search)\n// ---------------------------------------------------------------------------\n\n/**\n * Response body of `GET /files/search?q=&mime_prefix=&limit=&offset=`.\n * The backend matches against filename + path substring; `mime_prefix`\n * filters by `mime_type LIKE 'prefix%'`.\n */\nexport interface SearchFilesResponse {\n results: FileRecordApi[];\n query: string;\n total_returned: number;\n}\n\nexport interface SearchFilesParams {\n q: string;\n mimePrefix?: string;\n limit?: number;\n offset?: number;\n}\n\n// ---------------------------------------------------------------------------\n// 10h. Rename + copy (POST /files/{id}/rename, POST /files/{id}/copy)\n// ---------------------------------------------------------------------------\n\nexport interface RenameFileRequest {\n /** Full new logical path including filename. Backend auto-creates parents. */\n new_path: string;\n}\n\nexport interface CopyFileRequest {\n /** Full target logical path including filename. Backend auto-creates parents. */\n target_path: string;\n /** Default false. When false, conflicts return `409 file_already_exists`. */\n overwrite?: boolean;\n}\n\n// Convenience thunk-arg variants (camelCase mirrors of the request bodies).\nexport interface BulkDeleteFilesArg {\n fileIds: string[];\n}\n\nexport interface BulkMoveFilesArg {\n fileIds: string[];\n newParentFolderId: string | null;\n}\n\nexport interface BulkMoveFoldersArg {\n folderIds: string[];\n newParentId: string | null;\n}\n\nexport interface UpdateFolderArg {\n folderId: string;\n patch: {\n folderName?: string;\n parentId?: string | null;\n visibility?: Visibility;\n metadata?: Record<string, unknown>;\n };\n}\n\n// ---------------------------------------------------------------------------\n// 10i. Rename / copy thunk args (camelCase mirrors)\n// ---------------------------------------------------------------------------\n\nexport interface RenameFileToPathArg {\n fileId: string;\n /** Full new logical path including filename. */\n newPath: string;\n}\n\nexport interface CopyFileArg {\n fileId: string;\n /** Full target logical path including filename. */\n targetPath: string;\n overwrite?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// 10j. Search (camelCase mirrors)\n// ---------------------------------------------------------------------------\n\nexport interface SearchFilesArg {\n q: string;\n mimePrefix?: string;\n limit?: number;\n offset?: number;\n}\n\n// ---------------------------------------------------------------------------\n// 11. Request ledger\n// ---------------------------------------------------------------------------\n//\n// Every REST write registers a requestId in the ledger so the realtime\n// middleware can dedup echoes of our own optimistic writes.\n\nexport type RequestKind =\n | \"upload\"\n | \"update\"\n | \"delete\"\n | \"move\"\n | \"rename\"\n | \"restore-version\"\n | \"grant-permission\"\n | \"revoke-permission\"\n | \"create-share-link\"\n | \"revoke-share-link\"\n | \"folder-create\"\n | \"folder-update\"\n | \"folder-delete\"\n | \"bulk-delete-files\"\n | \"file-rename-path\"\n | \"file-copy\"\n | \"file-restore\"\n | \"bulk-move-files\"\n | \"bulk-move-folders\";\n\nexport interface LedgerEntry {\n requestId: string;\n kind: RequestKind;\n resourceId: string | null;\n resourceType: ResourceType | null;\n createdAt: number;\n}\n\n// ---------------------------------------------------------------------------\n// 12. Error codes — re-export backend error type, add files-specific codes\n// ---------------------------------------------------------------------------\n\nexport type { BackendApiError } from \"@ai-matrx/agents/matrx\";\n\n/**\n * Files-specific error codes.\n *\n * Aligned with the Python backend's error envelope. The retry posture\n * column is the FE contract — every caller must respect it:\n *\n * | Code | HTTP | Retry? | UX hint |\n * |---------------------------|------|---------|--------------------------|\n * | invalid_request | 400 | no | fix the request |\n * | invalid_path | 400 | no | fix the path |\n * | invalid_metadata | 400 | no | fix the patch |\n * | fingerprint_required | 400 | no | guest header missing |\n * | auth_required | 401 | no | sign in |\n * | permission_denied | 403 | no | request access |\n * | guest_id_mismatch | 403 | no | re-auth |\n * | not_found | 404 | no | resource gone |\n * | conflict | 409 | no | overwrite=true to force |\n * | file_already_exists | 409 | no | overwrite=true to force |\n * | guest_locked | 409 | no | already migrated |\n * | file_too_large | 413 | no | upgrade tier |\n * | storage_quota_exceeded | 413 | no | upgrade tier |\n * | file_count_exceeded | 413 | no | upgrade tier |\n * | daily_uploads_exceeded | 413 | no | wait until tomorrow |\n * | daily_bytes_exceeded | 413 | no | wait until tomorrow |\n * | bulk_too_large | 413 | no | smaller batch |\n * | rate_limited | 429 | YES (b) | exponential backoff |\n * | account_blocked | 423 | no | contact support |\n * | share_link_invalid | 410 | no | link revoked / expired |\n * | internal | 5xx | YES (b) | exponential backoff |\n * | cld_sync_unavailable | 503 | YES (b) | exponential backoff |\n */\nexport type CloudFilesErrorCode =\n | \"invalid_request\"\n | \"invalid_path\"\n | \"invalid_metadata\"\n | \"fingerprint_required\"\n | \"auth_required\"\n | \"permission_denied\"\n | \"guest_id_mismatch\"\n | \"not_found\"\n | \"conflict\"\n | \"file_already_exists\"\n | \"guest_locked\"\n | \"file_too_large\"\n | \"storage_quota_exceeded\"\n | \"file_count_exceeded\"\n | \"daily_uploads_exceeded\"\n | \"daily_bytes_exceeded\"\n | \"bulk_too_large\"\n | \"rate_limited\"\n | \"account_blocked\"\n | \"share_link_invalid\"\n | \"internal\"\n | \"cld_sync_unavailable\";\n\n// ---------------------------------------------------------------------------\n// 13. Type guards\n// ---------------------------------------------------------------------------\n\nexport function isCloudTreeFileRow(row: CloudTreeRow): row is CloudTreeFileRow {\n return row.kind === \"file\";\n}\n\nexport function isCloudTreeFolderRow(\n row: CloudTreeRow,\n): row is CloudTreeFolderRow {\n return row.kind === \"folder\";\n}\n\n// ---------------------------------------------------------------------------\n// 14. Asset pipeline (POST /assets and friends) — canonical wire shapes\n// ---------------------------------------------------------------------------\n//\n// One endpoint family handles every media upload in the platform:\n//\n// POST /assets multipart — upload + render\n// GET /assets/{file_id} read Asset envelope\n// PATCH /assets/{file_id} visibility / share / metadata\n// POST /assets/{file_id}/variants render more variants (idempotent)\n// GET /assets/presets preset registry\n// GET /files/{file_id}/asset click-to-render primitive\n//\n// Every endpoint above returns the same `Asset` envelope below. The\n// preset value controls which variants come back in `variants`.\n//\n// These types are hand-authored as the canonical TS surface and will\n// be replaced by the auto-generated python-derived types when that\n// regen pipeline runs. DO NOT modify `@ai-matrx/agents/generated/api-types`\n// by hand to keep them in sync — these are the source of truth until\n// the regen ships.\n\n/**\n * Preset name accepted by `POST /assets` and `POST /assets/{id}/variants`.\n * Adding a new preset on the backend is a coordinated change — extend\n * this union in lockstep.\n */\nexport type AssetPreset =\n | \"raw\"\n | \"podcast\"\n | \"social\"\n | \"web\"\n | \"email\"\n | \"logo\"\n | \"avatar\"\n | \"favicon\";\n\n/**\n * One rendered variant inside an `Asset`. Each variant is a distinct\n * `cld_files` row (so it shows up in the file tree and has its own\n * permissions) but the `Asset` envelope groups them under a single\n * `primary_key`-rooted record.\n *\n * URL fields (all durable — none expire):\n * - `url` canonical inline-renderable URL. CDN for public,\n * the durable download route for private/shared.\n * **Use this** for `<img src>` / `<video src>` /\n * `<audio src>`.\n * - `cdn_url` permanent CDN URL when public + CDN configured;\n * null otherwise.\n * - `download_url` durable URL with `Content-Disposition: attachment`\n * — forces a download dialog.\n */\nexport interface AssetVariant {\n key: string;\n file_id: string;\n file_path: string;\n width: number | null;\n height: number | null;\n mime_type: string | null;\n /**\n * File size in bytes. Renamed from `file_size` in Phase 0 (see\n * docs/PYTHON_UPDATES.md §3). Old servers may still send `file_size`\n * during the transition — consumers should fall back to the legacy\n * field name when reading older responses.\n */\n size_bytes: number | null;\n /**\n * Legacy alias for `size_bytes`. Populated by older servers during\n * the Phase 0 transition window. Read `size_bytes` first; fall back\n * to this only when re-hydrating older persisted responses.\n *\n * @deprecated Use `size_bytes`.\n */\n file_size?: number | null;\n url: string | null;\n cdn_url: string | null;\n download_url: string | null;\n metadata: Record<string, unknown>;\n}\n\n/**\n * Canonical envelope returned by every `/assets/*` and\n * `/files/{id}/asset` endpoint.\n *\n * - `file_id` master file id (the upload's \"original\" row).\n * - `primary_key` which variant the FE should render by default\n * (e.g. `\"cover_url\"` for podcast, `\"original\"` for raw).\n * - `primary_url` shorthand for `variants[primary_key].url`.\n * - `variants` always includes `\"original\"` plus zero or more\n * preset-driven derivatives.\n */\nexport interface Asset {\n file_id: string;\n visibility: Visibility;\n folder: string;\n preset: string | null;\n primary_key: string;\n primary_url: string | null;\n variants: Record<string, AssetVariant>;\n metadata: Record<string, unknown>;\n}\n\n/**\n * Request body for `POST /assets/{id}/variants`. Sends additional\n * variants for an already-uploaded file. Idempotent on `(file_id, key)`.\n */\nexport interface AddAssetVariantsRequest {\n preset?: AssetPreset;\n custom_variants?: ReadonlyArray<{\n key: string;\n suffix?: string;\n width?: number;\n height?: number;\n quality?: number;\n format?: string;\n }>;\n include_social_baseline?: boolean;\n}\n\n/**\n * Request body for `PATCH /assets/{id}`. Sparse — any field omitted is\n * left unchanged. `metadata` is MERGED server-side (matches the\n * `/files/{id}` PATCH semantics).\n */\nexport interface AssetPatchRequest {\n visibility?: Visibility;\n /** Comma-separated user IDs OR an explicit list. */\n share_with?: string | ReadonlyArray<string>;\n share_level?: PermissionLevel;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * One row in the `GET /assets/presets` response — describes a single\n * variant a preset will render.\n */\nexport interface AssetPresetVariantDescriptor {\n key: string;\n width: number | null;\n height: number | null;\n format: string | null;\n}\n\n/**\n * One preset row in the `GET /assets/presets` response.\n */\nexport interface AssetPresetDescriptor {\n name: AssetPreset;\n primary_key: string;\n include_baseline: boolean;\n variants: ReadonlyArray<AssetPresetVariantDescriptor>;\n}\n\n/**\n * Response body for `GET /assets/presets`. Drives the picker UI in\n * admin tools and lets agents enumerate every server-known preset\n * without hard-coding the list.\n */\nexport interface PresetsRegistryResponse {\n presets: ReadonlyArray<AssetPresetDescriptor>;\n social_baseline: ReadonlyArray<AssetPresetVariantDescriptor>;\n}\n"],"mappings":"AAkpBO,MAAM,0BAA0C;AAAA,EACrD,MAAM;AAAA,EACN,MAAM;AAAA,EACN,WAAW;AAAA,EACX,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,YAAY;AAAA;AAAA;AAAA;AAAA,EAIZ,SAAS;AACX;AAsmBO,SAAS,mBAAmB,KAA4C;AAC7E,SAAO,IAAI,SAAS;AACtB;AAEO,SAAS,qBACd,KAC2B;AAC3B,SAAO,IAAI,SAAS;AACtB;","names":[]}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/upload/cloudUpload.ts
|
|
3
|
+
*
|
|
4
|
+
* THE single source of truth for file uploads in this app.
|
|
5
|
+
*
|
|
6
|
+
* ════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* Why this exists
|
|
9
|
+
* ───────────────
|
|
10
|
+
* The single underlying upload primitive. Public callers go through the
|
|
11
|
+
* universal file handler (`fileHandler.upload(...)` /
|
|
12
|
+
* `useFileUpload`), which calls this. Server-side routes use
|
|
13
|
+
* `Api.Server.uploadAndShare` which wraps the same Python `/files/upload`
|
|
14
|
+
* endpoint with a server context.
|
|
15
|
+
*
|
|
16
|
+
* Rule: **every upload in the app goes through `cloudUpload` (this file)
|
|
17
|
+
* via the universal handler, OR through `Api.Server.uploadAndShare`.**
|
|
18
|
+
* Never call `supabase.from("cld_*").upsert(...)` or `ensureFolderPath`
|
|
19
|
+
* from a code path whose goal is "upload a file." The Python backend
|
|
20
|
+
* auto-creates any missing folders when you POST a full `file_path` —
|
|
21
|
+
* the browser never needs to touch `cld_folders` directly. That
|
|
22
|
+
* sidesteps the known RLS recursion bug AND keeps logic centralized.
|
|
23
|
+
*
|
|
24
|
+
* What this module owns:
|
|
25
|
+
* • Resolving a logical `file_path` from caller-supplied options.
|
|
26
|
+
* • Calling the Python `/files/upload` endpoint with progress.
|
|
27
|
+
* • Optionally creating a permanent share link.
|
|
28
|
+
* • Dispatching Redux upserts so the slice stays in sync.
|
|
29
|
+
* • Returning a uniform `{ ok: true, ... } | { ok: false, error }` shape
|
|
30
|
+
* — never null, never `[object Object]`, never silent.
|
|
31
|
+
*
|
|
32
|
+
* What this module does NOT do:
|
|
33
|
+
* • Touch `supabase.from("cld_*")` directly. Reads happen via the
|
|
34
|
+
* SECURITY DEFINER tree RPC (`get_user_file_tree`) and
|
|
35
|
+
* supabase realtime. Writes happen via the Python backend.
|
|
36
|
+
* • Block on folder lookups. The backend handles folder creation
|
|
37
|
+
* atomically as part of upload.
|
|
38
|
+
*/
|
|
39
|
+
import { type UploadProgressEvent } from "../host/python-client.js";
|
|
40
|
+
import type { AppDispatch } from "../host/store.js";
|
|
41
|
+
import type { PermissionLevel, Visibility } from "../types.js";
|
|
42
|
+
/**
|
|
43
|
+
* Files at or above this size route through the resumable TUS transport
|
|
44
|
+
* (`tusUpload.ts`); smaller files use the buffered multipart POST. Starting
|
|
45
|
+
* value 80 MB — tune only on evidence. Callers can force either transport
|
|
46
|
+
* via `CloudUploadOptions.transport`.
|
|
47
|
+
*/
|
|
48
|
+
export declare const TUS_TRANSPORT_THRESHOLD_BYTES: number;
|
|
49
|
+
export type UploadTransport = "buffered" | "tus";
|
|
50
|
+
/** The ONE place the buffered-vs-TUS decision is made. */
|
|
51
|
+
export declare function resolveUploadTransport(fileSizeBytes: number, override?: UploadTransport): UploadTransport;
|
|
52
|
+
export interface CloudUploadOptions {
|
|
53
|
+
/**
|
|
54
|
+
* Full logical file path INCLUDING the filename. Use this when you
|
|
55
|
+
* want to control the exact name (e.g. server-generated "{uuid}.jpg").
|
|
56
|
+
*/
|
|
57
|
+
filePath?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Folder path (no filename). The browser appends `file.name`
|
|
60
|
+
* automatically. The Python backend auto-creates any missing
|
|
61
|
+
* folders. This is the **preferred** option for almost every caller.
|
|
62
|
+
*
|
|
63
|
+
* Example: `folderPath: "Images/Chat"` → uploaded path becomes
|
|
64
|
+
* `"Images/Chat/<file.name>"`.
|
|
65
|
+
*/
|
|
66
|
+
folderPath?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Existing parentFolderId — only useful when you've already loaded the
|
|
69
|
+
* folder via the tree RPC or realtime. Most callers should pass
|
|
70
|
+
* `folderPath` instead so the backend handles folder creation.
|
|
71
|
+
*/
|
|
72
|
+
parentFolderId?: string | null;
|
|
73
|
+
visibility?: Visibility;
|
|
74
|
+
shareWith?: string[];
|
|
75
|
+
shareLevel?: PermissionLevel;
|
|
76
|
+
changeSummary?: string;
|
|
77
|
+
metadata?: Record<string, unknown>;
|
|
78
|
+
/** Progress callback (XHR upload progress). */
|
|
79
|
+
onProgress?: (event: UploadProgressEvent) => void;
|
|
80
|
+
signal?: AbortSignal;
|
|
81
|
+
/**
|
|
82
|
+
* Transport override. Default: `resolveUploadTransport` picks TUS for
|
|
83
|
+
* files ≥ `TUS_TRANSPORT_THRESHOLD_BYTES`, buffered otherwise.
|
|
84
|
+
*/
|
|
85
|
+
transport?: UploadTransport;
|
|
86
|
+
/**
|
|
87
|
+
* If true, also creates a permanent share link after upload. Returns
|
|
88
|
+
* the `shareUrl` (`/s/:token`) and the raw `shareToken`.
|
|
89
|
+
*/
|
|
90
|
+
createShareLink?: boolean;
|
|
91
|
+
/** Share-link permission — canonical `viewer` / `editor` levels. */
|
|
92
|
+
shareLinkPermissionLevel?: "viewer" | "editor";
|
|
93
|
+
shareLinkExpiresAt?: string | null;
|
|
94
|
+
shareLinkMaxUses?: number | null;
|
|
95
|
+
}
|
|
96
|
+
export interface CloudUploadSuccess {
|
|
97
|
+
ok: true;
|
|
98
|
+
fileId: string;
|
|
99
|
+
filePath: string;
|
|
100
|
+
fileSize: number | null;
|
|
101
|
+
versionNumber: number;
|
|
102
|
+
/** Backend-provided URL (storage URL — typically requires auth or signed). */
|
|
103
|
+
url: string | null;
|
|
104
|
+
shareToken?: string;
|
|
105
|
+
/**
|
|
106
|
+
* The PUBLIC LANDING PAGE for the share link, e.g.
|
|
107
|
+
* `https://app.example.com/s/<token>`. Renders an HTML page with
|
|
108
|
+
* file metadata and a download button. **Do NOT** use this for
|
|
109
|
+
* `<img src>`, `<video src>`, or hot-linking — the page is HTML, not
|
|
110
|
+
* the file bytes.
|
|
111
|
+
*/
|
|
112
|
+
shareUrl?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The PUBLIC DIRECT URL for the file bytes — points at Python's
|
|
115
|
+
* `{BACKEND}/share/{token}` endpoint, which serves inline-safe bytes (and may
|
|
116
|
+
* 302-redirect public files to their permanent CDN URL). Embed it in
|
|
117
|
+
* `<img src>`, Slack/Notion unfurls, or OG images. No Next.js hop.
|
|
118
|
+
*
|
|
119
|
+
* Populated whenever `shareUrl` is — they always go in pairs because
|
|
120
|
+
* both are derived from the same share token.
|
|
121
|
+
*/
|
|
122
|
+
directUrl?: string;
|
|
123
|
+
}
|
|
124
|
+
export interface CloudUploadFailure {
|
|
125
|
+
ok: false;
|
|
126
|
+
error: string;
|
|
127
|
+
/** Backend error code if available (e.g. "auth_required", "file_too_large"). */
|
|
128
|
+
errorCode?: string;
|
|
129
|
+
/** Filename for caller reference. */
|
|
130
|
+
fileName: string;
|
|
131
|
+
}
|
|
132
|
+
export type CloudUploadResult = CloudUploadSuccess | CloudUploadFailure;
|
|
133
|
+
/**
|
|
134
|
+
* Type guard — when `result.ok` discriminator narrowing isn't enough for
|
|
135
|
+
* TS (e.g. inside loops or where the union is inferred indirectly),
|
|
136
|
+
* call this and the compiler will narrow the branches correctly.
|
|
137
|
+
*/
|
|
138
|
+
export declare function isCloudUploadFailure(result: CloudUploadResult): result is CloudUploadFailure;
|
|
139
|
+
export declare function isCloudUploadSuccess(result: CloudUploadResult): result is CloudUploadSuccess;
|
|
140
|
+
/** Read an organization a caller already declared, in either accepted shape. */
|
|
141
|
+
export declare function organizationIdFromUploadMetadata(metadata: Record<string, unknown> | undefined): string | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Pure upload — POSTs to /files/upload with a full `file_path`. Backend
|
|
144
|
+
* auto-creates folders. Use this when you need raw access without Redux
|
|
145
|
+
* dispatches (e.g. server-side route handlers, isolated tests).
|
|
146
|
+
*
|
|
147
|
+
* For browser code that wants the file to appear in the UI, use
|
|
148
|
+
* `cloudUpload` (with dispatch) instead.
|
|
149
|
+
*/
|
|
150
|
+
export declare function cloudUploadRaw(file: File, rawOptions?: CloudUploadOptions): Promise<CloudUploadResult>;
|
|
151
|
+
/**
|
|
152
|
+
* Upload a single file. THIS is the function 99% of callers want.
|
|
153
|
+
*
|
|
154
|
+
* Side effects:
|
|
155
|
+
* • Dispatches `trackUploadStart`/`updateUploadProgress`/`updateUploadStatus`
|
|
156
|
+
* so progress bars in the UI stay in sync.
|
|
157
|
+
* • Dispatches `upsertFile` on success so the file appears in the slice
|
|
158
|
+
* immediately (no need to wait for a tree refresh).
|
|
159
|
+
* • Dispatches `attachChildToFolder` if `parentFolderId` is known.
|
|
160
|
+
*
|
|
161
|
+
* Returns the unified `CloudUploadResult`. Never throws — errors come
|
|
162
|
+
* back as `{ ok: false, error }` so callers always have a clear path.
|
|
163
|
+
*/
|
|
164
|
+
export declare function cloudUpload(file: File, rawOptions: CloudUploadOptions, dispatch: AppDispatch): Promise<CloudUploadResult>;
|
|
165
|
+
export interface CloudUploadManyOptions extends CloudUploadOptions {
|
|
166
|
+
/** Parallel ceiling. Defaults to 3. */
|
|
167
|
+
concurrency?: number;
|
|
168
|
+
}
|
|
169
|
+
export interface CloudUploadManyResult {
|
|
170
|
+
successes: CloudUploadSuccess[];
|
|
171
|
+
failures: CloudUploadFailure[];
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Upload multiple files with bounded concurrency. Returns a structured
|
|
175
|
+
* result so callers can show "3 of 5 uploaded" UI cleanly.
|
|
176
|
+
*/
|
|
177
|
+
export declare function cloudUploadMany(files: File[], options: CloudUploadManyOptions, dispatch: AppDispatch): Promise<CloudUploadManyResult>;
|
|
178
|
+
/**
|
|
179
|
+
* Imperative shortcut for non-React code that still wants Redux side
|
|
180
|
+
* effects. Pulls the dispatch from the store singleton.
|
|
181
|
+
*/
|
|
182
|
+
export declare function cloudUploadImperative(file: File, options: CloudUploadOptions): Promise<CloudUploadResult>;
|