@dxos/plugin-file 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.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/PLUGIN.mdl +140 -35
- package/dist/lib/FileCapabilities.mjs +1 -2
- package/dist/lib/FileCapabilities.mjs.map +1 -1
- package/dist/lib/FileLimits.mjs +30 -23
- package/dist/lib/FileLimits.mjs.map +1 -1
- package/dist/lib/FileOperation.mjs +95 -2
- package/dist/lib/FileOperation.mjs.map +1 -1
- package/dist/lib/FileSkill.mjs +65 -0
- package/dist/lib/FileSkill.mjs.map +1 -0
- package/dist/lib/StagedUpload.mjs +68 -0
- package/dist/lib/StagedUpload.mjs.map +1 -0
- package/dist/lib/assets/plugin-file-CT0bdZfZ.css +86 -0
- package/dist/lib/capabilities.browser.mjs +78 -0
- package/dist/lib/capabilities.browser.mjs.map +1 -0
- package/dist/lib/capabilities.mjs +26 -10
- package/dist/lib/capabilities.mjs.map +1 -1
- package/dist/lib/capabilities.node.mjs +19 -10
- package/dist/lib/capabilities.node.mjs.map +1 -1
- package/dist/lib/capabilities.tauri.mjs +78 -0
- package/dist/lib/capabilities.tauri.mjs.map +1 -0
- package/dist/lib/capabilities.workerd.mjs +19 -10
- package/dist/lib/capabilities.workerd.mjs.map +1 -1
- package/dist/lib/chunk-FileArticle.mjs +19 -50
- package/dist/lib/chunk-FileArticle.mjs.map +1 -1
- package/dist/lib/chunk-FileCard.mjs +56 -0
- package/dist/lib/chunk-FileCard.mjs.map +1 -0
- package/dist/lib/chunk-FileProperties.mjs +92 -0
- package/dist/lib/chunk-FileProperties.mjs.map +1 -0
- package/dist/lib/chunk-PLUGIN.mjs +6 -0
- package/dist/lib/chunk-PLUGIN.mjs.map +1 -0
- package/dist/lib/chunk-create-from-source.mjs +80 -0
- package/dist/lib/chunk-create-from-source.mjs.map +1 -0
- package/dist/lib/chunk-create-from-upload.mjs +36 -0
- package/dist/lib/chunk-create-from-upload.mjs.map +1 -0
- package/dist/lib/chunk-create-object.mjs +1 -1
- package/dist/lib/chunk-create.mjs +18 -30
- package/dist/lib/chunk-create.mjs.map +1 -1
- package/dist/lib/chunk-edge-backend.mjs +2 -2
- package/dist/lib/chunk-edge-backend.mjs.map +1 -1
- package/dist/lib/chunk-file-uploader.mjs +4 -4
- package/dist/lib/chunk-file-uploader.mjs.map +1 -1
- package/dist/lib/chunk-inline-backend.mjs +2 -2
- package/dist/lib/chunk-inline-backend.mjs.map +1 -1
- package/dist/lib/chunk-markdown-extension.mjs +9 -8
- package/dist/lib/chunk-markdown-extension.mjs.map +1 -1
- package/dist/lib/chunk-operation-handler.mjs +1 -1
- package/dist/lib/chunk-react-surface.mjs +23 -13
- package/dist/lib/chunk-react-surface.mjs.map +1 -1
- package/dist/lib/chunk-read.mjs +1 -1
- package/dist/lib/chunk-resolve-download.mjs +33 -0
- package/dist/lib/chunk-resolve-download.mjs.map +1 -0
- package/dist/lib/chunk-settings.mjs +3 -3
- package/dist/lib/chunk-settings.mjs.map +1 -1
- package/dist/lib/chunk-skill-definition.mjs +1 -1
- package/dist/lib/chunk-useFileUrl.mjs +56 -0
- package/dist/lib/chunk-useFileUrl.mjs.map +1 -0
- package/dist/lib/components.mjs +816 -31
- package/dist/lib/components.mjs.map +1 -1
- package/dist/lib/containers.mjs +25 -15
- package/dist/lib/containers.mjs.map +1 -1
- package/dist/lib/index.mjs +2 -2
- package/dist/lib/meta.mjs.map +1 -1
- package/dist/lib/operations.mjs +7 -1
- package/dist/lib/operations.mjs.map +1 -1
- package/dist/lib/skills.mjs +2 -30
- package/dist/lib/translations.mjs +26 -3
- package/dist/lib/translations.mjs.map +1 -1
- package/dist/types/src/FilePlugin.workerd.test.d.ts +2 -0
- package/dist/types/src/FilePlugin.workerd.test.d.ts.map +1 -0
- package/dist/types/src/StagedUpload.d.ts +82 -0
- package/dist/types/src/StagedUpload.d.ts.map +1 -0
- package/dist/types/src/capabilities/FileUploadField.d.ts +2 -2
- package/dist/types/src/capabilities/FileUploadField.d.ts.map +1 -1
- package/dist/types/src/capabilities/gen/browser.d.ts +14 -0
- package/dist/types/src/capabilities/gen/browser.d.ts.map +1 -0
- package/dist/types/src/capabilities/gen/node.d.ts +1 -1
- package/dist/types/src/capabilities/gen/node.d.ts.map +1 -1
- package/dist/types/src/capabilities/gen/tauri.d.ts +14 -0
- package/dist/types/src/capabilities/gen/tauri.d.ts.map +1 -0
- package/dist/types/src/capabilities/gen/workerd.d.ts +1 -1
- package/dist/types/src/capabilities/gen/workerd.d.ts.map +1 -1
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/components/FileInput/FileInput.d.ts +1 -3
- package/dist/types/src/components/FileInput/FileInput.d.ts.map +1 -1
- package/dist/types/src/components/FileInput/index.d.ts +1 -1
- package/dist/types/src/components/FileInput/index.d.ts.map +1 -1
- package/dist/types/src/components/PdfCanvas/PdfCanvas.d.ts +44 -0
- package/dist/types/src/components/PdfCanvas/PdfCanvas.d.ts.map +1 -0
- package/dist/types/src/components/PdfCanvas/PdfCanvas.stories.d.ts +111 -0
- package/dist/types/src/components/PdfCanvas/PdfCanvas.stories.d.ts.map +1 -0
- package/dist/types/src/components/PdfCanvas/index.d.ts +2 -0
- package/dist/types/src/components/PdfCanvas/index.d.ts.map +1 -0
- package/dist/types/src/components/PdfCanvas/pdf-search.d.ts +38 -0
- package/dist/types/src/components/PdfCanvas/pdf-search.d.ts.map +1 -0
- package/dist/types/src/components/Preview/Preview.d.ts +39 -0
- package/dist/types/src/components/Preview/Preview.d.ts.map +1 -0
- package/dist/types/src/components/Preview/Preview.stories.d.ts +110 -0
- package/dist/types/src/components/Preview/Preview.stories.d.ts.map +1 -0
- package/dist/types/src/components/Preview/PreviewContext.d.ts +24 -0
- package/dist/types/src/components/Preview/PreviewContext.d.ts.map +1 -0
- package/dist/types/src/components/Preview/index.d.ts +3 -0
- package/dist/types/src/components/Preview/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/containers/FileArticle/FileArticle.d.ts +3 -3
- package/dist/types/src/containers/FileArticle/FileArticle.d.ts.map +1 -1
- package/dist/types/src/containers/FileArticle/FileArticle.stories.d.ts +104 -0
- package/dist/types/src/containers/FileArticle/FileArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/FileArticle/index.d.ts +1 -1
- package/dist/types/src/containers/FileArticle/index.d.ts.map +1 -1
- package/dist/types/src/containers/FileCard/FileCard.d.ts +10 -0
- package/dist/types/src/containers/FileCard/FileCard.d.ts.map +1 -0
- package/dist/types/src/containers/FileCard/index.d.ts +2 -0
- package/dist/types/src/containers/FileCard/index.d.ts.map +1 -0
- package/dist/types/src/containers/FileProperties/FileProperties.d.ts +9 -0
- package/dist/types/src/containers/FileProperties/FileProperties.d.ts.map +1 -0
- package/dist/types/src/containers/FileProperties/index.d.ts +2 -0
- package/dist/types/src/containers/FileProperties/index.d.ts.map +1 -0
- package/dist/types/src/containers/FileSettings/FileSettings.d.ts +1 -1
- package/dist/types/src/containers/FileSettings/FileSettings.d.ts.map +1 -1
- package/dist/types/src/containers/FileSettings/index.d.ts +1 -1
- package/dist/types/src/containers/FileSettings/index.d.ts.map +1 -1
- package/dist/types/src/containers/index.d.ts +3 -1
- package/dist/types/src/containers/index.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +1 -1
- package/dist/types/src/extensions/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useFileUrl.d.ts +13 -0
- package/dist/types/src/hooks/useFileUrl.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/operations/create-from-source.d.ts +13 -0
- package/dist/types/src/operations/create-from-source.d.ts.map +1 -0
- package/dist/types/src/operations/create-from-source.test.d.ts +2 -0
- package/dist/types/src/operations/create-from-source.test.d.ts.map +1 -0
- package/dist/types/src/operations/create-from-upload.d.ts +38 -0
- package/dist/types/src/operations/create-from-upload.d.ts.map +1 -0
- package/dist/types/src/operations/create.d.ts +85 -9
- package/dist/types/src/operations/create.d.ts.map +1 -1
- package/dist/types/src/operations/index.d.ts.map +1 -1
- package/dist/types/src/operations/resolve-download.d.ts +42 -0
- package/dist/types/src/operations/resolve-download.d.ts.map +1 -0
- package/dist/types/src/operations/resolve-download.test.d.ts +2 -0
- package/dist/types/src/operations/resolve-download.test.d.ts.map +1 -0
- package/dist/types/src/skills/FileSkill.d.ts +6 -0
- package/dist/types/src/skills/FileSkill.d.ts.map +1 -0
- package/dist/types/src/skills/index.d.ts +1 -1
- package/dist/types/src/skills/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +49 -3
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/FileCapabilities.d.ts +3 -2
- package/dist/types/src/types/FileCapabilities.d.ts.map +1 -1
- package/dist/types/src/types/FileLimits.d.ts +9 -2
- package/dist/types/src/types/FileLimits.d.ts.map +1 -1
- package/dist/types/src/types/FileOperation.d.ts +73 -0
- package/dist/types/src/types/FileOperation.d.ts.map +1 -1
- package/dist/types/src/types/index.d.ts +5 -5
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +60 -34
- package/src/FilePlugin.workerd.test.ts +22 -0
- package/src/StagedUpload.ts +100 -0
- package/src/capabilities/FileUploadField.tsx +3 -3
- package/src/capabilities/edge-backend.ts +1 -1
- package/src/capabilities/file-uploader.ts +1 -1
- package/src/capabilities/gen/browser.ts +104 -0
- package/src/capabilities/gen/node.ts +14 -21
- package/src/capabilities/gen/tauri.ts +104 -0
- package/src/capabilities/gen/workerd.ts +14 -21
- package/src/capabilities/index.ts +33 -15
- package/src/capabilities/inline-backend.ts +1 -1
- package/src/capabilities/markdown-extension.ts +1 -1
- package/src/capabilities/react-surface.ts +22 -8
- package/src/capabilities/settings.ts +2 -2
- package/src/components/FileInput/FileInput.tsx +5 -14
- package/src/components/FileInput/index.ts +1 -1
- package/src/components/PdfCanvas/PdfCanvas.stories.tsx +128 -0
- package/src/components/PdfCanvas/PdfCanvas.tsx +631 -0
- package/src/components/{FilePreview → PdfCanvas}/index.ts +1 -1
- package/src/components/PdfCanvas/pdf-search.ts +89 -0
- package/src/components/PdfCanvas/text-layer.css +85 -0
- package/src/components/Preview/Preview.stories.tsx +118 -0
- package/src/components/Preview/Preview.tsx +314 -0
- package/src/components/Preview/PreviewContext.ts +42 -0
- package/src/components/Preview/index.ts +6 -0
- package/src/components/index.ts +3 -2
- package/src/containers/FileArticle/FileArticle.stories.tsx +147 -0
- package/src/containers/FileArticle/FileArticle.tsx +28 -69
- package/src/containers/FileArticle/index.ts +1 -1
- package/src/containers/FileCard/FileCard.tsx +73 -0
- package/src/containers/FileCard/index.ts +5 -0
- package/src/containers/FileProperties/FileProperties.tsx +107 -0
- package/src/containers/FileProperties/index.ts +5 -0
- package/src/containers/FileSettings/FileSettings.tsx +30 -25
- package/src/containers/FileSettings/index.ts +1 -1
- package/src/containers/index.ts +4 -2
- package/src/extensions/image.tsx +6 -6
- package/src/extensions/index.ts +1 -1
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useFileUrl.ts +86 -0
- package/src/index.ts +2 -2
- package/src/meta.ts +1 -1
- package/src/operations/create-from-source.test.ts +227 -0
- package/src/operations/create-from-source.ts +98 -0
- package/src/operations/create-from-upload.ts +53 -0
- package/src/operations/create.test.ts +25 -14
- package/src/operations/create.ts +8 -29
- package/src/operations/index.ts +5 -2
- package/src/operations/resolve-download.test.ts +68 -0
- package/src/operations/resolve-download.ts +49 -0
- package/src/plugin.test.ts +2 -2
- package/src/skills/FileSkill.ts +71 -0
- package/src/skills/index.ts +1 -1
- package/src/translations.ts +27 -4
- package/src/types/FileCapabilities.ts +4 -4
- package/src/types/FileLimits.ts +23 -13
- package/src/types/FileOperation.ts +110 -1
- package/src/types/index.ts +5 -5
- package/src/vite-env.d.ts +15 -0
- package/dist/lib/skills.mjs.map +0 -1
- package/dist/types/src/components/FilePreview/FilePreview.d.ts +0 -12
- package/dist/types/src/components/FilePreview/FilePreview.d.ts.map +0 -1
- package/dist/types/src/components/FilePreview/index.d.ts +0 -2
- package/dist/types/src/components/FilePreview/index.d.ts.map +0 -1
- package/dist/types/src/skills/file-skill.d.ts +0 -5
- package/dist/types/src/skills/file-skill.d.ts.map +0 -1
- package/src/components/FilePreview/FilePreview.tsx +0 -55
- package/src/skills/file-skill.ts +0 -40
package/PLUGIN.mdl
CHANGED
|
@@ -21,9 +21,10 @@ Each extension is defined in the Appendix or resolved via its URI.
|
|
|
21
21
|
|-------------|--------------------------------|
|
|
22
22
|
| `type` | `org.dxos.mdl.type@1.0` |
|
|
23
23
|
| `feat` | `org.dxos.mdl.feat@1.0` |
|
|
24
|
-
| `
|
|
24
|
+
| `scenario` | `org.dxos.mdl.scenario@1.0` |
|
|
25
25
|
| `component` | `org.dxos.mdl.component@1.0` |
|
|
26
26
|
| `op` | `org.dxos.mdl.op@1.0` |
|
|
27
|
+
| `test` | `org.dxos.mdl.test@1.0` |
|
|
27
28
|
|
|
28
29
|
## Types
|
|
29
30
|
|
|
@@ -59,11 +60,22 @@ type Settings
|
|
|
59
60
|
## Components
|
|
60
61
|
|
|
61
62
|
```mdl
|
|
62
|
-
component
|
|
63
|
+
component Preview
|
|
63
64
|
desc: |
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
Composite preview of a file's content, in Radix parts. Preview.Root holds
|
|
66
|
+
the source and whatever the content discovers about it, Preview.Content
|
|
67
|
+
renders by MIME type, and Preview.Toolbar sits above with controls. Split
|
|
68
|
+
this way because the toolbar needs facts only the content discovers — a
|
|
69
|
+
PDF's page count is known after pdf.js parses the document.
|
|
70
|
+
|
|
71
|
+
Root renders no element of its own: its parts are placed into slots that
|
|
72
|
+
belong to the surrounding container (Panel.Toolbar / Panel.Content), so a
|
|
73
|
+
wrapper would break the panel's grid.
|
|
74
|
+
|
|
75
|
+
Images, video and audio go through react-ui's MediaPlayer. PDFs render
|
|
76
|
+
through pdf.js to stacked canvases — one per page — rather than an
|
|
77
|
+
<iframe>, whose built-in viewer lives in another origin and cannot be
|
|
78
|
+
themed. Everything else falls back to a download link.
|
|
67
79
|
props:
|
|
68
80
|
type: string # MIME type
|
|
69
81
|
url: string # renderable data:, blob:, or http(s): URL
|
|
@@ -108,18 +120,21 @@ component FileArticle
|
|
|
108
120
|
Full-viewport article container rendered for article, section, and slide
|
|
109
121
|
roles. Loads the File's referenced Blob and resolves a renderable URL via
|
|
110
122
|
Blob.url; if the backend has no getUrl, falls back to Blob.read and
|
|
111
|
-
constructs a blob: object URL (revoked on unmount).
|
|
112
|
-
|
|
113
|
-
File is not attached to a database.
|
|
123
|
+
constructs a blob: object URL (revoked on unmount). Composes Preview's
|
|
124
|
+
parts into the panel's toolbar and content slots. Shows an empty state
|
|
125
|
+
while the URL is resolving or if the File is not attached to a database.
|
|
126
|
+
|
|
127
|
+
Full plank width, not the reading column: `dx-document` is right for prose
|
|
128
|
+
and wrong for a preview.
|
|
114
129
|
props:
|
|
115
130
|
role: AppSurface.Role
|
|
116
131
|
subject: File
|
|
117
132
|
layout: |
|
|
118
133
|
┌──────────────────────────────┐
|
|
119
|
-
│
|
|
120
|
-
|
|
134
|
+
│ Preview.Toolbar (n pages) │
|
|
135
|
+
├──────────────────────────────┤
|
|
136
|
+
│ Preview.Content │
|
|
121
137
|
│ (fills container) │
|
|
122
|
-
│ │
|
|
123
138
|
└──────────────────────────────┘
|
|
124
139
|
```
|
|
125
140
|
|
|
@@ -129,8 +144,8 @@ component FileArticle
|
|
|
129
144
|
op Create
|
|
130
145
|
key: org.dxos.operation.file.create
|
|
131
146
|
desc: |
|
|
132
|
-
|
|
133
|
-
BackendDescriptor, and composes File.fromBytes (which hashes/uploads the
|
|
147
|
+
Resolves the MIME type to store (FileLimits.toStoredMimeType), resolves the
|
|
148
|
+
active BackendDescriptor, and composes File.fromBytes (which hashes/uploads the
|
|
134
149
|
bytes via the Blob API and adds the resulting Blob to the database) to
|
|
135
150
|
construct a File ECHO object. Does not add the File object itself to the
|
|
136
151
|
space — callers do that separately.
|
|
@@ -139,7 +154,6 @@ op Create
|
|
|
139
154
|
db: Database
|
|
140
155
|
output: { object: File }
|
|
141
156
|
errors:
|
|
142
|
-
UnsupportedFileTypeError: MIME type is not image/*, video/*, or application/pdf
|
|
143
157
|
FileTooLargeError: file exceeds the 4 MB inline limit (only possible when the
|
|
144
158
|
resolved backend's storage is 'inline'; other backends scale beyond it)
|
|
145
159
|
NoBackendError: no BackendDescriptor capability is registered
|
|
@@ -150,8 +164,9 @@ op Create
|
|
|
150
164
|
```mdl
|
|
151
165
|
feat F-1: File upload
|
|
152
166
|
|
|
153
|
-
req F-1.1:
|
|
154
|
-
|
|
167
|
+
req F-1.1: Every MIME type MUST be accepted, including an absent one. A type
|
|
168
|
+
that is absent or that a browser executes (text/html, application/xhtml+xml,
|
|
169
|
+
text/xml, application/xml) MUST be stored as application/octet-stream.
|
|
155
170
|
|
|
156
171
|
req F-1.2: Maximum upload size MUST be 4 MB when the resolved backend's storage
|
|
157
172
|
is 'inline'. Other backends (e.g. edge, wnfs) are not subject to this cap.
|
|
@@ -165,8 +180,8 @@ feat F-1: File upload
|
|
|
165
180
|
renderable url resolved via Blob.url, if one is available)
|
|
166
181
|
|
|
167
182
|
req F-1.4:
|
|
168
|
-
when: file MIME type is
|
|
169
|
-
then:
|
|
183
|
+
when: file MIME type is unrecognized or empty (e.g. a .ndjson.gz log bundle)
|
|
184
|
+
then: the File is created; Preview shows the no-preview message and a download
|
|
170
185
|
|
|
171
186
|
req F-1.5:
|
|
172
187
|
when: file size exceeds the limit and the resolved backend's storage is 'inline'
|
|
@@ -200,14 +215,23 @@ feat F-2: Backend capability system
|
|
|
200
215
|
feat F-3: File preview
|
|
201
216
|
|
|
202
217
|
req F-3.1: FileArticle MUST resolve a renderable URL via Blob.url before
|
|
203
|
-
rendering
|
|
218
|
+
rendering Preview. For inline files this is always a data: URL
|
|
204
219
|
(constructed synchronously from the stored bytes, no Blob.read needed). For
|
|
205
220
|
external files with no registered getUrl, FileArticle falls back to
|
|
206
221
|
Blob.read and constructs a blob: URL, revoked on unmount.
|
|
207
222
|
|
|
208
|
-
req F-3.2:
|
|
209
|
-
image/*
|
|
210
|
-
other → <a download>.
|
|
223
|
+
req F-3.2: Preview.Content MUST render the appropriate element for each MIME
|
|
224
|
+
class: image/*, video/* and audio/* → MediaPlayer, application/pdf → pdf.js
|
|
225
|
+
canvases, other → <a download>.
|
|
226
|
+
|
|
227
|
+
req F-3.4: A PDF MUST render every page, not one at a time. The <iframe> this
|
|
228
|
+
replaced scrolled the whole document, so paginating would silently hide
|
|
229
|
+
content behind navigation that does not exist.
|
|
230
|
+
|
|
231
|
+
req F-3.5: Rendering MUST be cancelled before the document is torn down.
|
|
232
|
+
pdf.js rejects in-flight render and getPage promises with
|
|
233
|
+
RenderingCancelledException / "Transport destroyed" once the transport is
|
|
234
|
+
destroyed, and those rejections are unhandled if nothing cancels first.
|
|
211
235
|
|
|
212
236
|
req F-3.3: FileArticle surfaces are registered for article, section,
|
|
213
237
|
and slide roles so files embed correctly in Markdown and Presenter layouts.
|
|
@@ -235,7 +259,7 @@ feat F-5: Settings
|
|
|
235
259
|
## Acceptance
|
|
236
260
|
|
|
237
261
|
```mdl
|
|
238
|
-
|
|
262
|
+
scenario T-1: Upload image inline
|
|
239
263
|
given: only the inline BackendDescriptor is registered
|
|
240
264
|
when: user uploads a 1 MB PNG via the Create Object form
|
|
241
265
|
then:
|
|
@@ -245,7 +269,7 @@ test T-1: Upload image inline
|
|
|
245
269
|
```
|
|
246
270
|
|
|
247
271
|
```mdl
|
|
248
|
-
|
|
272
|
+
scenario T-2: Reject oversized file
|
|
249
273
|
given: inline backend selected, 4 MB limit
|
|
250
274
|
when: user uploads a 5 MB JPEG
|
|
251
275
|
then:
|
|
@@ -255,16 +279,15 @@ test T-2: Reject oversized file
|
|
|
255
279
|
```
|
|
256
280
|
|
|
257
281
|
```mdl
|
|
258
|
-
|
|
282
|
+
scenario T-3: Store an unrecognized MIME type
|
|
259
283
|
given: inline backend registered
|
|
260
284
|
when: user uploads a .zip file
|
|
261
285
|
then:
|
|
262
|
-
-
|
|
263
|
-
- no File object is created
|
|
286
|
+
- a File object is created whose Blob type is application/zip
|
|
264
287
|
```
|
|
265
288
|
|
|
266
289
|
```mdl
|
|
267
|
-
|
|
290
|
+
scenario T-4: Preview inline image
|
|
268
291
|
given: a File whose data Ref resolves to a Blob with data._tag === 'inline' and type === 'image/png'
|
|
269
292
|
when: FileArticle is rendered
|
|
270
293
|
then:
|
|
@@ -273,21 +296,21 @@ test T-4: Preview inline image
|
|
|
273
296
|
```
|
|
274
297
|
|
|
275
298
|
```mdl
|
|
276
|
-
|
|
299
|
+
scenario T-5: Preview PDF
|
|
277
300
|
given: a File with type === 'application/pdf' and a Blob resolved to an HTTP or data URL
|
|
278
301
|
when: FilePreview is rendered
|
|
279
302
|
then: an <iframe> element is rendered with the file URL as src
|
|
280
303
|
```
|
|
281
304
|
|
|
282
305
|
```mdl
|
|
283
|
-
|
|
306
|
+
scenario T-6: Backend fallback to inline
|
|
284
307
|
given: Settings.backend is 'wnfs' but no BackendDescriptor with storage 'wnfs' is registered
|
|
285
308
|
when: op:Create is invoked
|
|
286
309
|
then: the inline backend is used and the file is stored inline
|
|
287
310
|
```
|
|
288
311
|
|
|
289
312
|
```mdl
|
|
290
|
-
|
|
313
|
+
scenario T-7: External blob resolved via a registered BlobBackend's getUrl
|
|
291
314
|
given: a File whose data Ref resolves to a Blob with data._tag === 'external' and a
|
|
292
315
|
BlobBackend registered on the ECHO Hypergraph that resolves the Blob's URI scheme
|
|
293
316
|
and implements getUrl
|
|
@@ -299,7 +322,7 @@ test T-7: External blob resolved via a registered BlobBackend's getUrl
|
|
|
299
322
|
```
|
|
300
323
|
|
|
301
324
|
```mdl
|
|
302
|
-
|
|
325
|
+
scenario T-8: External blob without getUrl falls back to reading bytes
|
|
303
326
|
given: a File whose data Ref resolves to an external Blob whose registered
|
|
304
327
|
BlobBackend implements get but not getUrl
|
|
305
328
|
when: FileArticle is rendered
|
|
@@ -310,7 +333,7 @@ test T-8: External blob without getUrl falls back to reading bytes
|
|
|
310
333
|
```
|
|
311
334
|
|
|
312
335
|
```mdl
|
|
313
|
-
|
|
336
|
+
scenario T-9: No backend error
|
|
314
337
|
given: no BackendDescriptor capability is registered
|
|
315
338
|
when: op:Create is invoked
|
|
316
339
|
then: NoBackendError is returned
|
|
@@ -318,6 +341,88 @@ test T-9: No backend error
|
|
|
318
341
|
|
|
319
342
|
---
|
|
320
343
|
|
|
344
|
+
## QA
|
|
345
|
+
|
|
346
|
+
Executable tests. Each step carries a human instruction (`do`) and, where the step maps to an
|
|
347
|
+
invocable operation, an agent binding (`invoke`). Report failures by test and step number.
|
|
348
|
+
|
|
349
|
+
plugin-file is on by default (`FilePlugin.meta.profile.key` is in `getDefaults`; see
|
|
350
|
+
`packages/apps/composer-app/src/plugin-defs.tsx`).
|
|
351
|
+
|
|
352
|
+
```mdl
|
|
353
|
+
test QA-1: Upload an image and preview it
|
|
354
|
+
desc: The core file round-trip — upload an inline image via the Create Object form, place it, and read the preview back.
|
|
355
|
+
actors: both
|
|
356
|
+
covers: [F-1.1, F-1.3, F-3.1, F-3.2, T-1, T-4]
|
|
357
|
+
status: unverified
|
|
358
|
+
given:
|
|
359
|
+
- an identity exists
|
|
360
|
+
- space: a space is open
|
|
361
|
+
- collection: a collection in that space, to create into
|
|
362
|
+
- only the inline BackendDescriptor is registered (default)
|
|
363
|
+
- no File named "qa-image.png" exists — ABORT rather than reusing one this test did not create
|
|
364
|
+
steps:
|
|
365
|
+
- name: Upload the image
|
|
366
|
+
do: In the navtree, click + on a collection and choose File. Drop a small PNG (e.g. 10 KB) named "qa-image.png".
|
|
367
|
+
invoke: |
|
|
368
|
+
org.dxos.operation.file.create {
|
|
369
|
+
file: new File([Uint8Array.from(atob("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="), (c) => c.charCodeAt(0))], "qa-image.png", { type: "image/png" }),
|
|
370
|
+
db: $given.space.db
|
|
371
|
+
}
|
|
372
|
+
capture: created
|
|
373
|
+
expect: a File object is created with type "image/png", the given size, and a Blob whose
|
|
374
|
+
data resolves inline
|
|
375
|
+
assert: return $created.object.type === 'image/png' && !!$created.object.data
|
|
376
|
+
note: |
|
|
377
|
+
`file` is a browser `File` object, not a plain literal — the runner constructs it inline
|
|
378
|
+
with `new File(...)` in the invoke snippet rather than passing JSON, since `BrowserFile`
|
|
379
|
+
has no serializable shape.
|
|
380
|
+
- name: Place it in the collection
|
|
381
|
+
do: (the Create Object form places it for you)
|
|
382
|
+
invoke: org.dxos.operation.space.addObject { object: $created.object, target: $given.collection }
|
|
383
|
+
expect: "qa-image.png" appears as a row under the collection
|
|
384
|
+
assert: |
|
|
385
|
+
return $given.collection.objects.some((ref) => ref.target?.id === $created.object.id);
|
|
386
|
+
note: Coalesced with the previous step — `created.object` is live and cannot cross a snippet boundary.
|
|
387
|
+
- name: Open it and view the preview
|
|
388
|
+
do: Click "qa-image.png" in the navtree.
|
|
389
|
+
invoke: org.dxos.operation.appToolkit.open { subject: ["root/$given.space.id/content/collections/$created.object.id"] }
|
|
390
|
+
expect: FileArticle renders an <img> element showing the uploaded image, resolved via a data: URL
|
|
391
|
+
assert: |
|
|
392
|
+
const img = document.querySelector('img[src^="data:"]');
|
|
393
|
+
return !!img && img.complete && img.naturalWidth > 0;
|
|
394
|
+
after:
|
|
395
|
+
- name: Delete the File
|
|
396
|
+
do: Right-click "qa-image.png" in the navtree and choose Delete.
|
|
397
|
+
invoke: org.dxos.operation.space.removeObjects { objects: [$created.object], target: $given.collection }
|
|
398
|
+
expect: the row leaves the navtree and its plank closes
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
```mdl
|
|
402
|
+
test QA-2: Reject an oversized upload
|
|
403
|
+
desc: The rejection path (F-1.5) — a validation round-trip that must leave no ECHO write behind.
|
|
404
|
+
actors: agent
|
|
405
|
+
covers: [F-1.5, T-2]
|
|
406
|
+
status: unverified
|
|
407
|
+
given:
|
|
408
|
+
- an identity exists
|
|
409
|
+
- space: a space is open
|
|
410
|
+
- only the inline BackendDescriptor is registered (4 MB cap applies)
|
|
411
|
+
steps:
|
|
412
|
+
- name: Reject an oversized file
|
|
413
|
+
do: Attempt to upload a PNG larger than 4 MB.
|
|
414
|
+
invoke: |
|
|
415
|
+
org.dxos.operation.file.create {
|
|
416
|
+
file: new File([new Uint8Array(5 * 1024 * 1024)], "qa-big.png", { type: "image/png" }),
|
|
417
|
+
db: $given.space.db
|
|
418
|
+
}
|
|
419
|
+
expect: the upload is refused with FileTooLargeError and no File object is created
|
|
420
|
+
assert: |
|
|
421
|
+
return /FileTooLarge/.test($error?.message ?? $error?._tag ?? '');
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
321
426
|
## Appendix: Extension Definitions
|
|
322
427
|
|
|
323
428
|
```mdl
|
|
@@ -342,8 +447,8 @@ ext feat
|
|
|
342
447
|
```
|
|
343
448
|
|
|
344
449
|
```mdl
|
|
345
|
-
ext
|
|
346
|
-
uri: org.dxos.mdl.
|
|
450
|
+
ext scenario
|
|
451
|
+
uri: org.dxos.mdl.scenario@1.0
|
|
347
452
|
desc: An acceptance scenario expressed as given / when / then steps.
|
|
348
453
|
fields:
|
|
349
454
|
given?: Step | Step[]
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
|
-
import { ACCEPTED_MIME } from "./FileLimits.mjs";
|
|
3
2
|
import * as Capability from "@dxos/app-framework/Capability";
|
|
4
3
|
import { meta } from "#meta";
|
|
5
4
|
import * as Schema from "effect/Schema";
|
|
@@ -14,7 +13,7 @@ var SettingsAtom = Capability.makeSingleton()(`${meta.profile.key}.capability.se
|
|
|
14
13
|
var FileAction;
|
|
15
14
|
(function(_FileAction) {
|
|
16
15
|
const UploadAnnotationId = _FileAction.UploadAnnotationId = `${meta.profile.key}.annotation.upload`;
|
|
17
|
-
_FileAction.CreateFileSchema = Schema.Struct({ file: Schema.instanceOf(File).annotate({ [UploadAnnotationId]:
|
|
16
|
+
_FileAction.CreateFileSchema = Schema.Struct({ file: Schema.instanceOf(File).annotate({ [UploadAnnotationId]: true }) });
|
|
18
17
|
})(FileAction || (FileAction = {}));
|
|
19
18
|
//#endregion
|
|
20
19
|
export { Backend, FileAction, SettingsAtom, FileCapabilities_exports as t };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileCapabilities.mjs","names":[],"sources":["../../src/types/FileCapabilities.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as
|
|
1
|
+
{"version":3,"file":"FileCapabilities.mjs","names":[],"sources":["../../src/types/FileCapabilities.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport type * as Atom from 'effect/reactivity/Atom';\nimport * as Schema from 'effect/Schema';\n\nimport * as Capability from '@dxos/app-framework/Capability';\n\nimport { meta } from '#meta';\n\nimport * as Settings from './Settings.ts';\n\n/**\n * Descriptor for a registered `BlobBackend` storage option, surfaced in the file plugin's\n * settings UI. Actual upload/read logic lives in the Blob backend registered on the Hypergraph\n * under `storage` (see `client.graph.registerBlobBackend`) — this type carries no behavior.\n */\nexport type Backend = {\n /** Display label shown in the settings UI. */\n readonly name: string;\n /** Description shown next to the backend in settings. */\n readonly description?: string;\n /**\n * `Blob.fromBytes`'s `storage` option to use when this backend is selected. Also the settings\n * key identifying this backend — one `BlobBackend` is registered per storage name, so the two\n * concepts always coincide.\n */\n readonly storage: string;\n};\n\n// Multi capability: inline-backend and edge-backend each contribute one entry.\nexport const Backend = Capability.make<Backend>()(`${meta.profile.key}.capability.backend`);\n\nexport const SettingsAtom = Capability.makeSingleton<Atom.Writable<Settings.Settings>>()(\n `${meta.profile.key}.capability.settings`,\n);\n\nexport namespace FileAction {\n /** Marks a field as a file upload, so the form renders the drop zone for it. Any type is accepted. */\n export const UploadAnnotationId = `${meta.profile.key}.annotation.upload`;\n\n export const CreateFileSchema = Schema.Struct({\n file: Schema.instanceOf(File).annotate({\n [UploadAnnotationId]: true,\n }),\n });\n\n export type CreateFileForm = Schema.Schema.Type<typeof CreateFileSchema>;\n}\n"],"mappings":";;;;;;;;;;AAgCA,IAAa,UAAU,WAAW,KAAc,CAAC,CAAC,GAAG,KAAK,QAAQ,IAAI,oBAAoB;AAE1F,IAAa,eAAe,WAAW,cAAgD,CAAC,CACtF,GAAG,KAAK,QAAQ,IAAI,qBACtB;AAEO,IAAU;CAAV,SAAA,aAAA;CAEE,MAAM,qBAAA,YAAA,qBAAqB,GAAG,KAAK,QAAQ,IAAI;CAEzC,YAAA,mBAAmB,OAAO,OAAO,EAC5C,MAAM,OAAO,WAAW,IAAI,CAAC,CAAC,SAAS,GACpC,qBAAqB,KACxB,CAAC,EACH,CAAC;AAGH,EAAA,CAAA,eAAA,aAAA,CAAA,EAAA"}
|
package/dist/lib/FileLimits.mjs
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
2
|
//#region src/types/FileLimits.ts
|
|
3
3
|
var FileLimits_exports = /* @__PURE__ */ __exportAll({
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
FALLBACK_MIME_TYPE: () => FALLBACK_MIME_TYPE,
|
|
5
|
+
toStoredMimeType: () => toStoredMimeType
|
|
6
6
|
});
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
7
|
+
/** Stored in place of a type that is absent or would execute; the bytes are kept, only the label changes. */
|
|
8
|
+
var FALLBACK_MIME_TYPE = "application/octet-stream";
|
|
9
|
+
/**
|
|
10
|
+
* Media types a browser executes when it renders them.
|
|
11
|
+
*
|
|
12
|
+
* A stored file is handed back through a presigned, public or `blob:` URL on the app's origin, and
|
|
13
|
+
* HTML served from there runs with that origin — making an upload a stored-XSS primitive. Such
|
|
14
|
+
* files are still accepted, but labelled {@link FALLBACK_MIME_TYPE} so they download rather than
|
|
15
|
+
* render. Matches the blob service's own `ACTIVE_CONTENT_TYPES`, minus SVG, which image previews
|
|
16
|
+
* rely on.
|
|
17
|
+
*/
|
|
18
|
+
var ACTIVE_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
19
|
+
"text/html",
|
|
20
|
+
"application/xhtml+xml",
|
|
21
|
+
"text/xml",
|
|
22
|
+
"application/xml"
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* The media type to store for an upload. Every type is accepted — the file plugin keeps bytes it
|
|
26
|
+
* cannot preview — so this never rejects; it only neutralizes types that are missing or executable.
|
|
27
|
+
* Compared case-insensitively and without parameters: RFC 2045 defines type and subtype as
|
|
28
|
+
* case-insensitive, and `text/html; charset=utf-8` is still HTML.
|
|
29
|
+
*/
|
|
30
|
+
var toStoredMimeType = (type) => {
|
|
31
|
+
const declared = type?.trim() ?? "";
|
|
32
|
+
const base = declared.split(";")[0].trim().toLowerCase();
|
|
33
|
+
return base.length === 0 || ACTIVE_CONTENT_TYPES.has(base) ? FALLBACK_MIME_TYPE : declared;
|
|
27
34
|
};
|
|
28
35
|
//#endregion
|
|
29
|
-
export {
|
|
36
|
+
export { FALLBACK_MIME_TYPE, FileLimits_exports as t, toStoredMimeType };
|
|
30
37
|
|
|
31
38
|
//# sourceMappingURL=FileLimits.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileLimits.mjs","names":[],"sources":["../../src/types/FileLimits.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\
|
|
1
|
+
{"version":3,"file":"FileLimits.mjs","names":[],"sources":["../../src/types/FileLimits.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n/** Stored in place of a type that is absent or would execute; the bytes are kept, only the label changes. */\nexport const FALLBACK_MIME_TYPE = 'application/octet-stream';\n\n/**\n * Media types a browser executes when it renders them.\n *\n * A stored file is handed back through a presigned, public or `blob:` URL on the app's origin, and\n * HTML served from there runs with that origin — making an upload a stored-XSS primitive. Such\n * files are still accepted, but labelled {@link FALLBACK_MIME_TYPE} so they download rather than\n * render. Matches the blob service's own `ACTIVE_CONTENT_TYPES`, minus SVG, which image previews\n * rely on.\n */\nconst ACTIVE_CONTENT_TYPES = new Set(['text/html', 'application/xhtml+xml', 'text/xml', 'application/xml']);\n\n/**\n * The media type to store for an upload. Every type is accepted — the file plugin keeps bytes it\n * cannot preview — so this never rejects; it only neutralizes types that are missing or executable.\n * Compared case-insensitively and without parameters: RFC 2045 defines type and subtype as\n * case-insensitive, and `text/html; charset=utf-8` is still HTML.\n */\nexport const toStoredMimeType = (type: string | undefined): string => {\n const declared = type?.trim() ?? '';\n const base = declared.split(';')[0].trim().toLowerCase();\n return base.length === 0 || ACTIVE_CONTENT_TYPES.has(base) ? FALLBACK_MIME_TYPE : declared;\n};\n"],"mappings":";;;;;;;AAKA,IAAa,qBAAqB;;;;;;;;;;AAWlC,IAAM,uCAAuB,IAAI,IAAI;CAAC;CAAa;CAAyB;CAAY;AAAiB,CAAC;;;;;;;AAQ1G,IAAa,oBAAoB,SAAqC;CACpE,MAAM,WAAW,MAAM,KAAK,KAAK;CACjC,MAAM,OAAO,SAAS,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,YAAY;CACvD,OAAO,KAAK,WAAW,KAAK,qBAAqB,IAAI,IAAI,IAAI,qBAAqB;AACpF"}
|
|
@@ -9,7 +9,11 @@ import * as Struct from "effect/Struct";
|
|
|
9
9
|
//#region src/types/FileOperation.ts
|
|
10
10
|
var FileOperation_exports = /* @__PURE__ */ __exportAll({
|
|
11
11
|
Create: () => Create,
|
|
12
|
-
|
|
12
|
+
CreateFromSource: () => CreateFromSource,
|
|
13
|
+
CreateFromUpload: () => CreateFromUpload,
|
|
14
|
+
FileSource: () => FileSource,
|
|
15
|
+
Read: () => Read,
|
|
16
|
+
ResolveDownload: () => ResolveDownload
|
|
13
17
|
});
|
|
14
18
|
var Create = Operation.make({
|
|
15
19
|
meta: {
|
|
@@ -21,6 +25,71 @@ var Create = Operation.make({
|
|
|
21
25
|
input: FileAction.CreateFileSchema.mapFields(Struct.assign({ db: Database.Database })),
|
|
22
26
|
output: Schema.Struct({ object: Type.getSchema(File.File) })
|
|
23
27
|
});
|
|
28
|
+
/**
|
|
29
|
+
* Where the bytes come from. Mirrors `ContentBlock.ImageSource` rather than inventing a shape — it
|
|
30
|
+
* is how this repo already models "bytes, or a pointer to bytes".
|
|
31
|
+
*
|
|
32
|
+
* Two arms because their costs differ sharply: `base64` rides in the tool-call arguments and so
|
|
33
|
+
* occupies the caller's context, while `http` is fetched by the host and never crosses it.
|
|
34
|
+
*/
|
|
35
|
+
var FileSource = Schema.Union([Schema.Struct({
|
|
36
|
+
type: Schema.tag("base64"),
|
|
37
|
+
mediaType: Schema.String.annotate({ description: "MIME type of the encoded bytes, e.g. image/png." }),
|
|
38
|
+
data: Schema.String.annotate({ description: "Base64-encoded file contents." })
|
|
39
|
+
}), Schema.Struct({
|
|
40
|
+
type: Schema.tag("http"),
|
|
41
|
+
url: Schema.String.annotate({ description: "HTTPS URL the host downloads. Must not be a private address." })
|
|
42
|
+
})]);
|
|
43
|
+
/**
|
|
44
|
+
* Creates a file from serializable input, so an agent can reach it.
|
|
45
|
+
*
|
|
46
|
+
* Separate from {@link Create} rather than replacing it: `Create` takes a live browser `File`, which
|
|
47
|
+
* is right for the upload UI and cannot render as JSON Schema — an operation whose input cannot is
|
|
48
|
+
* dropped from the registry silently, and so is invisible to any tool caller.
|
|
49
|
+
*
|
|
50
|
+
* Takes the database as a service rather than an input field, which is both what makes the input
|
|
51
|
+
* serializable and what forces the caller to name a space.
|
|
52
|
+
*/
|
|
53
|
+
var CreateFromSource = Operation.make({
|
|
54
|
+
meta: {
|
|
55
|
+
key: DXN.make("org.dxos.operation.file.createFromSource"),
|
|
56
|
+
name: "Create File From Source",
|
|
57
|
+
description: "Creates a file in the space from base64 content or an HTTPS URL, storing it via the configured backend.",
|
|
58
|
+
icon: "ph--file-plus--regular"
|
|
59
|
+
},
|
|
60
|
+
input: Schema.Struct({
|
|
61
|
+
source: FileSource,
|
|
62
|
+
name: Schema.optional(Schema.String.annotate({ description: "Filename to record on the file object." }))
|
|
63
|
+
}),
|
|
64
|
+
output: Schema.Struct({ object: Type.getSchema(File.File) }),
|
|
65
|
+
services: [Database.Service, Capability.Service]
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Creates a file from bytes already uploaded to blob storage over a signed URL.
|
|
69
|
+
*
|
|
70
|
+
* The third arm alongside {@link CreateFromSource}'s two, and the one for a file that exists only
|
|
71
|
+
* on the caller's disk: `base64` would have a model emit the bytes token by token, and `http`
|
|
72
|
+
* requires them to already be served somewhere public. Here the caller uploads directly (see the
|
|
73
|
+
* MCP server's `createUpload` tool) and passes only the id, so a screen recording costs the same
|
|
74
|
+
* few tokens as a screenshot.
|
|
75
|
+
*
|
|
76
|
+
* No media type or size input: nothing the caller says about the bytes is trusted or needed, since
|
|
77
|
+
* the blob service measured them as they arrived.
|
|
78
|
+
*/
|
|
79
|
+
var CreateFromUpload = Operation.make({
|
|
80
|
+
meta: {
|
|
81
|
+
key: DXN.make("org.dxos.operation.file.createFromUpload"),
|
|
82
|
+
name: "Create File From Upload",
|
|
83
|
+
description: "Creates a file in the space from a completed direct upload, given the uploadId returned by the createUpload tool. Use after the upload command has succeeded.",
|
|
84
|
+
icon: "ph--upload--regular"
|
|
85
|
+
},
|
|
86
|
+
input: Schema.Struct({
|
|
87
|
+
uploadId: Schema.String.annotate({ description: "The uploadId returned by createUpload, whose bytes have already been uploaded." }),
|
|
88
|
+
name: Schema.optional(Schema.String.annotate({ description: "Filename to record on the file object." }))
|
|
89
|
+
}),
|
|
90
|
+
output: Schema.Struct({ object: Type.getSchema(File.File) }),
|
|
91
|
+
services: [Database.Service, Capability.Service]
|
|
92
|
+
});
|
|
24
93
|
var Read = Operation.make({
|
|
25
94
|
meta: {
|
|
26
95
|
key: DXN.make("org.dxos.operation.file.read"),
|
|
@@ -32,7 +101,31 @@ var Read = Operation.make({
|
|
|
32
101
|
output: ContentBlock.ContentBlockResult,
|
|
33
102
|
services: [Database.Service]
|
|
34
103
|
});
|
|
104
|
+
/**
|
|
105
|
+
* Names a file's bytes to the host that will sign a download URL for them — the reverse of
|
|
106
|
+
* {@link CreateFromUpload}.
|
|
107
|
+
*
|
|
108
|
+
* Host-internal: the MCP hosts' `createDownload` tool invokes it and signs the URL, since the
|
|
109
|
+
* signing secret (EDGE) or the loopback listener (`dx mcp serve`) lives in the host, not in the
|
|
110
|
+
* operation runtime. No skill lists it, so it is never offered to a model directly.
|
|
111
|
+
*/
|
|
112
|
+
var ResolveDownload = Operation.make({
|
|
113
|
+
meta: {
|
|
114
|
+
key: DXN.make("org.dxos.operation.file.resolveDownload"),
|
|
115
|
+
name: "Resolve File Download",
|
|
116
|
+
description: "Resolves a file to the id its host signs a download URL for.",
|
|
117
|
+
icon: "ph--file-arrow-down--regular"
|
|
118
|
+
},
|
|
119
|
+
input: Schema.Struct({ file: Ref.Ref(File.File).annotate({ description: "The file to download." }) }),
|
|
120
|
+
output: Schema.Struct({
|
|
121
|
+
downloadId: Schema.String.annotate({ description: "Opaque to the caller; names the bytes to the host that signs the download URL." }),
|
|
122
|
+
name: Schema.optional(Schema.String),
|
|
123
|
+
type: Schema.String,
|
|
124
|
+
size: Schema.Number
|
|
125
|
+
}),
|
|
126
|
+
services: [Database.Service]
|
|
127
|
+
});
|
|
35
128
|
//#endregion
|
|
36
|
-
export { Create, Read, FileOperation_exports as t };
|
|
129
|
+
export { Create, CreateFromSource, CreateFromUpload, FileSource, Read, ResolveDownload, FileOperation_exports as t };
|
|
37
130
|
|
|
38
131
|
//# sourceMappingURL=FileOperation.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FileOperation.mjs","names":[],"sources":["../../src/types/FileOperation.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Schema from 'effect/Schema';\nimport * as Struct from 'effect/Struct';\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as Operation from '@dxos/compute/Operation';\nimport { Database, DXN, Ref, Type } from '@dxos/echo';\nimport { ContentBlock, File } from '@dxos/types';\n\nimport * as FileCapabilities from './FileCapabilities';\n\nexport const Create = Operation.make({\n meta: { key: DXN.make('org.dxos.operation.file.create'), name: 'Create File', icon: 'ph--file--regular' },\n services: [Capability.Service],\n input: FileCapabilities.FileAction.CreateFileSchema.mapFields(Struct.assign({ db: Database.Database })),\n output: Schema.Struct({\n object: Type.getSchema(File.File),\n }),\n});\n\nexport const Read = Operation.make({\n meta: {\n key: DXN.make('org.dxos.operation.file.read'),\n name: 'Read File',\n description:\n 'Reads the contents of a file and returns them as a File content block (data URL for inline files, original URL for external files).',\n icon: 'ph--file-arrow-down--regular',\n },\n input: Schema.Struct({\n file: Ref.Ref(File.File).annotate({\n description: 'The file to read.',\n }),\n }),\n output: ContentBlock.ContentBlockResult,\n services: [Database.Service],\n});\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"FileOperation.mjs","names":[],"sources":["../../src/types/FileOperation.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\nimport * as Schema from 'effect/Schema';\nimport * as Struct from 'effect/Struct';\n\nimport * as Capability from '@dxos/app-framework/Capability';\nimport * as Operation from '@dxos/compute/Operation';\nimport { Database, DXN, Ref, Type } from '@dxos/echo';\nimport { ContentBlock, File } from '@dxos/types';\n\nimport * as FileCapabilities from './FileCapabilities.ts';\n\nexport const Create = Operation.make({\n meta: { key: DXN.make('org.dxos.operation.file.create'), name: 'Create File', icon: 'ph--file--regular' },\n services: [Capability.Service],\n input: FileCapabilities.FileAction.CreateFileSchema.mapFields(Struct.assign({ db: Database.Database })),\n output: Schema.Struct({\n object: Type.getSchema(File.File),\n }),\n});\n\n/**\n * Where the bytes come from. Mirrors `ContentBlock.ImageSource` rather than inventing a shape — it\n * is how this repo already models \"bytes, or a pointer to bytes\".\n *\n * Two arms because their costs differ sharply: `base64` rides in the tool-call arguments and so\n * occupies the caller's context, while `http` is fetched by the host and never crosses it.\n */\nexport const FileSource = Schema.Union([\n Schema.Struct({\n type: Schema.tag('base64'),\n mediaType: Schema.String.annotate({ description: 'MIME type of the encoded bytes, e.g. image/png.' }),\n data: Schema.String.annotate({ description: 'Base64-encoded file contents.' }),\n }),\n Schema.Struct({\n type: Schema.tag('http'),\n url: Schema.String.annotate({ description: 'HTTPS URL the host downloads. Must not be a private address.' }),\n }),\n]);\n\n/**\n * Creates a file from serializable input, so an agent can reach it.\n *\n * Separate from {@link Create} rather than replacing it: `Create` takes a live browser `File`, which\n * is right for the upload UI and cannot render as JSON Schema — an operation whose input cannot is\n * dropped from the registry silently, and so is invisible to any tool caller.\n *\n * Takes the database as a service rather than an input field, which is both what makes the input\n * serializable and what forces the caller to name a space.\n */\nexport const CreateFromSource = Operation.make({\n meta: {\n key: DXN.make('org.dxos.operation.file.createFromSource'),\n name: 'Create File From Source',\n description:\n 'Creates a file in the space from base64 content or an HTTPS URL, storing it via the configured backend.',\n icon: 'ph--file-plus--regular',\n },\n input: Schema.Struct({\n source: FileSource,\n name: Schema.optional(Schema.String.annotate({ description: 'Filename to record on the file object.' })),\n }),\n output: Schema.Struct({\n object: Type.getSchema(File.File),\n }),\n services: [Database.Service, Capability.Service],\n});\n\n/**\n * Creates a file from bytes already uploaded to blob storage over a signed URL.\n *\n * The third arm alongside {@link CreateFromSource}'s two, and the one for a file that exists only\n * on the caller's disk: `base64` would have a model emit the bytes token by token, and `http`\n * requires them to already be served somewhere public. Here the caller uploads directly (see the\n * MCP server's `createUpload` tool) and passes only the id, so a screen recording costs the same\n * few tokens as a screenshot.\n *\n * No media type or size input: nothing the caller says about the bytes is trusted or needed, since\n * the blob service measured them as they arrived.\n */\nexport const CreateFromUpload = Operation.make({\n meta: {\n key: DXN.make('org.dxos.operation.file.createFromUpload'),\n name: 'Create File From Upload',\n description:\n 'Creates a file in the space from a completed direct upload, given the uploadId returned by ' +\n 'the createUpload tool. Use after the upload command has succeeded.',\n icon: 'ph--upload--regular',\n },\n input: Schema.Struct({\n uploadId: Schema.String.annotate({\n description: 'The uploadId returned by createUpload, whose bytes have already been uploaded.',\n }),\n name: Schema.optional(Schema.String.annotate({ description: 'Filename to record on the file object.' })),\n }),\n output: Schema.Struct({\n object: Type.getSchema(File.File),\n }),\n services: [Database.Service, Capability.Service],\n});\n\nexport const Read = Operation.make({\n meta: {\n key: DXN.make('org.dxos.operation.file.read'),\n name: 'Read File',\n description:\n 'Reads the contents of a file and returns them as a File content block (data URL for inline files, original URL for external files).',\n icon: 'ph--file-arrow-down--regular',\n },\n input: Schema.Struct({\n file: Ref.Ref(File.File).annotate({\n description: 'The file to read.',\n }),\n }),\n output: ContentBlock.ContentBlockResult,\n services: [Database.Service],\n});\n\n/**\n * Names a file's bytes to the host that will sign a download URL for them — the reverse of\n * {@link CreateFromUpload}.\n *\n * Host-internal: the MCP hosts' `createDownload` tool invokes it and signs the URL, since the\n * signing secret (EDGE) or the loopback listener (`dx mcp serve`) lives in the host, not in the\n * operation runtime. No skill lists it, so it is never offered to a model directly.\n */\nexport const ResolveDownload = Operation.make({\n meta: {\n key: DXN.make('org.dxos.operation.file.resolveDownload'),\n name: 'Resolve File Download',\n description: 'Resolves a file to the id its host signs a download URL for.',\n icon: 'ph--file-arrow-down--regular',\n },\n input: Schema.Struct({\n file: Ref.Ref(File.File).annotate({ description: 'The file to download.' }),\n }),\n output: Schema.Struct({\n downloadId: Schema.String.annotate({\n description: 'Opaque to the caller; names the bytes to the host that signs the download URL.',\n }),\n name: Schema.optional(Schema.String),\n type: Schema.String,\n size: Schema.Number,\n }),\n services: [Database.Service],\n});\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,IAAa,SAAS,UAAU,KAAK;CACnC,MAAM;EAAE,KAAK,IAAI,KAAK,gCAAgC;EAAG,MAAM;EAAe,MAAM;CAAoB;CACxG,UAAU,CAAC,WAAW,OAAO;CAC7B,OAAA,WAAmC,iBAAiB,UAAU,OAAO,OAAO,EAAE,IAAI,SAAS,SAAS,CAAC,CAAC;CACtG,QAAQ,OAAO,OAAO,EACpB,QAAQ,KAAK,UAAU,KAAK,IAAI,EAClC,CAAC;AACH,CAAC;;;;;;;;AASD,IAAa,aAAa,OAAO,MAAM,CACrC,OAAO,OAAO;CACZ,MAAM,OAAO,IAAI,QAAQ;CACzB,WAAW,OAAO,OAAO,SAAS,EAAE,aAAa,kDAAkD,CAAC;CACpG,MAAM,OAAO,OAAO,SAAS,EAAE,aAAa,gCAAgC,CAAC;AAC/E,CAAC,GACD,OAAO,OAAO;CACZ,MAAM,OAAO,IAAI,MAAM;CACvB,KAAK,OAAO,OAAO,SAAS,EAAE,aAAa,+DAA+D,CAAC;AAC7G,CAAC,CACH,CAAC;;;;;;;;;;;AAYD,IAAa,mBAAmB,UAAU,KAAK;CAC7C,MAAM;EACJ,KAAK,IAAI,KAAK,0CAA0C;EACxD,MAAM;EACN,aACE;EACF,MAAM;CACR;CACA,OAAO,OAAO,OAAO;EACnB,QAAQ;EACR,MAAM,OAAO,SAAS,OAAO,OAAO,SAAS,EAAE,aAAa,yCAAyC,CAAC,CAAC;CACzG,CAAC;CACD,QAAQ,OAAO,OAAO,EACpB,QAAQ,KAAK,UAAU,KAAK,IAAI,EAClC,CAAC;CACD,UAAU,CAAC,SAAS,SAAS,WAAW,OAAO;AACjD,CAAC;;;;;;;;;;;;;AAcD,IAAa,mBAAmB,UAAU,KAAK;CAC7C,MAAM;EACJ,KAAK,IAAI,KAAK,0CAA0C;EACxD,MAAM;EACN,aACE;EAEF,MAAM;CACR;CACA,OAAO,OAAO,OAAO;EACnB,UAAU,OAAO,OAAO,SAAS,EAC/B,aAAa,iFACf,CAAC;EACD,MAAM,OAAO,SAAS,OAAO,OAAO,SAAS,EAAE,aAAa,yCAAyC,CAAC,CAAC;CACzG,CAAC;CACD,QAAQ,OAAO,OAAO,EACpB,QAAQ,KAAK,UAAU,KAAK,IAAI,EAClC,CAAC;CACD,UAAU,CAAC,SAAS,SAAS,WAAW,OAAO;AACjD,CAAC;AAED,IAAa,OAAO,UAAU,KAAK;CACjC,MAAM;EACJ,KAAK,IAAI,KAAK,8BAA8B;EAC5C,MAAM;EACN,aACE;EACF,MAAM;CACR;CACA,OAAO,OAAO,OAAO,EACnB,MAAM,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,SAAS,EAChC,aAAa,oBACf,CAAC,EACH,CAAC;CACD,QAAQ,aAAa;CACrB,UAAU,CAAC,SAAS,OAAO;AAC7B,CAAC;;;;;;;;;AAUD,IAAa,kBAAkB,UAAU,KAAK;CAC5C,MAAM;EACJ,KAAK,IAAI,KAAK,yCAAyC;EACvD,MAAM;EACN,aAAa;EACb,MAAM;CACR;CACA,OAAO,OAAO,OAAO,EACnB,MAAM,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,aAAa,wBAAwB,CAAC,EAC5E,CAAC;CACD,QAAQ,OAAO,OAAO;EACpB,YAAY,OAAO,OAAO,SAAS,EACjC,aAAa,iFACf,CAAC;EACD,MAAM,OAAO,SAAS,OAAO,MAAM;EACnC,MAAM,OAAO;EACb,MAAM,OAAO;CACf,CAAC;CACD,UAAU,CAAC,SAAS,OAAO;AAC7B,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
|
|
2
|
+
import { FileOperation } from "#types";
|
|
3
|
+
import * as Operation from "@dxos/compute/Operation";
|
|
4
|
+
import * as Skill from "@dxos/compute/Skill";
|
|
5
|
+
import * as Template from "@dxos/compute/Template";
|
|
6
|
+
import { trim } from "@dxos/util";
|
|
7
|
+
//#region src/skills/FileSkill.ts
|
|
8
|
+
var FileSkill_exports = /* @__PURE__ */ __exportAll({
|
|
9
|
+
key: () => key,
|
|
10
|
+
make: () => make,
|
|
11
|
+
operations: () => operations
|
|
12
|
+
});
|
|
13
|
+
var key = "org.dxos.skill.file";
|
|
14
|
+
var operations = [
|
|
15
|
+
FileOperation.Read,
|
|
16
|
+
FileOperation.CreateFromSource,
|
|
17
|
+
FileOperation.CreateFromUpload
|
|
18
|
+
];
|
|
19
|
+
var make = () => Skill.make({
|
|
20
|
+
key,
|
|
21
|
+
name: "File",
|
|
22
|
+
description: "Read the contents of files (images, videos, PDFs), and add new files to a space.",
|
|
23
|
+
tools: Skill.toolDefinitions({ operations }),
|
|
24
|
+
instructions: Template.make({ source: trim`
|
|
25
|
+
{{! File }}
|
|
26
|
+
|
|
27
|
+
You can read the contents of files.
|
|
28
|
+
Calling the ${Operation.toolName(FileOperation.Read)} tool returns the file contents as a File content block (a data URL for
|
|
29
|
+
inline files, the original URL for external files). The model receives the file natively
|
|
30
|
+
and can describe, transcribe, or otherwise reason over its contents.
|
|
31
|
+
|
|
32
|
+
You can also add a file to the space with the ${Operation.toolName(FileOperation.CreateFromSource)} tool, from one of two sources:
|
|
33
|
+
- \`{ "type": "http", "url": "https://..." }\` when the content is already reachable at a URL.
|
|
34
|
+
Prefer this whenever it applies: the bytes are fetched by the host and never pass through
|
|
35
|
+
the conversation. The URL must be https and must not point at a private address.
|
|
36
|
+
- \`{ "type": "base64", "mediaType": "image/png", "data": "..." }\` when you hold the bytes
|
|
37
|
+
yourself, such as an image you generated. Keep this under 1MB — the encoded payload counts
|
|
38
|
+
against the conversation, so a large file is slow and expensive before it is anything else.
|
|
39
|
+
|
|
40
|
+
For a file that is already on your own disk -- a screenshot you just took, a screen
|
|
41
|
+
recording, a log bundle -- use neither arm above. Upload it directly:
|
|
42
|
+
1. Call the host's \`createUpload\` tool (with the file's \`name\`, and its \`size\` if known).
|
|
43
|
+
2. Run the \`command\` it returns in a shell; it transfers the bytes with \`curl\`.
|
|
44
|
+
3. Call ${Operation.toolName(FileOperation.CreateFromUpload)} with the returned \`uploadId\` to create the file object.
|
|
45
|
+
The bytes go from disk to storage without passing through this conversation, so the cost is
|
|
46
|
+
the same whether the file is 40KB or 90MB. Base64 is never the right choice for a video.
|
|
47
|
+
|
|
48
|
+
To save a file from the space to your own disk, download it directly:
|
|
49
|
+
1. Call the host's \`createDownload\` tool with the file's reference.
|
|
50
|
+
2. Run the \`command\` it returns in a shell; it saves the file in the working directory.
|
|
51
|
+
Use ${Operation.toolName(FileOperation.Read)} only when you need to look at a file yourself; never read one
|
|
52
|
+
just to write its bytes out. \`createUpload\` and \`createDownload\` are tools of the MCP host, not
|
|
53
|
+
operations; a host without them (the in-app chat) has no direct transfer.
|
|
54
|
+
|
|
55
|
+
Any file type is accepted; HTML and XML are stored as \`application/octet-stream\` so they
|
|
56
|
+
download rather than render. Always pass the true media type of the content; do not infer it
|
|
57
|
+
from a file extension.
|
|
58
|
+
` }),
|
|
59
|
+
agentCanEnable: true,
|
|
60
|
+
mcpPrompt: true
|
|
61
|
+
});
|
|
62
|
+
//#endregion
|
|
63
|
+
export { key, make, operations, FileSkill_exports as t };
|
|
64
|
+
|
|
65
|
+
//# sourceMappingURL=FileSkill.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileSkill.mjs","names":[],"sources":["../../src/skills/FileSkill.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Operation from '@dxos/compute/Operation';\nimport * as Skill from '@dxos/compute/Skill';\nimport * as Template from '@dxos/compute/Template';\nimport { trim } from '@dxos/util';\n\nimport { FileOperation } from '#types';\n\nexport const key = 'org.dxos.skill.file';\n\n// Carried on the definition so a host that serves it over MCP can map its tool ids back to\n// operations without the plugin being activated.\nexport const operations: readonly Operation.Definition.Any[] = [\n FileOperation.Read,\n FileOperation.CreateFromSource,\n FileOperation.CreateFromUpload,\n];\n\nexport const make = () =>\n Skill.make({\n key: key,\n name: 'File',\n description: 'Read the contents of files (images, videos, PDFs), and add new files to a space.',\n tools: Skill.toolDefinitions({\n operations,\n }),\n instructions: Template.make({\n source: trim`\n {{! File }}\n\n You can read the contents of files.\n Calling the ${Operation.toolName(FileOperation.Read)} tool returns the file contents as a File content block (a data URL for\n inline files, the original URL for external files). The model receives the file natively\n and can describe, transcribe, or otherwise reason over its contents.\n\n You can also add a file to the space with the ${Operation.toolName(FileOperation.CreateFromSource)} tool, from one of two sources:\n - \\`{ \"type\": \"http\", \"url\": \"https://...\" }\\` when the content is already reachable at a URL.\n Prefer this whenever it applies: the bytes are fetched by the host and never pass through\n the conversation. The URL must be https and must not point at a private address.\n - \\`{ \"type\": \"base64\", \"mediaType\": \"image/png\", \"data\": \"...\" }\\` when you hold the bytes\n yourself, such as an image you generated. Keep this under 1MB — the encoded payload counts\n against the conversation, so a large file is slow and expensive before it is anything else.\n\n For a file that is already on your own disk -- a screenshot you just took, a screen\n recording, a log bundle -- use neither arm above. Upload it directly:\n 1. Call the host's \\`createUpload\\` tool (with the file's \\`name\\`, and its \\`size\\` if known).\n 2. Run the \\`command\\` it returns in a shell; it transfers the bytes with \\`curl\\`.\n 3. Call ${Operation.toolName(FileOperation.CreateFromUpload)} with the returned \\`uploadId\\` to create the file object.\n The bytes go from disk to storage without passing through this conversation, so the cost is\n the same whether the file is 40KB or 90MB. Base64 is never the right choice for a video.\n\n To save a file from the space to your own disk, download it directly:\n 1. Call the host's \\`createDownload\\` tool with the file's reference.\n 2. Run the \\`command\\` it returns in a shell; it saves the file in the working directory.\n Use ${Operation.toolName(FileOperation.Read)} only when you need to look at a file yourself; never read one\n just to write its bytes out. \\`createUpload\\` and \\`createDownload\\` are tools of the MCP host, not\n operations; a host without them (the in-app chat) has no direct transfer.\n\n Any file type is accepted; HTML and XML are stored as \\`application/octet-stream\\` so they\n download rather than render. Always pass the true media type of the content; do not infer it\n from a file extension.\n `,\n }),\n agentCanEnable: true,\n // Projected by the MCP hosts (`dx mcp serve`, EDGE's worker), so `file.createFromUpload` — the\n // other half of their `createUpload` tool — is discoverable and invocable there.\n mcpPrompt: true,\n });\n"],"mappings":";;;;;;;;;;;;AAWA,IAAa,MAAM;AAInB,IAAa,aAAkD;CAC7D,cAAc;CACd,cAAc;CACd,cAAc;AAChB;AAEA,IAAa,aACX,MAAM,KAAK;CACJ;CACL,MAAM;CACN,aAAa;CACb,OAAO,MAAM,gBAAgB,EAC3B,WACF,CAAC;CACD,cAAc,SAAS,KAAK,EAC1B,QAAQ,IAAI;;;;sBAII,UAAU,SAAS,cAAc,IAAI,EAAE;;;;wDAIL,UAAU,SAAS,cAAc,gBAAgB,EAAE;;;;;;;;;;;;kBAYzF,UAAU,SAAS,cAAc,gBAAgB,EAAE;;;;;;;cAOvD,UAAU,SAAS,cAAc,IAAI,EAAE;;;;;;;QAQjD,CAAC;CACD,gBAAgB;CAGhB,WAAW;AACb,CAAC"}
|