@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @ai-matrx/media
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
New subpath `@ai-matrx/media/files/engine` plus one subpath per engine module
|
|
6
|
+
(`@ai-matrx/media/files/engine/<dir>/<module>`) — chat package independence P16f, batch 2:
|
|
7
|
+
the files engine moved verbatim from matrx-frontend `features/files` + `lib/media`: the
|
|
8
|
+
`cloudFiles` slice, `cloudFilesRealtimeMiddleware`, `cloudFilesMutationToastMiddleware`,
|
|
9
|
+
thunks and selectors, the file handler (`normalize`, `resolve`, `upload`, `toMediaRef`,
|
|
10
|
+
`toTarget`), `useFile`/`useFileUpload`, multipart + TUS uploads and the dedup upload guard,
|
|
11
|
+
the byte cache, `recognizeOurFileUrl`. ESM-only, one module per file.
|
|
12
|
+
|
|
13
|
+
**Consumer action:** call `configureFilesHost({ db, server, store })` once before creating the
|
|
14
|
+
store (optionally `scope`, `ensureOrganizationContext`, `shareLinks`, `notify`), mount
|
|
15
|
+
`cloudFilesReducer` under `cloudFiles`, and add both middlewares. New peers (optional for
|
|
16
|
+
every other entry): `@reduxjs/toolkit` >=2, `react-redux` >=9.
|
|
17
|
+
|
|
3
18
|
## 0.8.0
|
|
4
19
|
|
|
5
20
|
New subpath `@ai-matrx/media/files` (chat package independence P16f, first batch): the file
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/api/assets.ts
|
|
3
|
+
*
|
|
4
|
+
* Typed REST wrapper for the unified asset (image / media) pipeline.
|
|
5
|
+
*
|
|
6
|
+
* POST /assets multipart upload + render
|
|
7
|
+
* GET /assets/{file_id} read Asset envelope
|
|
8
|
+
* PATCH /assets/{file_id} visibility / share / metadata
|
|
9
|
+
* POST /assets/{file_id}/variants render more variants
|
|
10
|
+
* GET /assets/presets list every server-known preset
|
|
11
|
+
* GET /files/{file_id}/asset any cld_files row → Asset envelope
|
|
12
|
+
*
|
|
13
|
+
* Every endpoint returns the same {@link Asset} envelope (see
|
|
14
|
+
* `features/files/types.ts`). `GET /files/{id}/asset` is the canonical
|
|
15
|
+
* "click-to-render" primitive — pass any file_id, get back inline-renderable
|
|
16
|
+
* URLs grouped by variant key.
|
|
17
|
+
*
|
|
18
|
+
* Auth + request-id + idempotency-key + bypass-secret plumbing all flow
|
|
19
|
+
* through the same client helpers used by `files.ts`; see
|
|
20
|
+
* [./client.ts](./client.ts) for the contract.
|
|
21
|
+
*/
|
|
22
|
+
import { type RequestOptions, type ResponseMeta, type UploadProgressEvent } from "../host/python-client.js";
|
|
23
|
+
import type { AddAssetVariantsRequest, Asset, AssetPatchRequest, AssetPreset, PresetsRegistryResponse, Visibility } from "../types.js";
|
|
24
|
+
import type { components } from "@ai-matrx/agents/generated/api-types";
|
|
25
|
+
/**
|
|
26
|
+
* Spec for a one-off custom variant — sent verbatim to the server in
|
|
27
|
+
* `custom_variants_json`. Use this for ad-hoc dimensions that don't fit
|
|
28
|
+
* any built-in preset. Prefer the preset registry when possible so the
|
|
29
|
+
* resulting variant has a well-known key downstream renderers recognize.
|
|
30
|
+
*/
|
|
31
|
+
export interface CustomVariantSpec {
|
|
32
|
+
key: string;
|
|
33
|
+
suffix?: string;
|
|
34
|
+
width?: number;
|
|
35
|
+
height?: number;
|
|
36
|
+
quality?: number;
|
|
37
|
+
format?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface UploadAssetParams {
|
|
40
|
+
file: File;
|
|
41
|
+
/** Preset name. Default: `"raw"` — no derived variants. */
|
|
42
|
+
preset?: AssetPreset;
|
|
43
|
+
/** Logical folder path. Server defaults to `Assets/<uuid>` when omitted. */
|
|
44
|
+
folder?: string;
|
|
45
|
+
/** Default `"public"`. */
|
|
46
|
+
visibility?: Visibility;
|
|
47
|
+
/** Comma-separated user IDs OR an explicit list (the wrapper joins). */
|
|
48
|
+
shareWith?: string | ReadonlyArray<string>;
|
|
49
|
+
/** Default `"read"`. */
|
|
50
|
+
shareLevel?: "read" | "write" | "admin";
|
|
51
|
+
/** Override the preset's default `include_social_baseline`. */
|
|
52
|
+
includeSocialBaseline?: boolean;
|
|
53
|
+
/** Free-form metadata to attach to every variant + the master row. */
|
|
54
|
+
metadata?: Record<string, unknown>;
|
|
55
|
+
/** Ad-hoc variant specs in addition to (or instead of) the preset. */
|
|
56
|
+
customVariants?: ReadonlyArray<CustomVariantSpec>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Upload an asset and render preset variants in one round-trip.
|
|
60
|
+
* Returns the canonical {@link Asset} envelope — the same shape every
|
|
61
|
+
* other endpoint in this module returns.
|
|
62
|
+
*/
|
|
63
|
+
export declare function uploadAsset(params: UploadAssetParams, opts?: RequestOptions): Promise<{
|
|
64
|
+
data: Asset;
|
|
65
|
+
meta: ResponseMeta;
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Same as `uploadAsset` but with progress callbacks. Useful for big
|
|
69
|
+
* uploads where the user expects a `Uploading 12 / 30 MB…` indicator.
|
|
70
|
+
*/
|
|
71
|
+
export declare function uploadAssetWithProgress(params: UploadAssetParams, onProgress: (event: UploadProgressEvent) => void, opts?: RequestOptions): Promise<{
|
|
72
|
+
data: Asset;
|
|
73
|
+
meta: ResponseMeta;
|
|
74
|
+
}>;
|
|
75
|
+
export interface GetAssetParams {
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read the Asset envelope for a known asset master file id.
|
|
79
|
+
*
|
|
80
|
+
* Use this when you already uploaded through `/assets` and want to
|
|
81
|
+
* re-read the envelope (e.g. after a visibility change). For a generic
|
|
82
|
+
* "give me an Asset for this file_id" call (where the file may or may
|
|
83
|
+
* not have been uploaded through the asset pipeline), prefer
|
|
84
|
+
* {@link getAssetForFile}.
|
|
85
|
+
*/
|
|
86
|
+
export declare function getAsset(fileId: string, _params?: GetAssetParams, opts?: RequestOptions): Promise<{
|
|
87
|
+
data: Asset;
|
|
88
|
+
meta: ResponseMeta;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Canonical click-to-render primitive: hand any cld_files row id, get
|
|
92
|
+
* back an Asset envelope (with at least an `original` variant). Works
|
|
93
|
+
* regardless of whether the file was uploaded through the asset
|
|
94
|
+
* pipeline. The hook around this is {@link useFileAsset}.
|
|
95
|
+
*/
|
|
96
|
+
export declare function getAssetForFile(fileId: string, _params?: GetAssetParams, opts?: RequestOptions): Promise<{
|
|
97
|
+
data: Asset;
|
|
98
|
+
meta: ResponseMeta;
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* PATCH `/assets/{file_id}` — change visibility / share / metadata.
|
|
102
|
+
* Returns the new Asset envelope reflecting the change (URLs included).
|
|
103
|
+
*/
|
|
104
|
+
export declare function patchAsset(fileId: string, body: AssetPatchRequest, opts?: RequestOptions): Promise<{
|
|
105
|
+
data: Asset;
|
|
106
|
+
meta: ResponseMeta;
|
|
107
|
+
}>;
|
|
108
|
+
/**
|
|
109
|
+
* POST `/assets/{file_id}/variants` — render more variants for an
|
|
110
|
+
* existing asset. Idempotent on `(file_id, variant_key)`: re-rendering
|
|
111
|
+
* a key that already exists is a no-op (the server returns the
|
|
112
|
+
* existing variant).
|
|
113
|
+
*/
|
|
114
|
+
export declare function addAssetVariants(fileId: string, body: AddAssetVariantsRequest, opts?: RequestOptions): Promise<{
|
|
115
|
+
data: Asset;
|
|
116
|
+
meta: ResponseMeta;
|
|
117
|
+
}>;
|
|
118
|
+
/**
|
|
119
|
+
* List every server-known preset and its variants. Use this to drive
|
|
120
|
+
* picker UIs / admin tooling instead of hardcoding the list in TS.
|
|
121
|
+
*/
|
|
122
|
+
export declare function getAssetPresets(opts?: RequestOptions): Promise<{
|
|
123
|
+
data: PresetsRegistryResponse;
|
|
124
|
+
meta: ResponseMeta;
|
|
125
|
+
}>;
|
|
126
|
+
/**
|
|
127
|
+
* Spec for a single preview variant — width/height drives the resize;
|
|
128
|
+
* `format` and `quality` control the output encoding. Sent verbatim to
|
|
129
|
+
* the server (no client-side image processing).
|
|
130
|
+
*
|
|
131
|
+
* `position` and `background_color` are honored when `fit: "cover"`:
|
|
132
|
+
* - Anchor strings (`"top"`, `"top-left"`, `"entropy"`, etc.) pick the
|
|
133
|
+
* crop region against one of nine fixed anchors or sharp's smart
|
|
134
|
+
* strategies.
|
|
135
|
+
* - A `{x, y}` point (each in `[0, 1]`) places the crop's centre at
|
|
136
|
+
* that focal point in the source image. The server clamps so the
|
|
137
|
+
* crop never escapes the source bounds.
|
|
138
|
+
*
|
|
139
|
+
* `background_color` is consulted in two places:
|
|
140
|
+
* - `fit: "contain"` — letterbox padding colour.
|
|
141
|
+
* - `format: "jpeg" | "avif"` — alpha-flatten colour (those formats
|
|
142
|
+
* don't carry alpha; transparent pixels become opaque).
|
|
143
|
+
*
|
|
144
|
+
* Field names are snake_case to match the Python multipart body schema
|
|
145
|
+
* verbatim — `position` / `background_color` reach Python as-is inside
|
|
146
|
+
* the `variants_json` array.
|
|
147
|
+
*
|
|
148
|
+
* These are DERIVED from the OpenAPI-generated schema, never hand-mirrored.
|
|
149
|
+
* A backend rename (e.g. `key` → `preset_id`) changes the generated type,
|
|
150
|
+
* which turns every drifted callsite into a compile error in the same PR
|
|
151
|
+
* that runs `pnpm sync-types`. Do NOT redeclare these as standalone
|
|
152
|
+
* interfaces — that severs the link and lets the wire contract drift
|
|
153
|
+
* silently (which is exactly how the `invalid_variant_spec` 400 shipped).
|
|
154
|
+
*/
|
|
155
|
+
export type PreviewVariantSpec = components["schemas"]["PreviewVariantSpec"];
|
|
156
|
+
/** Named anchor / smart-crop mode / focal point for `fit: "cover"`. */
|
|
157
|
+
export type PreviewVariantPosition = NonNullable<PreviewVariantSpec["position"]>;
|
|
158
|
+
/** One rendered variant in the `/assets/preview*` response. */
|
|
159
|
+
export type PreviewVariantResult = components["schemas"]["AssetPreviewVariant"];
|
|
160
|
+
/** Response envelope for `POST /assets/preview` and `.../multipart`. */
|
|
161
|
+
export type AssetPreviewResult = components["schemas"]["AssetPreviewResponse"];
|
|
162
|
+
export interface PdfCompressResult {
|
|
163
|
+
original_size: number;
|
|
164
|
+
compressed_size: number;
|
|
165
|
+
/** 0..1 — fraction of bytes saved. */
|
|
166
|
+
reduction_ratio: number;
|
|
167
|
+
mime_type: string;
|
|
168
|
+
data_url: string | null;
|
|
169
|
+
/** 5-minute row-less scratch URL — the ONLY lawful expiring URL shape. */
|
|
170
|
+
ephemeral_url: string | null;
|
|
171
|
+
/** Seconds until ephemeral_url expires (ephemeral mode only). */
|
|
172
|
+
expires_in?: number | null;
|
|
173
|
+
/** Tier the caller asked for. */
|
|
174
|
+
level_requested?: number | null;
|
|
175
|
+
/** Tier that actually produced the output (cap escalation may raise it). */
|
|
176
|
+
level_used?: number | null;
|
|
177
|
+
/** Whether max_size_bytes was met (null when no cap was sent). */
|
|
178
|
+
cap_satisfied?: boolean | null;
|
|
179
|
+
/** "inplace" | "raster" — which compression path produced the output. */
|
|
180
|
+
strategy_used?: string | null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* `POST /assets/preview/multipart` — render preview variants WITHOUT
|
|
184
|
+
* persisting the master. Image Studio uses this for the drag-and-drop
|
|
185
|
+
* preview flow; the user can then commit-to-save via `POST /assets`.
|
|
186
|
+
*
|
|
187
|
+
* Each variant comes back as either an inline `data_url` (small) or an
|
|
188
|
+
* `ephemeral_url` (5-minute row-less preview scratch).
|
|
189
|
+
*/
|
|
190
|
+
export declare function previewAssetMultipart(file: File | Blob, variants: PreviewVariantSpec[], options?: {
|
|
191
|
+
maxInlineBytes?: number;
|
|
192
|
+
}, opts?: RequestOptions): Promise<{
|
|
193
|
+
data: AssetPreviewResult;
|
|
194
|
+
meta: ResponseMeta;
|
|
195
|
+
}>;
|
|
196
|
+
/**
|
|
197
|
+
* `POST /assets/pdf-compress/multipart` — compress a PDF WITHOUT
|
|
198
|
+
* persisting it. Returns `data_url` (≤256 KB) or `ephemeral_url`
|
|
199
|
+
* (5-minute row-less scratch). Replaces the deleted Next.js Sharp/PDF route.
|
|
200
|
+
*
|
|
201
|
+
* @param level minimum compression tier (1..5). Default 3. Server may
|
|
202
|
+
* escalate above this when `maxSizeBytes` forces it.
|
|
203
|
+
* @param maxSizeBytes optional absolute upper bound on output size.
|
|
204
|
+
*/
|
|
205
|
+
export declare function compressPdfMultipart(file: File | Blob, options?: {
|
|
206
|
+
level?: number;
|
|
207
|
+
maxSizeBytes?: number;
|
|
208
|
+
maxInlineBytes?: number;
|
|
209
|
+
}, opts?: RequestOptions): Promise<{
|
|
210
|
+
data: PdfCompressResult;
|
|
211
|
+
meta: ResponseMeta;
|
|
212
|
+
}>;
|
|
213
|
+
/**
|
|
214
|
+
* Materialize a `PreviewVariantResult` / `PdfCompressResult` body that
|
|
215
|
+
* carries either `data_url` or `ephemeral_url` into a `Blob`. Useful when
|
|
216
|
+
* the FE needs the bytes locally (e.g. to wrap in a `File` for further
|
|
217
|
+
* upload).
|
|
218
|
+
*/
|
|
219
|
+
export declare function materializeAssetResult(result: {
|
|
220
|
+
data_url: string | null;
|
|
221
|
+
ephemeral_url: string | null;
|
|
222
|
+
mime_type: string;
|
|
223
|
+
}): Promise<Blob>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getJson,
|
|
3
|
+
patchJson,
|
|
4
|
+
postJson,
|
|
5
|
+
postMultipart,
|
|
6
|
+
uploadWithProgress
|
|
7
|
+
} from "../host/python-client.js";
|
|
8
|
+
import { withFileOrganization } from "./fileOrganization.js";
|
|
9
|
+
import { ENDPOINTS } from "@ai-matrx/agents/matrx";
|
|
10
|
+
import { apiMultipart } from "../host/typed-client.js";
|
|
11
|
+
function buildUploadForm(params) {
|
|
12
|
+
const form = new FormData();
|
|
13
|
+
form.append("file", params.file);
|
|
14
|
+
if (params.preset) form.append("preset", params.preset);
|
|
15
|
+
if (params.folder) form.append("folder", params.folder);
|
|
16
|
+
if (params.visibility) form.append("visibility", params.visibility);
|
|
17
|
+
if (params.shareWith !== void 0) {
|
|
18
|
+
const joined = Array.isArray(params.shareWith) ? params.shareWith.join(",") : params.shareWith;
|
|
19
|
+
if (joined) form.append("share_with", joined);
|
|
20
|
+
}
|
|
21
|
+
if (params.shareLevel) form.append("share_level", params.shareLevel);
|
|
22
|
+
if (params.includeSocialBaseline !== void 0)
|
|
23
|
+
form.append(
|
|
24
|
+
"include_social_baseline",
|
|
25
|
+
params.includeSocialBaseline ? "true" : "false"
|
|
26
|
+
);
|
|
27
|
+
if (params.metadata)
|
|
28
|
+
form.append("metadata_json", JSON.stringify(params.metadata));
|
|
29
|
+
if (params.customVariants && params.customVariants.length)
|
|
30
|
+
form.append("custom_variants_json", JSON.stringify(params.customVariants));
|
|
31
|
+
return form;
|
|
32
|
+
}
|
|
33
|
+
async function uploadAsset(params, opts = {}) {
|
|
34
|
+
const form = buildUploadForm(params);
|
|
35
|
+
return postMultipart(ENDPOINTS.assets.upload, form, opts);
|
|
36
|
+
}
|
|
37
|
+
async function uploadAssetWithProgress(params, onProgress, opts = {}) {
|
|
38
|
+
const form = buildUploadForm(params);
|
|
39
|
+
return uploadWithProgress(
|
|
40
|
+
ENDPOINTS.assets.upload,
|
|
41
|
+
form,
|
|
42
|
+
onProgress,
|
|
43
|
+
opts
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
async function getAsset(fileId, _params = {}, opts = {}) {
|
|
47
|
+
return getJson(ENDPOINTS.assets.detail(fileId), withFileOrganization(fileId, opts));
|
|
48
|
+
}
|
|
49
|
+
async function getAssetForFile(fileId, _params = {}, opts = {}) {
|
|
50
|
+
return getJson(ENDPOINTS.assets.forFile(fileId), withFileOrganization(fileId, opts));
|
|
51
|
+
}
|
|
52
|
+
async function patchAsset(fileId, body, opts = {}) {
|
|
53
|
+
const normalised = {
|
|
54
|
+
...body,
|
|
55
|
+
share_with: Array.isArray(body.share_with) ? body.share_with.join(",") : body.share_with
|
|
56
|
+
};
|
|
57
|
+
return patchJson(
|
|
58
|
+
ENDPOINTS.assets.patch(fileId),
|
|
59
|
+
normalised,
|
|
60
|
+
opts
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
async function addAssetVariants(fileId, body, opts = {}) {
|
|
64
|
+
return postJson(
|
|
65
|
+
ENDPOINTS.assets.addVariants(fileId),
|
|
66
|
+
body,
|
|
67
|
+
opts
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
async function getAssetPresets(opts = {}) {
|
|
71
|
+
return getJson(ENDPOINTS.assets.presets, opts);
|
|
72
|
+
}
|
|
73
|
+
async function previewAssetMultipart(file, variants, options = {}, opts = {}) {
|
|
74
|
+
const form = new FormData();
|
|
75
|
+
form.append("file", file);
|
|
76
|
+
form.append("variants_json", JSON.stringify(variants));
|
|
77
|
+
if (typeof options.maxInlineBytes === "number") {
|
|
78
|
+
form.append("max_inline_bytes", String(options.maxInlineBytes));
|
|
79
|
+
}
|
|
80
|
+
return apiMultipart(ENDPOINTS.assets.previewMultipart, form, opts);
|
|
81
|
+
}
|
|
82
|
+
async function compressPdfMultipart(file, options = {}, opts = {}) {
|
|
83
|
+
const form = new FormData();
|
|
84
|
+
form.append("file", file);
|
|
85
|
+
form.append("level", String(options.level ?? 3));
|
|
86
|
+
if (typeof options.maxSizeBytes === "number") {
|
|
87
|
+
form.append("max_size_bytes", String(options.maxSizeBytes));
|
|
88
|
+
}
|
|
89
|
+
if (typeof options.maxInlineBytes === "number") {
|
|
90
|
+
form.append("max_inline_bytes", String(options.maxInlineBytes));
|
|
91
|
+
}
|
|
92
|
+
return postMultipart(
|
|
93
|
+
ENDPOINTS.assets.pdfCompressMultipart,
|
|
94
|
+
form,
|
|
95
|
+
opts
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
async function materializeAssetResult(result) {
|
|
99
|
+
if (result.data_url) {
|
|
100
|
+
const response = await fetch(result.data_url);
|
|
101
|
+
return response.blob();
|
|
102
|
+
}
|
|
103
|
+
if (result.ephemeral_url) {
|
|
104
|
+
const response = await fetch(result.ephemeral_url);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`Failed to fetch ephemeral asset URL: HTTP ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
return response.blob();
|
|
109
|
+
}
|
|
110
|
+
throw new Error("Asset result has neither data_url nor ephemeral_url");
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
addAssetVariants,
|
|
114
|
+
compressPdfMultipart,
|
|
115
|
+
getAsset,
|
|
116
|
+
getAssetForFile,
|
|
117
|
+
getAssetPresets,
|
|
118
|
+
materializeAssetResult,
|
|
119
|
+
patchAsset,
|
|
120
|
+
previewAssetMultipart,
|
|
121
|
+
uploadAsset,
|
|
122
|
+
uploadAssetWithProgress
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=assets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/api/assets.ts"],"sourcesContent":["/**\n * features/files/api/assets.ts\n *\n * Typed REST wrapper for the unified asset (image / media) pipeline.\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\n * GET /assets/presets list every server-known preset\n * GET /files/{file_id}/asset any cld_files row → Asset envelope\n *\n * Every endpoint returns the same {@link Asset} envelope (see\n * `features/files/types.ts`). `GET /files/{id}/asset` is the canonical\n * \"click-to-render\" primitive — pass any file_id, get back inline-renderable\n * URLs grouped by variant key.\n *\n * Auth + request-id + idempotency-key + bypass-secret plumbing all flow\n * through the same client helpers used by `files.ts`; see\n * [./client.ts](./client.ts) for the contract.\n */\n\nimport {\n getJson,\n patchJson,\n postJson,\n postMultipart,\n uploadWithProgress,\n type RequestOptions,\n type ResponseMeta,\n type UploadProgressEvent,\n} from \"../host/python-client\";\nimport { withFileOrganization } from \"./fileOrganization\";\nimport type {\n AddAssetVariantsRequest,\n Asset,\n AssetPatchRequest,\n AssetPreset,\n PresetsRegistryResponse,\n Visibility,\n} from \"../types\";\nimport { ENDPOINTS } from \"@ai-matrx/agents/matrx\";\nimport { apiMultipart } from \"../host/typed-client\";\nimport type { components } from \"@ai-matrx/agents/generated/api-types\";\n\n// ---------------------------------------------------------------------------\n// Upload\n// ---------------------------------------------------------------------------\n\n/**\n * Spec for a one-off custom variant — sent verbatim to the server in\n * `custom_variants_json`. Use this for ad-hoc dimensions that don't fit\n * any built-in preset. Prefer the preset registry when possible so the\n * resulting variant has a well-known key downstream renderers recognize.\n */\nexport interface CustomVariantSpec {\n key: string;\n suffix?: string;\n width?: number;\n height?: number;\n quality?: number;\n format?: string;\n}\n\nexport interface UploadAssetParams {\n file: File;\n /** Preset name. Default: `\"raw\"` — no derived variants. */\n preset?: AssetPreset;\n /** Logical folder path. Server defaults to `Assets/<uuid>` when omitted. */\n folder?: string;\n /** Default `\"public\"`. */\n visibility?: Visibility;\n /** Comma-separated user IDs OR an explicit list (the wrapper joins). */\n shareWith?: string | ReadonlyArray<string>;\n /** Default `\"read\"`. */\n shareLevel?: \"read\" | \"write\" | \"admin\";\n /** Override the preset's default `include_social_baseline`. */\n includeSocialBaseline?: boolean;\n /** Free-form metadata to attach to every variant + the master row. */\n metadata?: Record<string, unknown>;\n /** Ad-hoc variant specs in addition to (or instead of) the preset. */\n customVariants?: ReadonlyArray<CustomVariantSpec>;\n}\n\nfunction buildUploadForm(params: UploadAssetParams): FormData {\n const form = new FormData();\n form.append(\"file\", params.file);\n if (params.preset) form.append(\"preset\", params.preset);\n if (params.folder) form.append(\"folder\", params.folder);\n if (params.visibility) form.append(\"visibility\", params.visibility);\n if (params.shareWith !== undefined) {\n const joined = Array.isArray(params.shareWith)\n ? params.shareWith.join(\",\")\n : (params.shareWith as string);\n if (joined) form.append(\"share_with\", joined);\n }\n if (params.shareLevel) form.append(\"share_level\", params.shareLevel);\n if (params.includeSocialBaseline !== undefined)\n form.append(\n \"include_social_baseline\",\n params.includeSocialBaseline ? \"true\" : \"false\",\n );\n if (params.metadata)\n form.append(\"metadata_json\", JSON.stringify(params.metadata));\n if (params.customVariants && params.customVariants.length)\n form.append(\"custom_variants_json\", JSON.stringify(params.customVariants));\n return form;\n}\n\n/**\n * Upload an asset and render preset variants in one round-trip.\n * Returns the canonical {@link Asset} envelope — the same shape every\n * other endpoint in this module returns.\n */\nexport async function uploadAsset(\n params: UploadAssetParams,\n opts: RequestOptions = {},\n): Promise<{ data: Asset; meta: ResponseMeta }> {\n const form = buildUploadForm(params);\n return postMultipart<Asset>(ENDPOINTS.assets.upload, form, opts);\n}\n\n/**\n * Same as `uploadAsset` but with progress callbacks. Useful for big\n * uploads where the user expects a `Uploading 12 / 30 MB…` indicator.\n */\nexport async function uploadAssetWithProgress(\n params: UploadAssetParams,\n onProgress: (event: UploadProgressEvent) => void,\n opts: RequestOptions = {},\n): Promise<{ data: Asset; meta: ResponseMeta }> {\n const form = buildUploadForm(params);\n return uploadWithProgress<Asset>(\n ENDPOINTS.assets.upload,\n form,\n onProgress,\n opts,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Read\n// ---------------------------------------------------------------------------\n\nexport interface GetAssetParams {}\n\n/**\n * Read the Asset envelope for a known asset master file id.\n *\n * Use this when you already uploaded through `/assets` and want to\n * re-read the envelope (e.g. after a visibility change). For a generic\n * \"give me an Asset for this file_id\" call (where the file may or may\n * not have been uploaded through the asset pipeline), prefer\n * {@link getAssetForFile}.\n */\nexport async function getAsset(\n fileId: string,\n _params: GetAssetParams = {},\n opts: RequestOptions = {},\n): Promise<{ data: Asset; meta: ResponseMeta }> {\n return getJson<Asset>(ENDPOINTS.assets.detail(fileId), withFileOrganization(fileId, opts));\n}\n\n/**\n * Canonical click-to-render primitive: hand any cld_files row id, get\n * back an Asset envelope (with at least an `original` variant). Works\n * regardless of whether the file was uploaded through the asset\n * pipeline. The hook around this is {@link useFileAsset}.\n */\nexport async function getAssetForFile(\n fileId: string,\n _params: GetAssetParams = {},\n opts: RequestOptions = {},\n): Promise<{ data: Asset; meta: ResponseMeta }> {\n return getJson<Asset>(ENDPOINTS.assets.forFile(fileId), withFileOrganization(fileId, opts));\n}\n\n// ---------------------------------------------------------------------------\n// Mutate\n// ---------------------------------------------------------------------------\n\n/**\n * PATCH `/assets/{file_id}` — change visibility / share / metadata.\n * Returns the new Asset envelope reflecting the change (URLs included).\n */\nexport async function patchAsset(\n fileId: string,\n body: AssetPatchRequest,\n opts: RequestOptions = {},\n): Promise<{ data: Asset; meta: ResponseMeta }> {\n // The backend accepts `share_with` as either a comma-separated string\n // or a list; normalise to the string form so callers can pass either.\n const normalised: AssetPatchRequest = {\n ...body,\n share_with: Array.isArray(body.share_with)\n ? body.share_with.join(\",\")\n : body.share_with,\n };\n return patchJson<Asset, AssetPatchRequest>(\n ENDPOINTS.assets.patch(fileId),\n normalised,\n opts,\n );\n}\n\n/**\n * POST `/assets/{file_id}/variants` — render more variants for an\n * existing asset. Idempotent on `(file_id, variant_key)`: re-rendering\n * a key that already exists is a no-op (the server returns the\n * existing variant).\n */\nexport async function addAssetVariants(\n fileId: string,\n body: AddAssetVariantsRequest,\n opts: RequestOptions = {},\n): Promise<{ data: Asset; meta: ResponseMeta }> {\n return postJson<Asset, AddAssetVariantsRequest>(\n ENDPOINTS.assets.addVariants(fileId),\n body,\n opts,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Presets registry\n// ---------------------------------------------------------------------------\n\n/**\n * List every server-known preset and its variants. Use this to drive\n * picker UIs / admin tooling instead of hardcoding the list in TS.\n */\nexport async function getAssetPresets(\n opts: RequestOptions = {},\n): Promise<{ data: PresetsRegistryResponse; meta: ResponseMeta }> {\n return getJson<PresetsRegistryResponse>(ENDPOINTS.assets.presets, opts);\n}\n\n// ---------------------------------------------------------------------------\n// No-persist preview + PDF compress (E.16 + E.17, matrx-utils v1.1.0)\n// ---------------------------------------------------------------------------\n\n/**\n * Spec for a single preview variant — width/height drives the resize;\n * `format` and `quality` control the output encoding. Sent verbatim to\n * the server (no client-side image processing).\n *\n * `position` and `background_color` are honored when `fit: \"cover\"`:\n * - Anchor strings (`\"top\"`, `\"top-left\"`, `\"entropy\"`, etc.) pick the\n * crop region against one of nine fixed anchors or sharp's smart\n * strategies.\n * - A `{x, y}` point (each in `[0, 1]`) places the crop's centre at\n * that focal point in the source image. The server clamps so the\n * crop never escapes the source bounds.\n *\n * `background_color` is consulted in two places:\n * - `fit: \"contain\"` — letterbox padding colour.\n * - `format: \"jpeg\" | \"avif\"` — alpha-flatten colour (those formats\n * don't carry alpha; transparent pixels become opaque).\n *\n * Field names are snake_case to match the Python multipart body schema\n * verbatim — `position` / `background_color` reach Python as-is inside\n * the `variants_json` array.\n *\n * These are DERIVED from the OpenAPI-generated schema, never hand-mirrored.\n * A backend rename (e.g. `key` → `preset_id`) changes the generated type,\n * which turns every drifted callsite into a compile error in the same PR\n * that runs `pnpm sync-types`. Do NOT redeclare these as standalone\n * interfaces — that severs the link and lets the wire contract drift\n * silently (which is exactly how the `invalid_variant_spec` 400 shipped).\n */\nexport type PreviewVariantSpec = components[\"schemas\"][\"PreviewVariantSpec\"];\n\n/** Named anchor / smart-crop mode / focal point for `fit: \"cover\"`. */\nexport type PreviewVariantPosition = NonNullable<\n PreviewVariantSpec[\"position\"]\n>;\n\n/** One rendered variant in the `/assets/preview*` response. */\nexport type PreviewVariantResult = components[\"schemas\"][\"AssetPreviewVariant\"];\n\n/** Response envelope for `POST /assets/preview` and `.../multipart`. */\nexport type AssetPreviewResult = components[\"schemas\"][\"AssetPreviewResponse\"];\n\nexport interface PdfCompressResult {\n original_size: number;\n compressed_size: number;\n /** 0..1 — fraction of bytes saved. */\n reduction_ratio: number;\n mime_type: string;\n data_url: string | null;\n /** 5-minute row-less scratch URL — the ONLY lawful expiring URL shape. */\n ephemeral_url: string | null;\n /** Seconds until ephemeral_url expires (ephemeral mode only). */\n expires_in?: number | null;\n /** Tier the caller asked for. */\n level_requested?: number | null;\n /** Tier that actually produced the output (cap escalation may raise it). */\n level_used?: number | null;\n /** Whether max_size_bytes was met (null when no cap was sent). */\n cap_satisfied?: boolean | null;\n /** \"inplace\" | \"raster\" — which compression path produced the output. */\n strategy_used?: string | null;\n}\n\n/**\n * `POST /assets/preview/multipart` — render preview variants WITHOUT\n * persisting the master. Image Studio uses this for the drag-and-drop\n * preview flow; the user can then commit-to-save via `POST /assets`.\n *\n * Each variant comes back as either an inline `data_url` (small) or an\n * `ephemeral_url` (5-minute row-less preview scratch).\n */\nexport async function previewAssetMultipart(\n file: File | Blob,\n variants: PreviewVariantSpec[],\n options: { maxInlineBytes?: number } = {},\n opts: RequestOptions = {},\n): Promise<{ data: AssetPreviewResult; meta: ResponseMeta }> {\n const form = new FormData();\n form.append(\"file\", file);\n // Python's multipart body declares `variants_json` (not `variants`) per\n // its OpenAPI schema. Match the wire name verbatim.\n form.append(\"variants_json\", JSON.stringify(variants));\n if (typeof options.maxInlineBytes === \"number\") {\n form.append(\"max_inline_bytes\", String(options.maxInlineBytes));\n }\n // Contract-bound: the response type is DERIVED from the OpenAPI operation\n // for this exact path, not asserted. A backend response-shape change turns\n // every reader of the result into a compile error after `pnpm sync-types`.\n return apiMultipart(ENDPOINTS.assets.previewMultipart, form, opts);\n}\n\n/**\n * `POST /assets/pdf-compress/multipart` — compress a PDF WITHOUT\n * persisting it. Returns `data_url` (≤256 KB) or `ephemeral_url`\n * (5-minute row-less scratch). Replaces the deleted Next.js Sharp/PDF route.\n *\n * @param level minimum compression tier (1..5). Default 3. Server may\n * escalate above this when `maxSizeBytes` forces it.\n * @param maxSizeBytes optional absolute upper bound on output size.\n */\nexport async function compressPdfMultipart(\n file: File | Blob,\n options: { level?: number; maxSizeBytes?: number; maxInlineBytes?: number } = {},\n opts: RequestOptions = {},\n): Promise<{ data: PdfCompressResult; meta: ResponseMeta }> {\n const form = new FormData();\n form.append(\"file\", file);\n form.append(\"level\", String(options.level ?? 3));\n if (typeof options.maxSizeBytes === \"number\") {\n form.append(\"max_size_bytes\", String(options.maxSizeBytes));\n }\n if (typeof options.maxInlineBytes === \"number\") {\n form.append(\"max_inline_bytes\", String(options.maxInlineBytes));\n }\n return postMultipart<PdfCompressResult>(\n ENDPOINTS.assets.pdfCompressMultipart,\n form,\n opts,\n );\n}\n\n/**\n * Materialize a `PreviewVariantResult` / `PdfCompressResult` body that\n * carries either `data_url` or `ephemeral_url` into a `Blob`. Useful when\n * the FE needs the bytes locally (e.g. to wrap in a `File` for further\n * upload).\n */\nexport async function materializeAssetResult(\n result: {\n data_url: string | null;\n ephemeral_url: string | null;\n mime_type: string;\n },\n): Promise<Blob> {\n if (result.data_url) {\n const response = await fetch(result.data_url);\n return response.blob();\n }\n if (result.ephemeral_url) {\n const response = await fetch(result.ephemeral_url);\n if (!response.ok) {\n throw new Error(`Failed to fetch ephemeral asset URL: HTTP ${response.status}`);\n }\n return response.blob();\n }\n throw new Error(\"Asset result has neither data_url nor ephemeral_url\");\n}\n"],"mappings":"AAsBA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP,SAAS,4BAA4B;AASrC,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AA0C7B,SAAS,gBAAgB,QAAqC;AAC5D,QAAM,OAAO,IAAI,SAAS;AAC1B,OAAK,OAAO,QAAQ,OAAO,IAAI;AAC/B,MAAI,OAAO,OAAQ,MAAK,OAAO,UAAU,OAAO,MAAM;AACtD,MAAI,OAAO,OAAQ,MAAK,OAAO,UAAU,OAAO,MAAM;AACtD,MAAI,OAAO,WAAY,MAAK,OAAO,cAAc,OAAO,UAAU;AAClE,MAAI,OAAO,cAAc,QAAW;AAClC,UAAM,SAAS,MAAM,QAAQ,OAAO,SAAS,IACzC,OAAO,UAAU,KAAK,GAAG,IACxB,OAAO;AACZ,QAAI,OAAQ,MAAK,OAAO,cAAc,MAAM;AAAA,EAC9C;AACA,MAAI,OAAO,WAAY,MAAK,OAAO,eAAe,OAAO,UAAU;AACnE,MAAI,OAAO,0BAA0B;AACnC,SAAK;AAAA,MACH;AAAA,MACA,OAAO,wBAAwB,SAAS;AAAA,IAC1C;AACF,MAAI,OAAO;AACT,SAAK,OAAO,iBAAiB,KAAK,UAAU,OAAO,QAAQ,CAAC;AAC9D,MAAI,OAAO,kBAAkB,OAAO,eAAe;AACjD,SAAK,OAAO,wBAAwB,KAAK,UAAU,OAAO,cAAc,CAAC;AAC3E,SAAO;AACT;AAOA,eAAsB,YACpB,QACA,OAAuB,CAAC,GACsB;AAC9C,QAAM,OAAO,gBAAgB,MAAM;AACnC,SAAO,cAAqB,UAAU,OAAO,QAAQ,MAAM,IAAI;AACjE;AAMA,eAAsB,wBACpB,QACA,YACA,OAAuB,CAAC,GACsB;AAC9C,QAAM,OAAO,gBAAgB,MAAM;AACnC,SAAO;AAAA,IACL,UAAU,OAAO;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAiBA,eAAsB,SACpB,QACA,UAA0B,CAAC,GAC3B,OAAuB,CAAC,GACsB;AAC9C,SAAO,QAAe,UAAU,OAAO,OAAO,MAAM,GAAG,qBAAqB,QAAQ,IAAI,CAAC;AAC3F;AAQA,eAAsB,gBACpB,QACA,UAA0B,CAAC,GAC3B,OAAuB,CAAC,GACsB;AAC9C,SAAO,QAAe,UAAU,OAAO,QAAQ,MAAM,GAAG,qBAAqB,QAAQ,IAAI,CAAC;AAC5F;AAUA,eAAsB,WACpB,QACA,MACA,OAAuB,CAAC,GACsB;AAG9C,QAAM,aAAgC;AAAA,IACpC,GAAG;AAAA,IACH,YAAY,MAAM,QAAQ,KAAK,UAAU,IACrC,KAAK,WAAW,KAAK,GAAG,IACxB,KAAK;AAAA,EACX;AACA,SAAO;AAAA,IACL,UAAU,OAAO,MAAM,MAAM;AAAA,IAC7B;AAAA,IACA;AAAA,EACF;AACF;AAQA,eAAsB,iBACpB,QACA,MACA,OAAuB,CAAC,GACsB;AAC9C,SAAO;AAAA,IACL,UAAU,OAAO,YAAY,MAAM;AAAA,IACnC;AAAA,IACA;AAAA,EACF;AACF;AAUA,eAAsB,gBACpB,OAAuB,CAAC,GACwC;AAChE,SAAO,QAAiC,UAAU,OAAO,SAAS,IAAI;AACxE;AA6EA,eAAsB,sBACpB,MACA,UACA,UAAuC,CAAC,GACxC,OAAuB,CAAC,GACmC;AAC3D,QAAM,OAAO,IAAI,SAAS;AAC1B,OAAK,OAAO,QAAQ,IAAI;AAGxB,OAAK,OAAO,iBAAiB,KAAK,UAAU,QAAQ,CAAC;AACrD,MAAI,OAAO,QAAQ,mBAAmB,UAAU;AAC9C,SAAK,OAAO,oBAAoB,OAAO,QAAQ,cAAc,CAAC;AAAA,EAChE;AAIA,SAAO,aAAa,UAAU,OAAO,kBAAkB,MAAM,IAAI;AACnE;AAWA,eAAsB,qBACpB,MACA,UAA8E,CAAC,GAC/E,OAAuB,CAAC,GACkC;AAC1D,QAAM,OAAO,IAAI,SAAS;AAC1B,OAAK,OAAO,QAAQ,IAAI;AACxB,OAAK,OAAO,SAAS,OAAO,QAAQ,SAAS,CAAC,CAAC;AAC/C,MAAI,OAAO,QAAQ,iBAAiB,UAAU;AAC5C,SAAK,OAAO,kBAAkB,OAAO,QAAQ,YAAY,CAAC;AAAA,EAC5D;AACA,MAAI,OAAO,QAAQ,mBAAmB,UAAU;AAC9C,SAAK,OAAO,oBAAoB,OAAO,QAAQ,cAAc,CAAC;AAAA,EAChE;AACA,SAAO;AAAA,IACL,UAAU,OAAO;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AACF;AAQA,eAAsB,uBACpB,QAKe;AACf,MAAI,OAAO,UAAU;AACnB,UAAM,WAAW,MAAM,MAAM,OAAO,QAAQ;AAC5C,WAAO,SAAS,KAAK;AAAA,EACvB;AACA,MAAI,OAAO,eAAe;AACxB,UAAM,WAAW,MAAM,MAAM,OAAO,aAAa;AACjD,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,MAAM,6CAA6C,SAAS,MAAM,EAAE;AAAA,IAChF;AACA,WAAO,SAAS,KAAK;AAAA,EACvB;AACA,QAAM,IAAI,MAAM,qDAAqD;AACvE;","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/api/direct.ts
|
|
3
|
+
*
|
|
4
|
+
* **Canonical data path for cloud-files.** Pure UI↔DB operations go DIRECT via
|
|
5
|
+
* supabase-js — never through the Python REST `/files/*` API. Our FE has
|
|
6
|
+
* Supabase; the REST layer exists only for consumers without it (the extension,
|
|
7
|
+
* external clients) and for byte-bearing ops (upload/download/sign). Routing a
|
|
8
|
+
* plain DB read/write through Python is two wasted hops through a slow server.
|
|
9
|
+
* See CLAUDE.md "Data flow" + features/files/CLOUD_FILES_RPC_DISPOSITIONS.md.
|
|
10
|
+
*
|
|
11
|
+
* The cloud-files tables live in the `files` Postgres schema (reach via
|
|
12
|
+
* `filesDb()`); the operations here are exposed as `public` SECURITY DEFINER
|
|
13
|
+
* RPCs that self-authorize against `auth.uid()`. The REST modules
|
|
14
|
+
* (`./files`, `./folders`, …) remain ONLY for byte-bearing ops + non-Supabase
|
|
15
|
+
* consumers — do not add pure-DB ops there.
|
|
16
|
+
*/
|
|
17
|
+
import type { StorageUsageResponse } from "../types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Read the authed user's tier + storage usage **directly** via the
|
|
20
|
+
* `get_usage_status` RPC (replaces `GET /files/usage`). The RPC enforces
|
|
21
|
+
* `auth.uid() = p_user_id`. Flattens the RPC's `{ limits, usage }` envelope
|
|
22
|
+
* into the flat `StorageUsageResponse` the UI consumes (same shape the Python
|
|
23
|
+
* endpoint produced).
|
|
24
|
+
*/
|
|
25
|
+
export declare function getUsageStatusDirect(userId: string, opts?: {
|
|
26
|
+
isGuest?: boolean;
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
}): Promise<StorageUsageResponse>;
|
|
29
|
+
/** Soft-delete a file (trash). Returns true if a row was deleted. */
|
|
30
|
+
export declare function softDeleteFileDirect(fileId: string): Promise<boolean>;
|
|
31
|
+
/** Soft-delete a folder + its descendant subtree (cascades in SQL). */
|
|
32
|
+
export declare function softDeleteFolderDirect(folderId: string): Promise<{
|
|
33
|
+
folders: number;
|
|
34
|
+
files: number;
|
|
35
|
+
links: number;
|
|
36
|
+
}>;
|
|
37
|
+
/** Restore a soft-deleted file from trash. Returns true if restored. */
|
|
38
|
+
export declare function restoreFileDirect(fileId: string): Promise<boolean>;
|
|
39
|
+
/** Restore a soft-deleted folder + its descendant subtree. */
|
|
40
|
+
export declare function restoreFolderDirect(folderId: string): Promise<{
|
|
41
|
+
folders: number;
|
|
42
|
+
files: number;
|
|
43
|
+
}>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { supabase } from "../host/supabase.js";
|
|
2
|
+
import { pgErrorToError } from "@ai-matrx/data";
|
|
3
|
+
import { runWithSessionRetry } from "../host/supabase.js";
|
|
4
|
+
async function getUsageStatusDirect(userId, opts = {}) {
|
|
5
|
+
const { data, error } = await runWithSessionRetry(() => {
|
|
6
|
+
let q = supabase.rpc("get_usage_status", {
|
|
7
|
+
p_user_id: userId,
|
|
8
|
+
p_is_guest: opts.isGuest ?? false
|
|
9
|
+
});
|
|
10
|
+
if (opts.signal) q = q.abortSignal(opts.signal);
|
|
11
|
+
return q;
|
|
12
|
+
});
|
|
13
|
+
if (error) throw pgErrorToError(error);
|
|
14
|
+
const { limits, usage } = data;
|
|
15
|
+
return {
|
|
16
|
+
tier_id: limits.tier_id,
|
|
17
|
+
tier_name: limits.tier_name,
|
|
18
|
+
is_blocked: limits.is_blocked,
|
|
19
|
+
blocked_reason: limits.blocked_reason,
|
|
20
|
+
bytes_used: usage.bytes_used,
|
|
21
|
+
files_count: usage.files_count,
|
|
22
|
+
daily_upload_count: usage.daily_upload_count,
|
|
23
|
+
daily_upload_bytes: usage.daily_upload_bytes,
|
|
24
|
+
max_storage_bytes: limits.max_storage_bytes,
|
|
25
|
+
max_file_size_bytes: limits.max_file_size_bytes,
|
|
26
|
+
max_files: limits.max_files,
|
|
27
|
+
max_versions_per_file: limits.max_versions_per_file,
|
|
28
|
+
max_daily_uploads: limits.max_daily_uploads,
|
|
29
|
+
max_daily_upload_bytes: limits.max_daily_upload_bytes,
|
|
30
|
+
max_bulk_items: limits.max_bulk_items,
|
|
31
|
+
rate_limit_uploads_per_min: limits.rate_limit_uploads_per_min,
|
|
32
|
+
rate_limit_downloads_per_min: limits.rate_limit_downloads_per_min,
|
|
33
|
+
features: limits.features ?? {},
|
|
34
|
+
ledger_measured: Boolean(usage.user_id),
|
|
35
|
+
ledger_measured_at: usage.updated_at ?? null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function softDeleteFileDirect(fileId) {
|
|
39
|
+
const { data, error } = await supabase.rpc("soft_delete_file", {
|
|
40
|
+
p_file_id: fileId
|
|
41
|
+
});
|
|
42
|
+
if (error) throw pgErrorToError(error);
|
|
43
|
+
return Boolean(data);
|
|
44
|
+
}
|
|
45
|
+
async function softDeleteFolderDirect(folderId) {
|
|
46
|
+
const { data, error } = await supabase.rpc("soft_delete_folder", {
|
|
47
|
+
p_folder_id: folderId
|
|
48
|
+
});
|
|
49
|
+
if (error) throw pgErrorToError(error);
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
async function restoreFileDirect(fileId) {
|
|
53
|
+
const { data, error } = await supabase.rpc("restore_file", {
|
|
54
|
+
p_file_id: fileId
|
|
55
|
+
});
|
|
56
|
+
if (error) throw pgErrorToError(error);
|
|
57
|
+
return Boolean(data);
|
|
58
|
+
}
|
|
59
|
+
async function restoreFolderDirect(folderId) {
|
|
60
|
+
const { data, error } = await supabase.rpc("restore_folder", {
|
|
61
|
+
p_folder_id: folderId
|
|
62
|
+
});
|
|
63
|
+
if (error) throw pgErrorToError(error);
|
|
64
|
+
return data;
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
getUsageStatusDirect,
|
|
68
|
+
restoreFileDirect,
|
|
69
|
+
restoreFolderDirect,
|
|
70
|
+
softDeleteFileDirect,
|
|
71
|
+
softDeleteFolderDirect
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=direct.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/api/direct.ts"],"sourcesContent":["/**\n * features/files/api/direct.ts\n *\n * **Canonical data path for cloud-files.** Pure UI↔DB operations go DIRECT via\n * supabase-js — never through the Python REST `/files/*` API. Our FE has\n * Supabase; the REST layer exists only for consumers without it (the extension,\n * external clients) and for byte-bearing ops (upload/download/sign). Routing a\n * plain DB read/write through Python is two wasted hops through a slow server.\n * See CLAUDE.md \"Data flow\" + features/files/CLOUD_FILES_RPC_DISPOSITIONS.md.\n *\n * The cloud-files tables live in the `files` Postgres schema (reach via\n * `filesDb()`); the operations here are exposed as `public` SECURITY DEFINER\n * RPCs that self-authorize against `auth.uid()`. The REST modules\n * (`./files`, `./folders`, …) remain ONLY for byte-bearing ops + non-Supabase\n * consumers — do not add pure-DB ops there.\n */\n\nimport { supabase } from \"../host/supabase\";\nimport { pgErrorToError } from \"@ai-matrx/data\";\nimport { runWithSessionRetry } from \"../host/supabase\";\nimport type { StorageUsageResponse } from \"../types\";\n\n// ---------------------------------------------------------------------------\n// Usage / quota\n// ---------------------------------------------------------------------------\n\n/** Nested shape returned by the `get_usage_status` RPC. */\ninterface UsageStatusRpc {\n limits: {\n tier_id: string;\n tier_name: string;\n is_blocked: boolean;\n blocked_reason: string | null;\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> | null;\n };\n /**\n * Either a real `files.user_storage_usage` row (which carries `user_id` and\n * `updated_at`) or the zero object the RPC synthesizes when no row exists.\n * The presence of `user_id` is the ONLY difference, and it is what tells the\n * meter whether the number was measured or invented.\n */\n usage: {\n user_id?: string | null;\n updated_at?: string | null;\n bytes_used: number;\n files_count: number;\n daily_upload_count: number;\n daily_upload_bytes: number;\n };\n}\n\n/**\n * Read the authed user's tier + storage usage **directly** via the\n * `get_usage_status` RPC (replaces `GET /files/usage`). The RPC enforces\n * `auth.uid() = p_user_id`. Flattens the RPC's `{ limits, usage }` envelope\n * into the flat `StorageUsageResponse` the UI consumes (same shape the Python\n * endpoint produced).\n */\nexport async function getUsageStatusDirect(\n userId: string,\n opts: { isGuest?: boolean; signal?: AbortSignal } = {},\n): Promise<StorageUsageResponse> {\n const { data, error } = await runWithSessionRetry(() => {\n let q = supabase.rpc(\"get_usage_status\", {\n p_user_id: userId,\n p_is_guest: opts.isGuest ?? false,\n });\n if (opts.signal) q = q.abortSignal(opts.signal);\n return q;\n });\n if (error) throw pgErrorToError(error);\n\n // get_usage_status returns Json directly (no row schema) — sanctioned\n // Json-direct cast per the type-safety skill's supabase-patterns.md.\n const { limits, usage } = data as unknown as UsageStatusRpc;\n return {\n tier_id: limits.tier_id,\n tier_name: limits.tier_name,\n is_blocked: limits.is_blocked,\n blocked_reason: limits.blocked_reason,\n bytes_used: usage.bytes_used,\n files_count: usage.files_count,\n daily_upload_count: usage.daily_upload_count,\n daily_upload_bytes: usage.daily_upload_bytes,\n max_storage_bytes: limits.max_storage_bytes,\n max_file_size_bytes: limits.max_file_size_bytes,\n max_files: limits.max_files,\n max_versions_per_file: limits.max_versions_per_file,\n max_daily_uploads: limits.max_daily_uploads,\n max_daily_upload_bytes: limits.max_daily_upload_bytes,\n max_bulk_items: limits.max_bulk_items,\n rate_limit_uploads_per_min: limits.rate_limit_uploads_per_min,\n rate_limit_downloads_per_min: limits.rate_limit_downloads_per_min,\n features: limits.features ?? {},\n ledger_measured: Boolean(usage.user_id),\n ledger_measured_at: usage.updated_at ?? null,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Soft delete / restore (metadata-only — NO byte cleanup, so direct)\n// ---------------------------------------------------------------------------\n//\n// There is no hard delete of a person's file or folder (delete means archive,\n// 2026-09-27); version prune purges S3 objects, so it stays on the Python path.\n// The mutation RPCs are hardened (auth.uid() + iam.has_access) — see\n// migrations/cld_files_mutation_rpc_auth_hardening.sql. The realtime middleware\n// applies a soft-delete echo idempotently (deletedAt → removeFile), so losing\n// the request-id echo dedup is harmless here.\n\n/** Soft-delete a file (trash). Returns true if a row was deleted. */\nexport async function softDeleteFileDirect(fileId: string): Promise<boolean> {\n const { data, error } = await supabase.rpc(\"soft_delete_file\", {\n p_file_id: fileId,\n });\n if (error) throw pgErrorToError(error);\n return Boolean(data);\n}\n\n/** Soft-delete a folder + its descendant subtree (cascades in SQL). */\nexport async function softDeleteFolderDirect(\n folderId: string,\n): Promise<{ folders: number; files: number; links: number }> {\n const { data, error } = await supabase.rpc(\"soft_delete_folder\", {\n p_folder_id: folderId,\n });\n if (error) throw pgErrorToError(error);\n // soft_delete_folder returns Json directly (no row schema) — sanctioned\n // Json-direct cast per the type-safety skill's supabase-patterns.md.\n return data as unknown as { folders: number; files: number; links: number };\n}\n\n/** Restore a soft-deleted file from trash. Returns true if restored. */\nexport async function restoreFileDirect(fileId: string): Promise<boolean> {\n const { data, error } = await supabase.rpc(\"restore_file\", {\n p_file_id: fileId,\n });\n if (error) throw pgErrorToError(error);\n return Boolean(data);\n}\n\n/** Restore a soft-deleted folder + its descendant subtree. */\nexport async function restoreFolderDirect(\n folderId: string,\n): Promise<{ folders: number; files: number }> {\n const { data, error } = await supabase.rpc(\"restore_folder\", {\n p_folder_id: folderId,\n });\n if (error) throw pgErrorToError(error);\n // restore_folder returns Json directly (no row schema) — sanctioned\n // Json-direct cast per the type-safety skill's supabase-patterns.md.\n return data as unknown as { folders: number; files: number };\n}\n"],"mappings":"AAiBA,SAAS,gBAAgB;AACzB,SAAS,sBAAsB;AAC/B,SAAS,2BAA2B;AAgDpC,eAAsB,qBACpB,QACA,OAAoD,CAAC,GACtB;AAC/B,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,oBAAoB,MAAM;AACtD,QAAI,IAAI,SAAS,IAAI,oBAAoB;AAAA,MACvC,WAAW;AAAA,MACX,YAAY,KAAK,WAAW;AAAA,IAC9B,CAAC;AACD,QAAI,KAAK,OAAQ,KAAI,EAAE,YAAY,KAAK,MAAM;AAC9C,WAAO;AAAA,EACT,CAAC;AACD,MAAI,MAAO,OAAM,eAAe,KAAK;AAIrC,QAAM,EAAE,QAAQ,MAAM,IAAI;AAC1B,SAAO;AAAA,IACL,SAAS,OAAO;AAAA,IAChB,WAAW,OAAO;AAAA,IAClB,YAAY,OAAO;AAAA,IACnB,gBAAgB,OAAO;AAAA,IACvB,YAAY,MAAM;AAAA,IAClB,aAAa,MAAM;AAAA,IACnB,oBAAoB,MAAM;AAAA,IAC1B,oBAAoB,MAAM;AAAA,IAC1B,mBAAmB,OAAO;AAAA,IAC1B,qBAAqB,OAAO;AAAA,IAC5B,WAAW,OAAO;AAAA,IAClB,uBAAuB,OAAO;AAAA,IAC9B,mBAAmB,OAAO;AAAA,IAC1B,wBAAwB,OAAO;AAAA,IAC/B,gBAAgB,OAAO;AAAA,IACvB,4BAA4B,OAAO;AAAA,IACnC,8BAA8B,OAAO;AAAA,IACrC,UAAU,OAAO,YAAY,CAAC;AAAA,IAC9B,iBAAiB,QAAQ,MAAM,OAAO;AAAA,IACtC,oBAAoB,MAAM,cAAc;AAAA,EAC1C;AACF;AAcA,eAAsB,qBAAqB,QAAkC;AAC3E,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,SAAS,IAAI,oBAAoB;AAAA,IAC7D,WAAW;AAAA,EACb,CAAC;AACD,MAAI,MAAO,OAAM,eAAe,KAAK;AACrC,SAAO,QAAQ,IAAI;AACrB;AAGA,eAAsB,uBACpB,UAC4D;AAC5D,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,SAAS,IAAI,sBAAsB;AAAA,IAC/D,aAAa;AAAA,EACf,CAAC;AACD,MAAI,MAAO,OAAM,eAAe,KAAK;AAGrC,SAAO;AACT;AAGA,eAAsB,kBAAkB,QAAkC;AACxE,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,SAAS,IAAI,gBAAgB;AAAA,IACzD,WAAW;AAAA,EACb,CAAC;AACD,MAAI,MAAO,OAAM,eAAe,KAAK;AACrC,SAAO,QAAQ,IAAI;AACrB;AAGA,eAAsB,oBACpB,UAC6C;AAC7C,QAAM,EAAE,MAAM,MAAM,IAAI,MAAM,SAAS,IAAI,kBAAkB;AAAA,IAC3D,aAAa;AAAA,EACf,CAAC;AACD,MAAI,MAAO,OAAM,eAAe,KAAK;AAGrC,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/api/fileOrganization.ts — LANE GATES-TAIL (VERIFIER-21 #2)
|
|
3
|
+
*
|
|
4
|
+
* A REQUEST ABOUT ONE FILE CARRIES THAT FILE'S ORGANIZATION — READ FROM THE FILE, NEVER FROM
|
|
5
|
+
* THE PERSON'S PICKER.
|
|
6
|
+
*
|
|
7
|
+
* The defect: in a fresh session (header "Choose org") `/files/f/<id>` named the file and then
|
|
8
|
+
* said "Couldn't load this file — GET /files/<id>/download → HTTP 400", because every per-file
|
|
9
|
+
* request to the files service went out with no `X-Organization-Id` and the service refused it
|
|
10
|
+
* ("Choose the organization you're working in"). Picking an organization in the shell made it
|
|
11
|
+
* open. Access is personal (Arman, 2026-09-23): whether ONE object opens is never decided by
|
|
12
|
+
* which organization is selected, and an object page resolves its organization FROM THE OBJECT.
|
|
13
|
+
*
|
|
14
|
+
* Where the file's organization comes from, in order:
|
|
15
|
+
* 1. an organization the caller already named (`opts.organizationId`) — always wins;
|
|
16
|
+
* 2. an organization remembered for this file id — the file page seeds it from the row it
|
|
17
|
+
* read under RLS as the person (`files.files.organization_id`), and every per-file read
|
|
18
|
+
* that answers records the row's own `organization_id`;
|
|
19
|
+
* 3. the file row already in the files slice (`cloudFiles.filesById[id].organizationId`),
|
|
20
|
+
* loaded by a list or tree under the person's access.
|
|
21
|
+
* With none of these the request goes as before (the transport's own rule decides); nothing
|
|
22
|
+
* here ever reads the active selection, so nothing here can guess.
|
|
23
|
+
*/
|
|
24
|
+
import type { RequestOptions } from "../host/python-client.js";
|
|
25
|
+
/** Record the organization a file lives in, as the file's own row says. */
|
|
26
|
+
export declare function rememberFileOrganization(fileId: string | null | undefined, organizationId: string | null | undefined): void;
|
|
27
|
+
/** Record it from any per-file answer that carries the row (`FileRecordApi` and kin). */
|
|
28
|
+
export declare function rememberFileOrganizationFrom(record: unknown): void;
|
|
29
|
+
/** The organization this file lives in, as far as the file itself has told us; null otherwise. */
|
|
30
|
+
export declare function fileOrganizationId(fileId: string): string | null;
|
|
31
|
+
/** `opts` with the file's own organization filled in when the caller named none. */
|
|
32
|
+
export declare function withFileOrganization<T extends RequestOptions>(fileId: string, opts: T): T;
|
|
33
|
+
/** Test seam: forget every remembered file organization. */
|
|
34
|
+
export declare function __resetFileOrganizationsForTest(): void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getStoreSingleton } from "../host/store.js";
|
|
2
|
+
const remembered = /* @__PURE__ */ new Map();
|
|
3
|
+
function isOrganizationId(value) {
|
|
4
|
+
return typeof value === "string" && value.length > 0;
|
|
5
|
+
}
|
|
6
|
+
function rememberFileOrganization(fileId, organizationId) {
|
|
7
|
+
if (!fileId || !isOrganizationId(organizationId)) return;
|
|
8
|
+
remembered.set(fileId, organizationId);
|
|
9
|
+
}
|
|
10
|
+
function rememberFileOrganizationFrom(record) {
|
|
11
|
+
if (!record || typeof record !== "object") return;
|
|
12
|
+
const row = record;
|
|
13
|
+
const id = typeof row.id === "string" ? row.id : typeof row.file_id === "string" ? row.file_id : null;
|
|
14
|
+
rememberFileOrganization(id, row.organization_id);
|
|
15
|
+
}
|
|
16
|
+
function fileOrganizationId(fileId) {
|
|
17
|
+
const known = remembered.get(fileId);
|
|
18
|
+
if (known) return known;
|
|
19
|
+
const state = getStoreSingleton()?.getState();
|
|
20
|
+
const fromSlice = state?.cloudFiles?.filesById?.[fileId]?.organizationId;
|
|
21
|
+
return isOrganizationId(fromSlice) ? fromSlice : null;
|
|
22
|
+
}
|
|
23
|
+
function withFileOrganization(fileId, opts) {
|
|
24
|
+
if (opts.organizationId) return opts;
|
|
25
|
+
const organizationId = fileOrganizationId(fileId);
|
|
26
|
+
return organizationId ? { ...opts, organizationId } : opts;
|
|
27
|
+
}
|
|
28
|
+
function __resetFileOrganizationsForTest() {
|
|
29
|
+
remembered.clear();
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
__resetFileOrganizationsForTest,
|
|
33
|
+
fileOrganizationId,
|
|
34
|
+
rememberFileOrganization,
|
|
35
|
+
rememberFileOrganizationFrom,
|
|
36
|
+
withFileOrganization
|
|
37
|
+
};
|
|
38
|
+
//# sourceMappingURL=fileOrganization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/api/fileOrganization.ts"],"sourcesContent":["/**\n * features/files/api/fileOrganization.ts — LANE GATES-TAIL (VERIFIER-21 #2)\n *\n * A REQUEST ABOUT ONE FILE CARRIES THAT FILE'S ORGANIZATION — READ FROM THE FILE, NEVER FROM\n * THE PERSON'S PICKER.\n *\n * The defect: in a fresh session (header \"Choose org\") `/files/f/<id>` named the file and then\n * said \"Couldn't load this file — GET /files/<id>/download → HTTP 400\", because every per-file\n * request to the files service went out with no `X-Organization-Id` and the service refused it\n * (\"Choose the organization you're working in\"). Picking an organization in the shell made it\n * open. Access is personal (Arman, 2026-09-23): whether ONE object opens is never decided by\n * which organization is selected, and an object page resolves its organization FROM THE OBJECT.\n *\n * Where the file's organization comes from, in order:\n * 1. an organization the caller already named (`opts.organizationId`) — always wins;\n * 2. an organization remembered for this file id — the file page seeds it from the row it\n * read under RLS as the person (`files.files.organization_id`), and every per-file read\n * that answers records the row's own `organization_id`;\n * 3. the file row already in the files slice (`cloudFiles.filesById[id].organizationId`),\n * loaded by a list or tree under the person's access.\n * With none of these the request goes as before (the transport's own rule decides); nothing\n * here ever reads the active selection, so nothing here can guess.\n */\n\nimport type { RequestOptions } from \"../host/python-client\";\nimport { getStoreSingleton } from \"../host/store\";\n\nconst remembered = new Map<string, string>();\n\nfunction isOrganizationId(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\n/** Record the organization a file lives in, as the file's own row says. */\nexport function rememberFileOrganization(\n fileId: string | null | undefined,\n organizationId: string | null | undefined,\n): void {\n if (!fileId || !isOrganizationId(organizationId)) return;\n remembered.set(fileId, organizationId);\n}\n\n/** Record it from any per-file answer that carries the row (`FileRecordApi` and kin). */\nexport function rememberFileOrganizationFrom(record: unknown): void {\n if (!record || typeof record !== \"object\") return;\n const row = record as { id?: unknown; file_id?: unknown; organization_id?: unknown };\n const id = typeof row.id === \"string\" ? row.id : typeof row.file_id === \"string\" ? row.file_id : null;\n rememberFileOrganization(id, row.organization_id as string | null | undefined);\n}\n\n/** The organization this file lives in, as far as the file itself has told us; null otherwise. */\nexport function fileOrganizationId(fileId: string): string | null {\n const known = remembered.get(fileId);\n if (known) return known;\n const state = getStoreSingleton()?.getState() as\n | { cloudFiles?: { filesById?: Record<string, { organizationId?: string | null } | undefined> } }\n | undefined;\n const fromSlice = state?.cloudFiles?.filesById?.[fileId]?.organizationId;\n return isOrganizationId(fromSlice) ? fromSlice : null;\n}\n\n/** `opts` with the file's own organization filled in when the caller named none. */\nexport function withFileOrganization<T extends RequestOptions>(fileId: string, opts: T): T {\n if (opts.organizationId) return opts;\n const organizationId = fileOrganizationId(fileId);\n return organizationId ? { ...opts, organizationId } : opts;\n}\n\n/** Test seam: forget every remembered file organization. */\nexport function __resetFileOrganizationsForTest(): void {\n remembered.clear();\n}\n"],"mappings":"AAyBA,SAAS,yBAAyB;AAElC,MAAM,aAAa,oBAAI,IAAoB;AAE3C,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AAGO,SAAS,yBACd,QACA,gBACM;AACN,MAAI,CAAC,UAAU,CAAC,iBAAiB,cAAc,EAAG;AAClD,aAAW,IAAI,QAAQ,cAAc;AACvC;AAGO,SAAS,6BAA6B,QAAuB;AAClE,MAAI,CAAC,UAAU,OAAO,WAAW,SAAU;AAC3C,QAAM,MAAM;AACZ,QAAM,KAAK,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;AACjG,2BAAyB,IAAI,IAAI,eAA4C;AAC/E;AAGO,SAAS,mBAAmB,QAA+B;AAChE,QAAM,QAAQ,WAAW,IAAI,MAAM;AACnC,MAAI,MAAO,QAAO;AAClB,QAAM,QAAQ,kBAAkB,GAAG,SAAS;AAG5C,QAAM,YAAY,OAAO,YAAY,YAAY,MAAM,GAAG;AAC1D,SAAO,iBAAiB,SAAS,IAAI,YAAY;AACnD;AAGO,SAAS,qBAA+C,QAAgB,MAAY;AACzF,MAAI,KAAK,eAAgB,QAAO;AAChC,QAAM,iBAAiB,mBAAmB,MAAM;AAChD,SAAO,iBAAiB,EAAE,GAAG,MAAM,eAAe,IAAI;AACxD;AAGO,SAAS,kCAAwC;AACtD,aAAW,MAAM;AACnB;","names":[]}
|