@ai-matrx/media 0.8.0 → 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 +15 -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/package.json +393 -9
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/api/office.ts
|
|
3
|
+
*
|
|
4
|
+
* Office (docx / pptx / xlsx) server-side read endpoints on aidream — the
|
|
5
|
+
* matrx-files Office codec exposed over HTTP. Powers the OfficePreview
|
|
6
|
+
* previewer (extract → markdown) and the "Convert to PDF" file action.
|
|
7
|
+
*
|
|
8
|
+
* Backend: aidream/api/routers/office_generation.py (`/office/*`).
|
|
9
|
+
*/
|
|
10
|
+
import type { components } from "@ai-matrx/agents/generated/api-types";
|
|
11
|
+
/** Markdown view of an existing Office file (whole doc + per-portion). */
|
|
12
|
+
export type OfficeExtraction = components["schemas"]["OfficeExtractionResponse"];
|
|
13
|
+
/** One slide / sheet / section of an extracted document. */
|
|
14
|
+
export type OfficePortion = components["schemas"]["OfficePortionOut"];
|
|
15
|
+
/** FileRef for a generated / converted Office asset. */
|
|
16
|
+
export type OfficeFileRef = components["schemas"]["OfficeGenerationResponse"];
|
|
17
|
+
/**
|
|
18
|
+
* Extract an Office file the caller can read into markdown + portions.
|
|
19
|
+
* The server does the parsing (incl. the LibreOffice legacy bridge for
|
|
20
|
+
* .doc/.ppt) — the bytes never reach the browser.
|
|
21
|
+
*/
|
|
22
|
+
export declare function extractOfficeMarkdown(fileId: string): Promise<OfficeExtraction>;
|
|
23
|
+
/**
|
|
24
|
+
* Render an Office file to PDF server-side (LibreOffice). Returns the NEW
|
|
25
|
+
* pdf file's FileRef — callers typically navigate to `/files/f/{file_id}`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function convertOfficeToPdf(fileId: string): Promise<OfficeFileRef>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { apiGet, apiPost, buildPath } from "../host/typed-client.js";
|
|
2
|
+
async function extractOfficeMarkdown(fileId) {
|
|
3
|
+
const { data } = await apiGet(
|
|
4
|
+
buildPath("/office/{file_id}/markdown", { file_id: fileId })
|
|
5
|
+
);
|
|
6
|
+
return data;
|
|
7
|
+
}
|
|
8
|
+
async function convertOfficeToPdf(fileId) {
|
|
9
|
+
const { data } = await apiPost(
|
|
10
|
+
buildPath("/office/{file_id}/convert", { file_id: fileId }),
|
|
11
|
+
{ target: "pdf" }
|
|
12
|
+
);
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
convertOfficeToPdf,
|
|
17
|
+
extractOfficeMarkdown
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=office.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/api/office.ts"],"sourcesContent":["/**\n * features/files/api/office.ts\n *\n * Office (docx / pptx / xlsx) server-side read endpoints on aidream — the\n * matrx-files Office codec exposed over HTTP. Powers the OfficePreview\n * previewer (extract → markdown) and the \"Convert to PDF\" file action.\n *\n * Backend: aidream/api/routers/office_generation.py (`/office/*`).\n */\n\nimport { apiGet, apiPost, buildPath } from \"../host/typed-client\";\nimport type { components } from \"@ai-matrx/agents/generated/api-types\";\n\n/** Markdown view of an existing Office file (whole doc + per-portion). */\nexport type OfficeExtraction =\n components[\"schemas\"][\"OfficeExtractionResponse\"];\n/** One slide / sheet / section of an extracted document. */\nexport type OfficePortion = components[\"schemas\"][\"OfficePortionOut\"];\n/** FileRef for a generated / converted Office asset. */\nexport type OfficeFileRef = components[\"schemas\"][\"OfficeGenerationResponse\"];\n\n/**\n * Extract an Office file the caller can read into markdown + portions.\n * The server does the parsing (incl. the LibreOffice legacy bridge for\n * .doc/.ppt) — the bytes never reach the browser.\n */\nexport async function extractOfficeMarkdown(\n fileId: string,\n): Promise<OfficeExtraction> {\n const { data } = await apiGet(\n buildPath(\"/office/{file_id}/markdown\", { file_id: fileId }),\n );\n return data;\n}\n\n/**\n * Render an Office file to PDF server-side (LibreOffice). Returns the NEW\n * pdf file's FileRef — callers typically navigate to `/files/f/{file_id}`.\n */\nexport async function convertOfficeToPdf(\n fileId: string,\n): Promise<OfficeFileRef> {\n const { data } = await apiPost(\n buildPath(\"/office/{file_id}/convert\", { file_id: fileId }),\n { target: \"pdf\" },\n );\n return data;\n}\n"],"mappings":"AAUA,SAAS,QAAQ,SAAS,iBAAiB;AAgB3C,eAAsB,sBACpB,QAC2B;AAC3B,QAAM,EAAE,KAAK,IAAI,MAAM;AAAA,IACrB,UAAU,8BAA8B,EAAE,SAAS,OAAO,CAAC;AAAA,EAC7D;AACA,SAAO;AACT;AAMA,eAAsB,mBACpB,QACwB;AACxB,QAAM,EAAE,KAAK,IAAI,MAAM;AAAA,IACrB,UAAU,6BAA6B,EAAE,SAAS,OAAO,CAAC;AAAA,IAC1D,EAAE,QAAQ,MAAM;AAAA,EAClB;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/api/permissions.ts
|
|
3
|
+
*
|
|
4
|
+
* Grant / revoke permissions on files and folders. Direct-to-Supabase:
|
|
5
|
+
* `iam.fn_list_resource_permissions` / `fn_grant_resource_permission` /
|
|
6
|
+
* `fn_revoke_resource_permission` mirror the retired Python
|
|
7
|
+
* `PermissionsManager` exactly — every call requires 'admin' access on the
|
|
8
|
+
* resource via `iam.has_access(type, id, 'admin')`, the same auth.uid()-based
|
|
9
|
+
* resolver RLS itself calls. Identity from auth.uid() only.
|
|
10
|
+
*/
|
|
11
|
+
import type { GrantPermissionRequest, IamResourcePermissionRpcRow } from "../types.js";
|
|
12
|
+
export declare function listFilePermissions(fileId: string, _opts?: unknown): Promise<{
|
|
13
|
+
data: IamResourcePermissionRpcRow[];
|
|
14
|
+
}>;
|
|
15
|
+
export declare function grantFilePermission(fileId: string, body: GrantPermissionRequest, _opts?: unknown): Promise<{
|
|
16
|
+
data: IamResourcePermissionRpcRow;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function revokeFilePermission(fileId: string, granteeId: string, params?: {
|
|
19
|
+
granteeType?: "user" | "group";
|
|
20
|
+
}, _opts?: unknown): Promise<{
|
|
21
|
+
data: {
|
|
22
|
+
deleted: boolean;
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
25
|
+
export declare function listFolderPermissions(folderId: string, _opts?: unknown): Promise<{
|
|
26
|
+
data: IamResourcePermissionRpcRow[];
|
|
27
|
+
}>;
|
|
28
|
+
export declare function grantFolderPermission(folderId: string, body: GrantPermissionRequest, _opts?: unknown): Promise<{
|
|
29
|
+
data: IamResourcePermissionRpcRow;
|
|
30
|
+
}>;
|
|
31
|
+
export declare function revokeFolderPermission(folderId: string, granteeId: string, params?: {
|
|
32
|
+
granteeType?: "user" | "group";
|
|
33
|
+
}, _opts?: unknown): Promise<{
|
|
34
|
+
data: {
|
|
35
|
+
deleted: boolean;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createClient } from "../host/supabase.js";
|
|
2
|
+
import { iamDb } from "../host/supabase.js";
|
|
3
|
+
const SHARING_UNAVAILABLE = {
|
|
4
|
+
file: {
|
|
5
|
+
list: "We couldn't load who this file is shared with. You may not be allowed to manage its sharing.",
|
|
6
|
+
grant: "We couldn't share this file. You may not be allowed to manage its sharing.",
|
|
7
|
+
revoke: "We couldn't remove that person's access to this file. You may not be allowed to manage its sharing."
|
|
8
|
+
},
|
|
9
|
+
folder: {
|
|
10
|
+
list: "We couldn't load who this folder is shared with. You may not be allowed to manage its sharing.",
|
|
11
|
+
grant: "We couldn't share this folder. You may not be allowed to manage its sharing.",
|
|
12
|
+
revoke: "We couldn't remove that person's access to this folder. You may not be allowed to manage its sharing."
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function parsePermissionRpcRows(data) {
|
|
16
|
+
if (!Array.isArray(data)) return [];
|
|
17
|
+
return data;
|
|
18
|
+
}
|
|
19
|
+
function parsePermissionRpcRow(data) {
|
|
20
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
21
|
+
throw new Error("fn_grant_resource_permission returned an invalid row");
|
|
22
|
+
}
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
async function listPermissions(resourceType, resourceId) {
|
|
26
|
+
const supabase = createClient();
|
|
27
|
+
const { data, error } = await iamDb(supabase).rpc(
|
|
28
|
+
"fn_list_resource_permissions",
|
|
29
|
+
{
|
|
30
|
+
p_resource_type: resourceType,
|
|
31
|
+
p_resource_id: resourceId
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
if (error) throw new Error(SHARING_UNAVAILABLE[resourceType].list);
|
|
35
|
+
return parsePermissionRpcRows(data);
|
|
36
|
+
}
|
|
37
|
+
async function grantPermission(resourceType, resourceId, body) {
|
|
38
|
+
const supabase = createClient();
|
|
39
|
+
const { data, error } = await iamDb(supabase).rpc(
|
|
40
|
+
"fn_grant_resource_permission",
|
|
41
|
+
{
|
|
42
|
+
p_resource_type: resourceType,
|
|
43
|
+
p_resource_id: resourceId,
|
|
44
|
+
p_grantee_id: body.grantee_id,
|
|
45
|
+
p_grantee_type: body.grantee_type ?? "user",
|
|
46
|
+
p_level: body.level ?? "read",
|
|
47
|
+
p_expires_at: body.expires_at ?? void 0
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
if (error) throw new Error(SHARING_UNAVAILABLE[resourceType].grant);
|
|
51
|
+
return parsePermissionRpcRow(data);
|
|
52
|
+
}
|
|
53
|
+
async function revokePermission(resourceType, resourceId, granteeId, granteeType = "user") {
|
|
54
|
+
const supabase = createClient();
|
|
55
|
+
const { data, error } = await iamDb(supabase).rpc(
|
|
56
|
+
"fn_revoke_resource_permission",
|
|
57
|
+
{
|
|
58
|
+
p_resource_type: resourceType,
|
|
59
|
+
p_resource_id: resourceId,
|
|
60
|
+
p_grantee_id: granteeId,
|
|
61
|
+
// The user-group ACL tier is removed DB-side; 'group' has no meaningful
|
|
62
|
+
// target column, so it's treated the same as the default 'user' grantee.
|
|
63
|
+
p_grantee_type: granteeType === "group" ? "user" : granteeType
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
if (error) throw new Error(SHARING_UNAVAILABLE[resourceType].revoke);
|
|
67
|
+
return { deleted: Boolean(data) };
|
|
68
|
+
}
|
|
69
|
+
async function listFilePermissions(fileId, _opts) {
|
|
70
|
+
return { data: await listPermissions("file", fileId) };
|
|
71
|
+
}
|
|
72
|
+
async function grantFilePermission(fileId, body, _opts) {
|
|
73
|
+
return { data: await grantPermission("file", fileId, body) };
|
|
74
|
+
}
|
|
75
|
+
async function revokeFilePermission(fileId, granteeId, params = {}, _opts) {
|
|
76
|
+
return {
|
|
77
|
+
data: await revokePermission(
|
|
78
|
+
"file",
|
|
79
|
+
fileId,
|
|
80
|
+
granteeId,
|
|
81
|
+
params.granteeType ?? "user"
|
|
82
|
+
)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function listFolderPermissions(folderId, _opts) {
|
|
86
|
+
return { data: await listPermissions("folder", folderId) };
|
|
87
|
+
}
|
|
88
|
+
async function grantFolderPermission(folderId, body, _opts) {
|
|
89
|
+
return { data: await grantPermission("folder", folderId, body) };
|
|
90
|
+
}
|
|
91
|
+
async function revokeFolderPermission(folderId, granteeId, params = {}, _opts) {
|
|
92
|
+
return {
|
|
93
|
+
data: await revokePermission(
|
|
94
|
+
"folder",
|
|
95
|
+
folderId,
|
|
96
|
+
granteeId,
|
|
97
|
+
params.granteeType ?? "user"
|
|
98
|
+
)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
grantFilePermission,
|
|
103
|
+
grantFolderPermission,
|
|
104
|
+
listFilePermissions,
|
|
105
|
+
listFolderPermissions,
|
|
106
|
+
revokeFilePermission,
|
|
107
|
+
revokeFolderPermission
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=permissions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/api/permissions.ts"],"sourcesContent":["/**\n * features/files/api/permissions.ts\n *\n * Grant / revoke permissions on files and folders. Direct-to-Supabase:\n * `iam.fn_list_resource_permissions` / `fn_grant_resource_permission` /\n * `fn_revoke_resource_permission` mirror the retired Python\n * `PermissionsManager` exactly — every call requires 'admin' access on the\n * resource via `iam.has_access(type, id, 'admin')`, the same auth.uid()-based\n * resolver RLS itself calls. Identity from auth.uid() only.\n */\n\nimport { createClient } from \"../host/supabase\";\nimport { iamDb } from \"../host/supabase\";\nimport type {\n GrantPermissionRequest,\n IamResourcePermissionRpcRow,\n} from \"../types\";\n\n/**\n * What the user reads when one of the three RPCs fails. The driver's own text\n * (\"permission denied for schema iam\", \"PGRST202 …\") is never shown: the RPC\n * refuses unless the caller has 'admin' on the resource, so the only honest\n * thing to say is that the sharing settings didn't load or the change didn't\n * stick. The raw PostgREST error is still captured in full by the client-wide\n * proxy in `lib/diagnostics/supabaseErrorCapture.ts`, so nothing is lost — it\n * just lands in the Error Inspector instead of in front of a person.\n */\nconst SHARING_UNAVAILABLE = {\n file: {\n list: \"We couldn't load who this file is shared with. You may not be allowed to manage its sharing.\",\n grant: \"We couldn't share this file. You may not be allowed to manage its sharing.\",\n revoke: \"We couldn't remove that person's access to this file. You may not be allowed to manage its sharing.\",\n },\n folder: {\n list: \"We couldn't load who this folder is shared with. You may not be allowed to manage its sharing.\",\n grant: \"We couldn't share this folder. You may not be allowed to manage its sharing.\",\n revoke: \"We couldn't remove that person's access to this folder. You may not be allowed to manage its sharing.\",\n },\n} as const;\n\nfunction parsePermissionRpcRows(data: unknown): IamResourcePermissionRpcRow[] {\n if (!Array.isArray(data)) return [];\n return data as unknown as IamResourcePermissionRpcRow[];\n}\n\nfunction parsePermissionRpcRow(data: unknown): IamResourcePermissionRpcRow {\n if (!data || typeof data !== \"object\" || Array.isArray(data)) {\n throw new Error(\"fn_grant_resource_permission returned an invalid row\");\n }\n return data as unknown as IamResourcePermissionRpcRow;\n}\n\nasync function listPermissions(\n resourceType: \"file\" | \"folder\",\n resourceId: string,\n): Promise<IamResourcePermissionRpcRow[]> {\n const supabase = createClient();\n const { data, error } = await iamDb(supabase).rpc(\n \"fn_list_resource_permissions\",\n {\n p_resource_type: resourceType,\n p_resource_id: resourceId,\n },\n );\n if (error) throw new Error(SHARING_UNAVAILABLE[resourceType].list);\n return parsePermissionRpcRows(data);\n}\n\nasync function grantPermission(\n resourceType: \"file\" | \"folder\",\n resourceId: string,\n body: GrantPermissionRequest,\n): Promise<IamResourcePermissionRpcRow> {\n const supabase = createClient();\n const { data, error } = await iamDb(supabase).rpc(\n \"fn_grant_resource_permission\",\n {\n p_resource_type: resourceType,\n p_resource_id: resourceId,\n p_grantee_id: body.grantee_id,\n p_grantee_type: body.grantee_type ?? \"user\",\n p_level: body.level ?? \"read\",\n p_expires_at: body.expires_at ?? undefined,\n },\n );\n if (error) throw new Error(SHARING_UNAVAILABLE[resourceType].grant);\n return parsePermissionRpcRow(data);\n}\n\nasync function revokePermission(\n resourceType: \"file\" | \"folder\",\n resourceId: string,\n granteeId: string,\n granteeType: \"user\" | \"group\" = \"user\",\n): Promise<{ deleted: boolean }> {\n const supabase = createClient();\n const { data, error } = await iamDb(supabase).rpc(\n \"fn_revoke_resource_permission\",\n {\n p_resource_type: resourceType,\n p_resource_id: resourceId,\n p_grantee_id: granteeId,\n // The user-group ACL tier is removed DB-side; 'group' has no meaningful\n // target column, so it's treated the same as the default 'user' grantee.\n p_grantee_type: granteeType === \"group\" ? \"user\" : granteeType,\n },\n );\n if (error) throw new Error(SHARING_UNAVAILABLE[resourceType].revoke);\n return { deleted: Boolean(data) };\n}\n\n// ---------------------------------------------------------------------------\n// File permissions\n// ---------------------------------------------------------------------------\n\n// `opts` is accepted-and-ignored on every export below — callers still pass\n// the old python-client `{ requestId }` shape (features/files/redux/thunks.ts\n// uses it for its own request ledger, not the network call itself), and\n// dropping the parameter would force every call site to change for no\n// behavioral gain. It plays no role in a direct-Supabase call.\n\nexport async function listFilePermissions(\n fileId: string,\n _opts?: unknown,\n): Promise<{ data: IamResourcePermissionRpcRow[] }> {\n return { data: await listPermissions(\"file\", fileId) };\n}\n\nexport async function grantFilePermission(\n fileId: string,\n body: GrantPermissionRequest,\n _opts?: unknown,\n): Promise<{ data: IamResourcePermissionRpcRow }> {\n return { data: await grantPermission(\"file\", fileId, body) };\n}\n\nexport async function revokeFilePermission(\n fileId: string,\n granteeId: string,\n params: { granteeType?: \"user\" | \"group\" } = {},\n _opts?: unknown,\n): Promise<{ data: { deleted: boolean } }> {\n return {\n data: await revokePermission(\n \"file\",\n fileId,\n granteeId,\n params.granteeType ?? \"user\",\n ),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Folder permissions (cascade to contents)\n// ---------------------------------------------------------------------------\n\nexport async function listFolderPermissions(\n folderId: string,\n _opts?: unknown,\n): Promise<{ data: IamResourcePermissionRpcRow[] }> {\n return { data: await listPermissions(\"folder\", folderId) };\n}\n\nexport async function grantFolderPermission(\n folderId: string,\n body: GrantPermissionRequest,\n _opts?: unknown,\n): Promise<{ data: IamResourcePermissionRpcRow }> {\n return { data: await grantPermission(\"folder\", folderId, body) };\n}\n\nexport async function revokeFolderPermission(\n folderId: string,\n granteeId: string,\n params: { granteeType?: \"user\" | \"group\" } = {},\n _opts?: unknown,\n): Promise<{ data: { deleted: boolean } }> {\n return {\n data: await revokePermission(\n \"folder\",\n folderId,\n granteeId,\n params.granteeType ?? \"user\",\n ),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/api/versions.ts
|
|
3
|
+
*
|
|
4
|
+
* File-version endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Backend contract: features/files/cld_files_frontend.md §6 (Versions).
|
|
7
|
+
*/
|
|
8
|
+
import { type RequestOptions, type ResponseMeta } from "../host/python-client.js";
|
|
9
|
+
import type { CloudFileVersionRow, FileRecordApi } from "../types.js";
|
|
10
|
+
export declare function listVersions(fileId: string, opts?: RequestOptions): Promise<{
|
|
11
|
+
data: CloudFileVersionRow[];
|
|
12
|
+
meta: ResponseMeta;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function getVersion(fileId: string, versionNumber: number, opts?: RequestOptions): Promise<{
|
|
15
|
+
data: CloudFileVersionRow;
|
|
16
|
+
meta: ResponseMeta;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function downloadVersion(fileId: string, versionNumber: number, opts?: RequestOptions): Promise<{
|
|
19
|
+
blob: Blob;
|
|
20
|
+
filename: string | null;
|
|
21
|
+
meta: ResponseMeta;
|
|
22
|
+
}>;
|
|
23
|
+
export declare function restoreVersion(fileId: string, versionNumber: number, opts?: RequestOptions): Promise<{
|
|
24
|
+
data: FileRecordApi;
|
|
25
|
+
meta: ResponseMeta;
|
|
26
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {
|
|
2
|
+
downloadBlob,
|
|
3
|
+
getJson,
|
|
4
|
+
postJson
|
|
5
|
+
} from "../host/python-client.js";
|
|
6
|
+
async function listVersions(fileId, opts = {}) {
|
|
7
|
+
return getJson(
|
|
8
|
+
`/files/${fileId}/versions`,
|
|
9
|
+
opts
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
async function getVersion(fileId, versionNumber, opts = {}) {
|
|
13
|
+
return getJson(
|
|
14
|
+
`/files/${fileId}/versions/${versionNumber}`,
|
|
15
|
+
opts
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
async function downloadVersion(fileId, versionNumber, opts = {}) {
|
|
19
|
+
return downloadBlob(
|
|
20
|
+
`/files/${fileId}/versions/${versionNumber}/download`,
|
|
21
|
+
opts
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
async function restoreVersion(fileId, versionNumber, opts = {}) {
|
|
25
|
+
return postJson(
|
|
26
|
+
`/files/${fileId}/versions/${versionNumber}/restore`,
|
|
27
|
+
{},
|
|
28
|
+
opts
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
downloadVersion,
|
|
33
|
+
getVersion,
|
|
34
|
+
listVersions,
|
|
35
|
+
restoreVersion
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=versions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/api/versions.ts"],"sourcesContent":["/**\n * features/files/api/versions.ts\n *\n * File-version endpoints.\n *\n * Backend contract: features/files/cld_files_frontend.md §6 (Versions).\n */\n\nimport {\n downloadBlob,\n getJson,\n postJson,\n type RequestOptions,\n type ResponseMeta,\n} from \"../host/python-client\";\nimport type { CloudFileVersionRow, FileRecordApi } from \"../types\";\n\n// list/get stay on the raw client: the contract response is `FileVersionRecord`\n// (all-optional + index-signature), whereas consumers read the concrete DB-row\n// `CloudFileVersionRow`. Binding would erase the row's concrete fields.\nexport async function listVersions(\n fileId: string,\n opts: RequestOptions = {},\n): Promise<{ data: CloudFileVersionRow[]; meta: ResponseMeta }> {\n return getJson<CloudFileVersionRow[]>(\n `/files/${fileId}/versions`,\n opts,\n );\n}\n\nexport async function getVersion(\n fileId: string,\n versionNumber: number,\n opts: RequestOptions = {},\n): Promise<{ data: CloudFileVersionRow; meta: ResponseMeta }> {\n return getJson<CloudFileVersionRow>(\n `/files/${fileId}/versions/${versionNumber}`,\n opts,\n );\n}\n\n// Binary download (blob bytes, not JSON) — stays on the raw `downloadBlob` helper;\n// the typed client only covers JSON responses.\nexport async function downloadVersion(\n fileId: string,\n versionNumber: number,\n opts: RequestOptions = {},\n): Promise<{ blob: Blob; filename: string | null; meta: ResponseMeta }> {\n return downloadBlob(\n `/files/${fileId}/versions/${versionNumber}/download`,\n opts,\n );\n}\n\nexport async function restoreVersion(\n fileId: string,\n versionNumber: number,\n opts: RequestOptions = {},\n): Promise<{ data: FileRecordApi; meta: ResponseMeta }> {\n // Stays on the raw client: the contract declares NO request body for this\n // operation, but the server accepts (and this call sends) an empty `{}` — routing\n // through `apiPost` would type the body as `undefined` and could change the wire.\n // The response type is already contract-bound: `FileRecordApi` is an alias of\n // `components[\"schemas\"][\"FileRecord\"]`, exactly what the operation returns.\n return postJson<FileRecordApi, Record<string, never>>(\n `/files/${fileId}/versions/${versionNumber}/restore`,\n {},\n opts,\n );\n}\n"],"mappings":"AAQA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAMP,eAAsB,aACpB,QACA,OAAuB,CAAC,GACsC;AAC9D,SAAO;AAAA,IACL,UAAU,MAAM;AAAA,IAChB;AAAA,EACF;AACF;AAEA,eAAsB,WACpB,QACA,eACA,OAAuB,CAAC,GACoC;AAC5D,SAAO;AAAA,IACL,UAAU,MAAM,aAAa,aAAa;AAAA,IAC1C;AAAA,EACF;AACF;AAIA,eAAsB,gBACpB,QACA,eACA,OAAuB,CAAC,GAC8C;AACtE,SAAO;AAAA,IACL,UAAU,MAAM,aAAa,aAAa;AAAA,IAC1C;AAAA,EACF;AACF;AAEA,eAAsB,eACpB,QACA,eACA,OAAuB,CAAC,GAC8B;AAMtD,SAAO;AAAA,IACL,UAAU,MAAM,aAAa,aAAa;AAAA,IAC1C,CAAC;AAAA,IACD;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/cache/idb-store.ts
|
|
3
|
+
*
|
|
4
|
+
* IndexedDB tier of the 3-tier byte cache (memory LRU → IDB → network).
|
|
5
|
+
*
|
|
6
|
+
* Dexie wrapper for the `matrx-blob-cache` database — a SEPARATE DB from
|
|
7
|
+
* `matrx-sync` so cache eviction can never threaten sync persistence.
|
|
8
|
+
* Schema is intentionally simple: one `blobs` table keyed by a string
|
|
9
|
+
* cache key, indexed by `userId` (for sign-out purge) and
|
|
10
|
+
* `lastAccessedAt` (for LRU eviction).
|
|
11
|
+
*
|
|
12
|
+
* Cache key strategy (see plan §4A):
|
|
13
|
+
* - Owned files (current version): `${userId}:${fileId}:current:${checksum}`
|
|
14
|
+
* - Owned files (pinned version): `${userId}:${fileId}:${N}:${checksum}`
|
|
15
|
+
* - URL-keyed entries (share-link bytes, etc.): `${userId}:url:${sha256(url)}`
|
|
16
|
+
*
|
|
17
|
+
* Signed S3 URLs (either AWS dialect — SigV4 `X-Amz-*` or SigV2
|
|
18
|
+
* `AWSAccessKeyId`/`Signature`/`Expires`) are NEVER cached because they
|
|
19
|
+
* expire — caching them would create stale-URL bugs.
|
|
20
|
+
*
|
|
21
|
+
* Failure modes:
|
|
22
|
+
* - Private browsing / IDB disabled / quota exceeded: `openBlobCacheDb()`
|
|
23
|
+
* rejects → all reads return null, all writes resolve as no-ops. The
|
|
24
|
+
* in-memory LRU + network stay functional.
|
|
25
|
+
*
|
|
26
|
+
* Identity scoping:
|
|
27
|
+
* - Every entry is stamped with `userId`. Sign-out calls `clearForUser`
|
|
28
|
+
* so cross-user contamination is structurally impossible.
|
|
29
|
+
*
|
|
30
|
+
* Budget:
|
|
31
|
+
* - Default 2 GB total. Eviction is size-aware-LRU: oldest entries above
|
|
32
|
+
* a 2 MB floor are evicted first; tiny entries (<2 MB) only evict when
|
|
33
|
+
* the cache is otherwise full of small entries.
|
|
34
|
+
* - QuotaExceededError triggers an immediate eviction down to 70% of
|
|
35
|
+
* budget and a single retry. Subsequent failures fall back to network.
|
|
36
|
+
*/
|
|
37
|
+
import Dexie, { type Table } from "dexie";
|
|
38
|
+
export declare const BLOB_CACHE_DB_NAME = "matrx-blob-cache";
|
|
39
|
+
export declare const BLOB_CACHE_SCHEMA_VERSION = 1;
|
|
40
|
+
export declare const DEFAULT_BUDGET_BYTES: number;
|
|
41
|
+
export type BlobCacheSource = "files-download" | "share" | "cdn" | "url";
|
|
42
|
+
export interface BlobCacheEntry {
|
|
43
|
+
/** Composite cache key — see header comment for the key shape. */
|
|
44
|
+
key: string;
|
|
45
|
+
userId: string;
|
|
46
|
+
/** Null for url-keyed entries (share links / arbitrary URLs). */
|
|
47
|
+
fileId: string | null;
|
|
48
|
+
version: number | null;
|
|
49
|
+
checksum: string | null;
|
|
50
|
+
mimeType: string;
|
|
51
|
+
bytes: number;
|
|
52
|
+
blob: Blob;
|
|
53
|
+
etag: string | null;
|
|
54
|
+
fetchedAt: number;
|
|
55
|
+
lastAccessedAt: number;
|
|
56
|
+
source: BlobCacheSource;
|
|
57
|
+
}
|
|
58
|
+
interface BlobCacheDb extends Dexie {
|
|
59
|
+
blobs: Table<BlobCacheEntry, string>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Open the Dexie blob-cache DB. Lazy + memoized — the server-render pass
|
|
63
|
+
* never touches IDB. Returns `null` when IDB is unavailable (private
|
|
64
|
+
* browsing, server-side, quota exhausted at open time); callers should
|
|
65
|
+
* treat that as "no persistent tier" and fall through to network.
|
|
66
|
+
*/
|
|
67
|
+
export declare function openBlobCacheDb(): Promise<BlobCacheDb | null>;
|
|
68
|
+
export declare function getEntry(key: string): Promise<BlobCacheEntry | null>;
|
|
69
|
+
export declare function putEntry(entry: BlobCacheEntry): Promise<void>;
|
|
70
|
+
export declare function deleteEntry(key: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Drop every entry whose key starts with `${userId}:${fileId}:`. Use on
|
|
73
|
+
* realtime version-bump / hard-delete so every cached version of a file
|
|
74
|
+
* gets evicted regardless of the version segment in the key.
|
|
75
|
+
*/
|
|
76
|
+
export declare function deleteEntriesForFile(userId: string, fileId: string): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Drop every entry for the signed-out user. Called from the sign-out
|
|
79
|
+
* code path so the next user can never read the previous user's blobs.
|
|
80
|
+
*/
|
|
81
|
+
export declare function clearForUser(userId: string): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Test/debug helper. Returns `{ entryCount, totalBytes }`. The admin
|
|
84
|
+
* observability panel reads this.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getStats(): Promise<{
|
|
87
|
+
entryCount: number;
|
|
88
|
+
totalBytes: number;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Public LRU evict for a given user — exposed so the admin observability
|
|
92
|
+
* panel can offer an "Evict now" button.
|
|
93
|
+
*/
|
|
94
|
+
export declare function evictForUser(userId: string, targetBytes: number): Promise<void>;
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import Dexie from "dexie";
|
|
2
|
+
import { logger } from "../support/logger.js";
|
|
3
|
+
import { extractErrorMessage } from "@ai-matrx/data/net";
|
|
4
|
+
const BLOB_CACHE_DB_NAME = "matrx-blob-cache";
|
|
5
|
+
const BLOB_CACHE_SCHEMA_VERSION = 1;
|
|
6
|
+
const DEFAULT_BUDGET_BYTES = 2 * 1024 * 1024 * 1024;
|
|
7
|
+
const SIZE_FLOOR_BYTES = 2 * 1024 * 1024;
|
|
8
|
+
function hasIndexedDb() {
|
|
9
|
+
if (typeof globalThis === "undefined") return false;
|
|
10
|
+
return typeof globalThis.indexedDB !== "undefined";
|
|
11
|
+
}
|
|
12
|
+
let dbPromise = null;
|
|
13
|
+
function openBlobCacheDb() {
|
|
14
|
+
if (dbPromise) return dbPromise;
|
|
15
|
+
dbPromise = (async () => {
|
|
16
|
+
if (!hasIndexedDb()) {
|
|
17
|
+
logger.info("blob-cache.unavailable", {
|
|
18
|
+
meta: { reason: "no-indexedDB" }
|
|
19
|
+
});
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const db = new Dexie(BLOB_CACHE_DB_NAME);
|
|
24
|
+
db.version(BLOB_CACHE_SCHEMA_VERSION).stores({
|
|
25
|
+
// Primary key `key`; secondary indexes for purge + eviction.
|
|
26
|
+
blobs: "key, userId, lastAccessedAt, bytes"
|
|
27
|
+
});
|
|
28
|
+
await db.open();
|
|
29
|
+
logger.info("blob-cache.open.success", {
|
|
30
|
+
meta: { schemaVersion: BLOB_CACHE_SCHEMA_VERSION }
|
|
31
|
+
});
|
|
32
|
+
return db;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
logger.warn("blob-cache.open.error", {
|
|
35
|
+
meta: { error: extractErrorMessage(err) }
|
|
36
|
+
});
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
return dbPromise;
|
|
41
|
+
}
|
|
42
|
+
async function getEntry(key) {
|
|
43
|
+
const db = await openBlobCacheDb();
|
|
44
|
+
if (!db) return null;
|
|
45
|
+
try {
|
|
46
|
+
const entry = await db.blobs.get(key);
|
|
47
|
+
if (!entry) return null;
|
|
48
|
+
void db.blobs.update(key, { lastAccessedAt: Date.now() });
|
|
49
|
+
return entry;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.warn("blob-cache.read.error", {
|
|
52
|
+
meta: { key, error: extractErrorMessage(err) }
|
|
53
|
+
});
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function putEntry(entry) {
|
|
58
|
+
const db = await openBlobCacheDb();
|
|
59
|
+
if (!db) return;
|
|
60
|
+
try {
|
|
61
|
+
await db.blobs.put(entry);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (isQuotaExceeded(err)) {
|
|
64
|
+
try {
|
|
65
|
+
await evictToBudget(db, entry.userId, DEFAULT_BUDGET_BYTES * 0.7);
|
|
66
|
+
await db.blobs.put(entry);
|
|
67
|
+
} catch (retryErr) {
|
|
68
|
+
logger.warn("blob-cache.write.quota-exceeded", {
|
|
69
|
+
meta: { key: entry.key, error: extractErrorMessage(retryErr) }
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
logger.warn("blob-cache.write.error", {
|
|
75
|
+
meta: { key: entry.key, error: extractErrorMessage(err) }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function deleteEntry(key) {
|
|
80
|
+
const db = await openBlobCacheDb();
|
|
81
|
+
if (!db) return;
|
|
82
|
+
try {
|
|
83
|
+
await db.blobs.delete(key);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.warn("blob-cache.delete.error", {
|
|
86
|
+
meta: { key, error: extractErrorMessage(err) }
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function deleteEntriesForFile(userId, fileId) {
|
|
91
|
+
const db = await openBlobCacheDb();
|
|
92
|
+
if (!db) return;
|
|
93
|
+
try {
|
|
94
|
+
await db.blobs.where("userId").equals(userId).and((entry) => entry.fileId === fileId).delete();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger.warn("blob-cache.delete-for-file.error", {
|
|
97
|
+
meta: { userId, fileId, error: extractErrorMessage(err) }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function clearForUser(userId) {
|
|
102
|
+
const db = await openBlobCacheDb();
|
|
103
|
+
if (!db) return;
|
|
104
|
+
try {
|
|
105
|
+
await db.blobs.where("userId").equals(userId).delete();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
logger.warn("blob-cache.clear-for-user.error", {
|
|
108
|
+
meta: { userId, error: extractErrorMessage(err) }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function getStats() {
|
|
113
|
+
const db = await openBlobCacheDb();
|
|
114
|
+
if (!db) return { entryCount: 0, totalBytes: 0 };
|
|
115
|
+
try {
|
|
116
|
+
let count = 0;
|
|
117
|
+
let total = 0;
|
|
118
|
+
await db.blobs.each((entry) => {
|
|
119
|
+
count += 1;
|
|
120
|
+
total += entry.bytes;
|
|
121
|
+
});
|
|
122
|
+
return { entryCount: count, totalBytes: total };
|
|
123
|
+
} catch {
|
|
124
|
+
return { entryCount: 0, totalBytes: 0 };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function isQuotaExceeded(err) {
|
|
128
|
+
if (!err || typeof err !== "object") return false;
|
|
129
|
+
const e = err;
|
|
130
|
+
return e.name === "QuotaExceededError" || e.code === 22;
|
|
131
|
+
}
|
|
132
|
+
async function evictToBudget(db, userId, targetBytes) {
|
|
133
|
+
const entries = await db.blobs.where("userId").equals(userId).toArray();
|
|
134
|
+
const ordered = entries.sort((a, b) => {
|
|
135
|
+
const aBig = a.bytes >= SIZE_FLOOR_BYTES ? 0 : 1;
|
|
136
|
+
const bBig = b.bytes >= SIZE_FLOOR_BYTES ? 0 : 1;
|
|
137
|
+
if (aBig !== bBig) return aBig - bBig;
|
|
138
|
+
return a.lastAccessedAt - b.lastAccessedAt;
|
|
139
|
+
});
|
|
140
|
+
let total = ordered.reduce((sum, e) => sum + e.bytes, 0);
|
|
141
|
+
const toDelete = [];
|
|
142
|
+
for (const entry of ordered) {
|
|
143
|
+
if (total <= targetBytes) break;
|
|
144
|
+
toDelete.push(entry.key);
|
|
145
|
+
total -= entry.bytes;
|
|
146
|
+
}
|
|
147
|
+
if (toDelete.length > 0) {
|
|
148
|
+
await db.blobs.bulkDelete(toDelete);
|
|
149
|
+
logger.info("blob-cache.evicted", {
|
|
150
|
+
meta: { userId, evicted: toDelete.length, totalAfter: total }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function evictForUser(userId, targetBytes) {
|
|
155
|
+
const db = await openBlobCacheDb();
|
|
156
|
+
if (!db) return;
|
|
157
|
+
await evictToBudget(db, userId, targetBytes);
|
|
158
|
+
}
|
|
159
|
+
export {
|
|
160
|
+
BLOB_CACHE_DB_NAME,
|
|
161
|
+
BLOB_CACHE_SCHEMA_VERSION,
|
|
162
|
+
DEFAULT_BUDGET_BYTES,
|
|
163
|
+
clearForUser,
|
|
164
|
+
deleteEntriesForFile,
|
|
165
|
+
deleteEntry,
|
|
166
|
+
evictForUser,
|
|
167
|
+
getEntry,
|
|
168
|
+
getStats,
|
|
169
|
+
openBlobCacheDb,
|
|
170
|
+
putEntry
|
|
171
|
+
};
|
|
172
|
+
//# sourceMappingURL=idb-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/cache/idb-store.ts"],"sourcesContent":["/**\n * features/files/cache/idb-store.ts\n *\n * IndexedDB tier of the 3-tier byte cache (memory LRU → IDB → network).\n *\n * Dexie wrapper for the `matrx-blob-cache` database — a SEPARATE DB from\n * `matrx-sync` so cache eviction can never threaten sync persistence.\n * Schema is intentionally simple: one `blobs` table keyed by a string\n * cache key, indexed by `userId` (for sign-out purge) and\n * `lastAccessedAt` (for LRU eviction).\n *\n * Cache key strategy (see plan §4A):\n * - Owned files (current version): `${userId}:${fileId}:current:${checksum}`\n * - Owned files (pinned version): `${userId}:${fileId}:${N}:${checksum}`\n * - URL-keyed entries (share-link bytes, etc.): `${userId}:url:${sha256(url)}`\n *\n * Signed S3 URLs (either AWS dialect — SigV4 `X-Amz-*` or SigV2\n * `AWSAccessKeyId`/`Signature`/`Expires`) are NEVER cached because they\n * expire — caching them would create stale-URL bugs.\n *\n * Failure modes:\n * - Private browsing / IDB disabled / quota exceeded: `openBlobCacheDb()`\n * rejects → all reads return null, all writes resolve as no-ops. The\n * in-memory LRU + network stay functional.\n *\n * Identity scoping:\n * - Every entry is stamped with `userId`. Sign-out calls `clearForUser`\n * so cross-user contamination is structurally impossible.\n *\n * Budget:\n * - Default 2 GB total. Eviction is size-aware-LRU: oldest entries above\n * a 2 MB floor are evicted first; tiny entries (<2 MB) only evict when\n * the cache is otherwise full of small entries.\n * - QuotaExceededError triggers an immediate eviction down to 70% of\n * budget and a single retry. Subsequent failures fall back to network.\n */\n\nimport Dexie, { type Table } from \"dexie\";\nimport { logger } from \"../support/logger\";\nimport { extractErrorMessage } from \"@ai-matrx/data/net\";\n\nexport const BLOB_CACHE_DB_NAME = \"matrx-blob-cache\";\nexport const BLOB_CACHE_SCHEMA_VERSION = 1;\nexport const DEFAULT_BUDGET_BYTES = 2 * 1024 * 1024 * 1024; // 2 GB\nconst SIZE_FLOOR_BYTES = 2 * 1024 * 1024; // 2 MB — evict bigger entries first\n\nexport type BlobCacheSource =\n | \"files-download\"\n | \"share\"\n | \"cdn\"\n | \"url\";\n\nexport interface BlobCacheEntry {\n /** Composite cache key — see header comment for the key shape. */\n key: string;\n userId: string;\n /** Null for url-keyed entries (share links / arbitrary URLs). */\n fileId: string | null;\n version: number | null;\n checksum: string | null;\n mimeType: string;\n bytes: number;\n blob: Blob;\n etag: string | null;\n fetchedAt: number;\n lastAccessedAt: number;\n source: BlobCacheSource;\n}\n\ninterface BlobCacheDb extends Dexie {\n blobs: Table<BlobCacheEntry, string>;\n}\n\nfunction hasIndexedDb(): boolean {\n if (typeof globalThis === \"undefined\") return false;\n return typeof (globalThis as { indexedDB?: unknown }).indexedDB !== \"undefined\";\n}\n\nlet dbPromise: Promise<BlobCacheDb | null> | null = null;\n\n/**\n * Open the Dexie blob-cache DB. Lazy + memoized — the server-render pass\n * never touches IDB. Returns `null` when IDB is unavailable (private\n * browsing, server-side, quota exhausted at open time); callers should\n * treat that as \"no persistent tier\" and fall through to network.\n */\nexport function openBlobCacheDb(): Promise<BlobCacheDb | null> {\n if (dbPromise) return dbPromise;\n dbPromise = (async () => {\n if (!hasIndexedDb()) {\n logger.info(\"blob-cache.unavailable\", {\n meta: { reason: \"no-indexedDB\" },\n });\n return null;\n }\n try {\n const db = new Dexie(BLOB_CACHE_DB_NAME) as BlobCacheDb;\n db.version(BLOB_CACHE_SCHEMA_VERSION).stores({\n // Primary key `key`; secondary indexes for purge + eviction.\n blobs: \"key, userId, lastAccessedAt, bytes\",\n });\n await db.open();\n logger.info(\"blob-cache.open.success\", {\n meta: { schemaVersion: BLOB_CACHE_SCHEMA_VERSION },\n });\n return db;\n } catch (err) {\n logger.warn(\"blob-cache.open.error\", {\n meta: { error: extractErrorMessage(err) },\n });\n return null;\n }\n })();\n return dbPromise;\n}\n\n// ---------------------------------------------------------------------------\n// Read / write\n// ---------------------------------------------------------------------------\n\nexport async function getEntry(key: string): Promise<BlobCacheEntry | null> {\n const db = await openBlobCacheDb();\n if (!db) return null;\n try {\n const entry = await db.blobs.get(key);\n if (!entry) return null;\n // Bump LRU position. Fire-and-forget — the read result is already\n // returned, and a missed lastAccessedAt bump just shortens this entry's\n // lifetime in the next eviction pass (acceptable).\n void db.blobs.update(key, { lastAccessedAt: Date.now() });\n return entry;\n } catch (err) {\n logger.warn(\"blob-cache.read.error\", {\n meta: { key, error: extractErrorMessage(err) },\n });\n return null;\n }\n}\n\nexport async function putEntry(entry: BlobCacheEntry): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.put(entry);\n // Best-effort eviction pass. If quota is exhausted we'll catch and\n // retry the put once after an emergency eviction.\n } catch (err) {\n // Quota-exceeded: try to free space, retry once, then give up.\n if (isQuotaExceeded(err)) {\n try {\n await evictToBudget(db, entry.userId, DEFAULT_BUDGET_BYTES * 0.7);\n await db.blobs.put(entry);\n } catch (retryErr) {\n logger.warn(\"blob-cache.write.quota-exceeded\", {\n meta: { key: entry.key, error: extractErrorMessage(retryErr) },\n });\n }\n return;\n }\n logger.warn(\"blob-cache.write.error\", {\n meta: { key: entry.key, error: extractErrorMessage(err) },\n });\n }\n}\n\nexport async function deleteEntry(key: string): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.delete(key);\n } catch (err) {\n logger.warn(\"blob-cache.delete.error\", {\n meta: { key, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Drop every entry whose key starts with `${userId}:${fileId}:`. Use on\n * realtime version-bump / hard-delete so every cached version of a file\n * gets evicted regardless of the version segment in the key.\n */\nexport async function deleteEntriesForFile(\n userId: string,\n fileId: string,\n): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.where(\"userId\").equals(userId).and((entry) => entry.fileId === fileId).delete();\n } catch (err) {\n logger.warn(\"blob-cache.delete-for-file.error\", {\n meta: { userId, fileId, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Drop every entry for the signed-out user. Called from the sign-out\n * code path so the next user can never read the previous user's blobs.\n */\nexport async function clearForUser(userId: string): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n try {\n await db.blobs.where(\"userId\").equals(userId).delete();\n } catch (err) {\n logger.warn(\"blob-cache.clear-for-user.error\", {\n meta: { userId, error: extractErrorMessage(err) },\n });\n }\n}\n\n/**\n * Test/debug helper. Returns `{ entryCount, totalBytes }`. The admin\n * observability panel reads this.\n */\nexport async function getStats(): Promise<{ entryCount: number; totalBytes: number }> {\n const db = await openBlobCacheDb();\n if (!db) return { entryCount: 0, totalBytes: 0 };\n try {\n let count = 0;\n let total = 0;\n await db.blobs.each((entry) => {\n count += 1;\n total += entry.bytes;\n });\n return { entryCount: count, totalBytes: total };\n } catch {\n return { entryCount: 0, totalBytes: 0 };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Eviction\n// ---------------------------------------------------------------------------\n\nfunction isQuotaExceeded(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { name?: string; code?: number };\n return e.name === \"QuotaExceededError\" || e.code === 22;\n}\n\n/**\n * Evict entries (size-aware-LRU) for `userId` until total bytes ≤ target.\n *\n * Strategy: entries are sorted by `(bytes >= SIZE_FLOOR_BYTES, lastAccessedAt)`\n * so large stale entries go first; small entries only get evicted once the\n * large pile is gone. Stops when the per-user total drops below `targetBytes`.\n */\nasync function evictToBudget(\n db: BlobCacheDb,\n userId: string,\n targetBytes: number,\n): Promise<void> {\n const entries = await db.blobs\n .where(\"userId\")\n .equals(userId)\n .toArray();\n const ordered = entries.sort((a, b) => {\n const aBig = a.bytes >= SIZE_FLOOR_BYTES ? 0 : 1;\n const bBig = b.bytes >= SIZE_FLOOR_BYTES ? 0 : 1;\n if (aBig !== bBig) return aBig - bBig;\n return a.lastAccessedAt - b.lastAccessedAt;\n });\n let total = ordered.reduce((sum, e) => sum + e.bytes, 0);\n const toDelete: string[] = [];\n for (const entry of ordered) {\n if (total <= targetBytes) break;\n toDelete.push(entry.key);\n total -= entry.bytes;\n }\n if (toDelete.length > 0) {\n await db.blobs.bulkDelete(toDelete);\n logger.info(\"blob-cache.evicted\", {\n meta: { userId, evicted: toDelete.length, totalAfter: total },\n });\n }\n}\n\n/**\n * Public LRU evict for a given user — exposed so the admin observability\n * panel can offer an \"Evict now\" button.\n */\nexport async function evictForUser(userId: string, targetBytes: number): Promise<void> {\n const db = await openBlobCacheDb();\n if (!db) return;\n await evictToBudget(db, userId, targetBytes);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/cache/policy.ts
|
|
3
|
+
*
|
|
4
|
+
* Per-MIME size policy for the IndexedDB tier. The in-memory LRU caches
|
|
5
|
+
* everything (it's session-scoped and bounded by total bytes); IDB only
|
|
6
|
+
* persists what's worth re-loading across page reloads.
|
|
7
|
+
*
|
|
8
|
+
* The defaults are conservative: cache the large, expensive-to-re-fetch
|
|
9
|
+
* artifacts (PDFs, video posters, audio) and the small text-y assets
|
|
10
|
+
* (markdown, JSON, code) that previewers consult repeatedly.
|
|
11
|
+
*
|
|
12
|
+
* Returned `false` means "do not write to IDB". Memory LRU still caches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function shouldPersistInIdb(mimeType: string | null | undefined, byteSize: number): boolean;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const POLICIES = [
|
|
2
|
+
{ prefix: "image/", maxBytes: 50 * 1024 * 1024 },
|
|
3
|
+
{ prefix: "audio/", maxBytes: 100 * 1024 * 1024 },
|
|
4
|
+
// Videos cache only up to 50 MB by default. Larger videos rely on
|
|
5
|
+
// native HTML5 streaming + server-side Range responses.
|
|
6
|
+
{ prefix: "video/", maxBytes: 50 * 1024 * 1024 },
|
|
7
|
+
{ exact: ["application/pdf"], maxBytes: 250 * 1024 * 1024 },
|
|
8
|
+
{
|
|
9
|
+
exact: [
|
|
10
|
+
"application/zip",
|
|
11
|
+
"application/x-zip-compressed",
|
|
12
|
+
"application/x-tar",
|
|
13
|
+
"application/gzip"
|
|
14
|
+
],
|
|
15
|
+
maxBytes: 500 * 1024 * 1024
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
prefix: "text/",
|
|
19
|
+
maxBytes: 10 * 1024 * 1024
|
|
20
|
+
},
|
|
21
|
+
{ exact: ["application/json", "application/xml"], maxBytes: 10 * 1024 * 1024 }
|
|
22
|
+
];
|
|
23
|
+
const FALLBACK_MAX_BYTES = 25 * 1024 * 1024;
|
|
24
|
+
function shouldPersistInIdb(mimeType, byteSize) {
|
|
25
|
+
if (!mimeType || byteSize <= 0) return false;
|
|
26
|
+
const normalized = mimeType.toLowerCase();
|
|
27
|
+
for (const policy of POLICIES) {
|
|
28
|
+
if (policy.exact?.includes(normalized)) {
|
|
29
|
+
return byteSize <= policy.maxBytes;
|
|
30
|
+
}
|
|
31
|
+
if (policy.prefix && normalized.startsWith(policy.prefix)) {
|
|
32
|
+
return byteSize <= policy.maxBytes;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return byteSize <= FALLBACK_MAX_BYTES;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
shouldPersistInIdb
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=policy.js.map
|