@qilin-agent/attachment 0.0.0-stage → 3.1.3
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/LICENSE +21 -0
- package/README.i18n.yaml +67 -0
- package/README.md +137 -2
- package/README.zh.md +138 -0
- package/lib/index.js +344 -0
- package/lib/types/admission.d.ts +26 -0
- package/lib/types/admission.js +52 -0
- package/lib/types/brand.d.ts +19 -0
- package/lib/types/brand.js +18 -0
- package/lib/types/error.d.ts +42 -0
- package/lib/types/error.js +72 -0
- package/lib/types/index.d.ts +132 -0
- package/lib/types/index.js +162 -0
- package/lib/types/request-projection.d.ts +28 -0
- package/lib/types/request-projection.js +49 -0
- package/lib/types/types.d.ts +155 -0
- package/lib/types/types.js +3 -0
- package/package.json +42 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { Service } from "@qilin-agent/kylin";
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
//#region lib/types/error.js
|
|
4
|
+
/** Attachment failure class. @module @qilin-agent/attachment/error */
|
|
5
|
+
const IMAGE_ADMISSION_ERROR_CODES = [
|
|
6
|
+
"TOO_MANY_IMAGES",
|
|
7
|
+
"IMAGES_TOO_LARGE",
|
|
8
|
+
"UNSUPPORTED_IMAGE_TYPE",
|
|
9
|
+
"INVALID_IMAGE_BASE64",
|
|
10
|
+
"INVALID_IMAGE",
|
|
11
|
+
"IMAGE_TYPE_MISMATCH",
|
|
12
|
+
"IMAGE_TOO_LARGE",
|
|
13
|
+
"IMAGE_TOO_MANY_PIXELS",
|
|
14
|
+
"IMAGE_DIMENSION_TOO_LARGE"
|
|
15
|
+
];
|
|
16
|
+
const ATTACHMENT_ERROR_CODES = [
|
|
17
|
+
...IMAGE_ADMISSION_ERROR_CODES,
|
|
18
|
+
"INVALID_FILE_BASE64",
|
|
19
|
+
"INVALID_ATTACHMENT_REF",
|
|
20
|
+
"ATTACHMENT_CORRUPT",
|
|
21
|
+
"ATTACHMENT_WRITE_FAILED",
|
|
22
|
+
"ATTACHMENT_NOT_FOUND",
|
|
23
|
+
"ATTACHMENT_READ_FAILED",
|
|
24
|
+
"ATTACHMENT_PROJECTION_UNSUPPORTED",
|
|
25
|
+
"ATTACHMENT_FILES_UNSUPPORTED"
|
|
26
|
+
];
|
|
27
|
+
/** Runtime membership for structurally compatible errors crossing package boundaries. */
|
|
28
|
+
const IMAGE_ADMISSION_ERROR_CODE_SET = new Set(IMAGE_ADMISSION_ERROR_CODES);
|
|
29
|
+
const ATTACHMENT_ERROR_CODE_SET = new Set(ATTACHMENT_ERROR_CODES);
|
|
30
|
+
/**
|
|
31
|
+
* Stable failures suitable for host RPC error mapping.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately re-implements the `HarnessError` shape instead of extending it:
|
|
34
|
+
* the base lives in `@qilin-agent/llm`, which itself depends on this
|
|
35
|
+
* package (`ImageBlock` references `ImageAttachmentRef`), so sharing the base
|
|
36
|
+
* would create a dependency cycle. Consumers route on `code`, never on the
|
|
37
|
+
* prototype chain, so the shapes stay interchangeable at the wire boundary.
|
|
38
|
+
*/
|
|
39
|
+
var AttachmentError = class extends Error {
|
|
40
|
+
/** Stable machine-routing failure code. */
|
|
41
|
+
code;
|
|
42
|
+
/**
|
|
43
|
+
* @param message - human-readable failure description without raw bytes or host paths.
|
|
44
|
+
* @param code - stable machine-routing code.
|
|
45
|
+
* @param options - optional chained cause.
|
|
46
|
+
*/
|
|
47
|
+
constructor(message, code, options) {
|
|
48
|
+
super(message, options);
|
|
49
|
+
this.name = "AttachmentError";
|
|
50
|
+
this.code = code;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Identify attachment failures by their stable code across duplicate package installations.
|
|
55
|
+
* @param error - failure raised while validating, persisting, or reading an attachment.
|
|
56
|
+
* @returns whether the failure carries a recognized attachment error code.
|
|
57
|
+
*/
|
|
58
|
+
function isAttachmentError(error) {
|
|
59
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" && ATTACHMENT_ERROR_CODE_SET.has(error.code);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Distinguish caller-correctable image admission failures from storage faults.
|
|
63
|
+
* @param error - failure raised while validating or persisting an image batch.
|
|
64
|
+
* @returns whether the caller can correct the proposed image content or batch.
|
|
65
|
+
*/
|
|
66
|
+
function isImageAdmissionError(error) {
|
|
67
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" && IMAGE_ADMISSION_ERROR_CODE_SET.has(error.code);
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region lib/types/admission.js
|
|
71
|
+
/** Wire-form admission of base64-encoded image uploads. @module @qilin-agent/attachment/admission */
|
|
72
|
+
/** Decode one upload payload while rejecting non-canonical base64 forms. */
|
|
73
|
+
function decodeCanonicalBase64(data, empty, code) {
|
|
74
|
+
const decoded = Buffer.from(data, "base64");
|
|
75
|
+
if (data.length === 0 && empty === "reject" || decoded.toString("base64") !== data) throw new AttachmentError(code === "INVALID_IMAGE_BASE64" ? "Image upload is not canonical base64." : "File upload is not canonical base64.", code);
|
|
76
|
+
return new Uint8Array(decoded);
|
|
77
|
+
}
|
|
78
|
+
function decodeBase64(data) {
|
|
79
|
+
return decodeCanonicalBase64(data, "reject", "INVALID_IMAGE_BASE64");
|
|
80
|
+
}
|
|
81
|
+
/** Store input for one decoded upload. */
|
|
82
|
+
function saveInput(image) {
|
|
83
|
+
return {
|
|
84
|
+
data: decodeBase64(image.data),
|
|
85
|
+
mediaType: image.mediaType,
|
|
86
|
+
...image.name === void 0 ? {} : { name: image.name }
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Admit one wire image batch: enforce canonical base64 on every member, then
|
|
91
|
+
* delegate batch admission — count and aggregate-byte limits, media-type and
|
|
92
|
+
* per-image validation, ordered commit — to {@link AttachmentStore.saveImages}.
|
|
93
|
+
* The shared entry for every RPC endpoint accepting browser uploads.
|
|
94
|
+
* @param attachments - the deployment attachment store owning batch policy.
|
|
95
|
+
* @param images - base64-encoded uploads in caller order.
|
|
96
|
+
* @returns durable references in the same order as `images`.
|
|
97
|
+
* @throws AttachmentError on a non-canonical payload or a refused batch.
|
|
98
|
+
*/
|
|
99
|
+
async function admitEncodedImages(attachments, images) {
|
|
100
|
+
return attachments.saveImages(images.map(saveInput));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Admit one wire file upload: enforce canonical base64 (an empty file is a
|
|
104
|
+
* valid zero-byte payload), then delegate verbatim commit to
|
|
105
|
+
* {@link AttachmentStore.saveFile}. The shared entry for every RPC endpoint
|
|
106
|
+
* accepting browser file uploads.
|
|
107
|
+
* @param attachments - the deployment attachment store.
|
|
108
|
+
* @param file - base64-encoded upload and optional display name.
|
|
109
|
+
* @returns the durable content-addressed file reference.
|
|
110
|
+
* @throws AttachmentError on a non-canonical payload or a storage failure.
|
|
111
|
+
*/
|
|
112
|
+
async function admitEncodedFile(attachments, file) {
|
|
113
|
+
return attachments.saveFile({
|
|
114
|
+
data: decodeCanonicalBase64(file.data, "accept", "INVALID_FILE_BASE64"),
|
|
115
|
+
...file.name === void 0 ? {} : { name: file.name }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region lib/types/brand.js
|
|
120
|
+
/** Attachment identifier brand. @module @qilin-agent/attachment/brand */
|
|
121
|
+
/**
|
|
122
|
+
* Brand a validated storage identifier.
|
|
123
|
+
* @param value - backend-produced opaque identifier.
|
|
124
|
+
* @returns the branded identifier.
|
|
125
|
+
*/
|
|
126
|
+
function AttachmentId(value) {
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Brand a validated request-image transformation identifier.
|
|
131
|
+
* @param value - attachment-provider-produced opaque identifier.
|
|
132
|
+
* @returns the branded identifier.
|
|
133
|
+
*/
|
|
134
|
+
function ImageVariantId(value) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region lib/types/request-projection.js
|
|
139
|
+
/**
|
|
140
|
+
* Pure request-projection geometry shared by model routes and provider-side
|
|
141
|
+
* request pricing. @module @qilin-agent/attachment/request-projection
|
|
142
|
+
*/
|
|
143
|
+
/**
|
|
144
|
+
* Compute aspect-preserving integer dimensions within a hard total-pixel budget.
|
|
145
|
+
* @param width - positive source width.
|
|
146
|
+
* @param height - positive source height.
|
|
147
|
+
* @param maxPixels - positive width-times-height cap.
|
|
148
|
+
* @returns inward-rounded dimensions; small images are not enlarged.
|
|
149
|
+
*/
|
|
150
|
+
function requestImageDimensions(width, height, maxPixels) {
|
|
151
|
+
const scale = Math.min(1, Math.sqrt(maxPixels / (width * height)));
|
|
152
|
+
if (scale === 1) return {
|
|
153
|
+
width,
|
|
154
|
+
height
|
|
155
|
+
};
|
|
156
|
+
if (width >= height) {
|
|
157
|
+
let projectedWidth = Math.max(1, Math.floor(width * scale));
|
|
158
|
+
let projectedHeight = Math.max(1, Math.round(projectedWidth * height / width));
|
|
159
|
+
while (projectedWidth * projectedHeight > maxPixels && projectedWidth > 1) {
|
|
160
|
+
projectedWidth -= 1;
|
|
161
|
+
projectedHeight = Math.max(1, Math.round(projectedWidth * height / width));
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
width: projectedWidth,
|
|
165
|
+
height: projectedHeight
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
let projectedHeight = Math.max(1, Math.floor(height * scale));
|
|
169
|
+
let projectedWidth = Math.max(1, Math.round(projectedHeight * width / height));
|
|
170
|
+
while (projectedWidth * projectedHeight > maxPixels && projectedHeight > 1) {
|
|
171
|
+
projectedHeight -= 1;
|
|
172
|
+
projectedWidth = Math.max(1, Math.round(projectedHeight * width / height));
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
width: projectedWidth,
|
|
176
|
+
height: projectedHeight
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Compute aspect-preserving integer dimensions with an exact long edge; the
|
|
181
|
+
* short edge rounds to the nearest pixel, as an encoder resize by the
|
|
182
|
+
* long edge alone.
|
|
183
|
+
* @param width - positive source width.
|
|
184
|
+
* @param height - positive source height.
|
|
185
|
+
* @param longEdge - positive target for the longer source edge.
|
|
186
|
+
* @returns the target dimensions; a long edge at or above the source returns the source unchanged.
|
|
187
|
+
*/
|
|
188
|
+
function longEdgeDimensions(width, height, longEdge) {
|
|
189
|
+
if (longEdge >= Math.max(width, height)) return {
|
|
190
|
+
width,
|
|
191
|
+
height
|
|
192
|
+
};
|
|
193
|
+
return width >= height ? {
|
|
194
|
+
width: longEdge,
|
|
195
|
+
height: Math.max(1, Math.round(longEdge * height / width))
|
|
196
|
+
} : {
|
|
197
|
+
width: Math.max(1, Math.round(longEdge * width / height)),
|
|
198
|
+
height: longEdge
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region lib/types/index.js
|
|
203
|
+
/** Durable attachment storage seam (`ctx.attachments`). @module @qilin-agent/attachment */
|
|
204
|
+
/** Immutable binary attachment service. Implementations validate bytes before publishing a reference. */
|
|
205
|
+
var AttachmentStore = class extends Service {
|
|
206
|
+
constructor(ctx) {
|
|
207
|
+
super(ctx, "attachments");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Validate one ordered image batch before committing any member.
|
|
211
|
+
* Validation failures start no writes; storage failures return no partial
|
|
212
|
+
* references, although already published content-addressed objects may stay
|
|
213
|
+
* unreachable until a future retention policy collects them.
|
|
214
|
+
* @param inputs - encoded images in their owning message order.
|
|
215
|
+
* @returns durable references in the exact input order.
|
|
216
|
+
*/
|
|
217
|
+
validateImageBatch(inputs) {
|
|
218
|
+
const { maxImagesPerMessage, maxMessageImageBytes, mediaTypes } = this.imageLimits;
|
|
219
|
+
if (inputs.length > maxImagesPerMessage) throw new AttachmentError("Image batch exceeds the configured image-count limit.", "TOO_MANY_IMAGES");
|
|
220
|
+
if (inputs.reduce((sum, input) => sum + input.data.byteLength, 0) > maxMessageImageBytes) throw new AttachmentError("Image batch exceeds the configured aggregate image-byte limit.", "IMAGES_TOO_LARGE");
|
|
221
|
+
for (const input of inputs) if (!mediaTypes.includes(input.mediaType)) throw new AttachmentError(`Image type ${input.mediaType} is not accepted by this deployment.`, "UNSUPPORTED_IMAGE_TYPE");
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Validate and durably commit one ordered image batch.
|
|
225
|
+
* @param inputs - encoded images in owning-message order.
|
|
226
|
+
* @returns durable normalized attachment references in the same order after every member succeeds.
|
|
227
|
+
*/
|
|
228
|
+
async saveImages(inputs) {
|
|
229
|
+
this.validateImageBatch(inputs);
|
|
230
|
+
for (const input of inputs) await this.validateImage(input);
|
|
231
|
+
const refs = [];
|
|
232
|
+
for (const input of inputs) refs.push(await this.saveImage(input));
|
|
233
|
+
return refs;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Admit one Host prompt and replace each uploaded image with its durable reference.
|
|
237
|
+
* Text and durable file references pass through unchanged. A prompt without image parts performs no storage operation.
|
|
238
|
+
* @param content - prompt parts in message order after file receipt resolution.
|
|
239
|
+
* @returns admitted prompt parts in the same order as `content`.
|
|
240
|
+
* @throws AttachmentError when the image batch is refused.
|
|
241
|
+
*/
|
|
242
|
+
async admitPromptContent(content) {
|
|
243
|
+
if (content.every((part) => part.type !== "image")) return content.map((part) => part.type === "text" ? {
|
|
244
|
+
type: "text",
|
|
245
|
+
text: part.text
|
|
246
|
+
} : {
|
|
247
|
+
type: "file",
|
|
248
|
+
attachment: part.attachment
|
|
249
|
+
});
|
|
250
|
+
const refs = await admitEncodedImages(this, content.filter((part) => part.type === "image"));
|
|
251
|
+
let next = 0;
|
|
252
|
+
return content.map((part) => {
|
|
253
|
+
if (part.type === "text") return {
|
|
254
|
+
type: "text",
|
|
255
|
+
text: part.text
|
|
256
|
+
};
|
|
257
|
+
if (part.type === "file") return {
|
|
258
|
+
type: "file",
|
|
259
|
+
attachment: part.attachment
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
type: "image",
|
|
263
|
+
attachment: refs[next++]
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Decode and durably commit one canonical base64 file upload.
|
|
269
|
+
* @param input - canonical base64 bytes and optional display name.
|
|
270
|
+
* @returns the durable content-addressed file reference.
|
|
271
|
+
* @throws AttachmentError when the encoding or storage operation is refused.
|
|
272
|
+
*/
|
|
273
|
+
admitEncodedFile(input) {
|
|
274
|
+
return admitEncodedFile(this, input);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Identify a failure emitted by this attachment capability by its stable code.
|
|
278
|
+
* @param error - value caught from an attachment operation.
|
|
279
|
+
* @returns whether the value is an attachment failure.
|
|
280
|
+
*/
|
|
281
|
+
isAttachmentError(error) {
|
|
282
|
+
return isAttachmentError(error);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Locate the provider-owned normalized object in the harness host filesystem.
|
|
286
|
+
* @param ref - durable normalized attachment reference.
|
|
287
|
+
* @returns an absolute host path, or undefined when this backend is not host-file-backed.
|
|
288
|
+
* @throws an AttachmentError when the durable reference is invalid.
|
|
289
|
+
*/
|
|
290
|
+
imageHostPath(ref) {}
|
|
291
|
+
/**
|
|
292
|
+
* Durably commit one file byte-for-byte before its owning session event is
|
|
293
|
+
* appended. Files carry no admission limits: any byte content and length is
|
|
294
|
+
* accepted, and the stored object is the exact submitted bytes. Backends
|
|
295
|
+
* without verbatim file storage keep this default rejection.
|
|
296
|
+
* @param input - exact bytes and optional display name.
|
|
297
|
+
* @returns the durable content-addressed file reference.
|
|
298
|
+
*/
|
|
299
|
+
saveFile(input) {
|
|
300
|
+
return Promise.reject(new AttachmentError("The mounted attachment provider cannot store verbatim files.", "ATTACHMENT_FILES_UNSUPPORTED"));
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Durably commit one file byte-for-byte from bounded chunks. Providers must
|
|
304
|
+
* apply backpressure and must not collect the complete file in memory.
|
|
305
|
+
* Backends without streamed verbatim storage keep this default rejection.
|
|
306
|
+
* @param input - ordered exact bytes, optional cancellation, and display name.
|
|
307
|
+
* @returns the durable content-addressed file reference.
|
|
308
|
+
*/
|
|
309
|
+
saveFileStream(input) {
|
|
310
|
+
return Promise.reject(new AttachmentError("The mounted attachment provider cannot stream verbatim files.", "ATTACHMENT_FILES_UNSUPPORTED"));
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Read and verify one verbatim stored file as bounded chunks. Providers must
|
|
314
|
+
* not collect the complete file in memory. Backends without verbatim file
|
|
315
|
+
* reads keep this default rejection.
|
|
316
|
+
* @param ref - durable reference from the session log.
|
|
317
|
+
* @param signal - optional cancellation for backend reads and verification work.
|
|
318
|
+
* @returns exact file bytes in order; integrity failures reject the iteration.
|
|
319
|
+
*/
|
|
320
|
+
async *readFileStream(ref, signal) {
|
|
321
|
+
signal?.throwIfAborted();
|
|
322
|
+
await Promise.reject(new AttachmentError("The mounted attachment provider cannot read verbatim files.", "ATTACHMENT_FILES_UNSUPPORTED"));
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Locate the verbatim stored file object in the harness host filesystem.
|
|
326
|
+
* @param ref - durable file reference.
|
|
327
|
+
* @returns an absolute host path, or undefined when this backend is not host-file-backed.
|
|
328
|
+
* @throws an AttachmentError when the durable reference is invalid.
|
|
329
|
+
*/
|
|
330
|
+
fileHostPath(ref) {}
|
|
331
|
+
/**
|
|
332
|
+
* Generate or read one deterministic model-request version from the stored normalized image.
|
|
333
|
+
* @param ref - durable provider-independent normalized attachment reference.
|
|
334
|
+
* @param target - route-chosen dimensions and byte target; an unmet byte target yields the smallest ladder output.
|
|
335
|
+
* @param signal - optional cancellation.
|
|
336
|
+
* @returns request bytes and the cache/upload identity covering every transform input.
|
|
337
|
+
*/
|
|
338
|
+
readImageRequest(ref, target, signal) {
|
|
339
|
+
signal?.throwIfAborted();
|
|
340
|
+
return Promise.reject(new AttachmentError("The mounted attachment provider cannot derive model-request images.", "ATTACHMENT_PROJECTION_UNSUPPORTED"));
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
//#endregion
|
|
344
|
+
export { AttachmentError, AttachmentId, AttachmentStore, AttachmentStore as default, ImageVariantId, admitEncodedFile, admitEncodedImages, isAttachmentError, isImageAdmissionError, longEdgeDimensions, requestImageDimensions };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Wire-form admission of base64-encoded image uploads. @module @qilin-agent/attachment/admission */
|
|
2
|
+
import type { AttachmentStore } from './index.ts';
|
|
3
|
+
import type { EncodedFileAttachment, EncodedImageAttachment, FileAttachmentRef, ImageAttachmentRef } from './types.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Admit one wire image batch: enforce canonical base64 on every member, then
|
|
6
|
+
* delegate batch admission — count and aggregate-byte limits, media-type and
|
|
7
|
+
* per-image validation, ordered commit — to {@link AttachmentStore.saveImages}.
|
|
8
|
+
* The shared entry for every RPC endpoint accepting browser uploads.
|
|
9
|
+
* @param attachments - the deployment attachment store owning batch policy.
|
|
10
|
+
* @param images - base64-encoded uploads in caller order.
|
|
11
|
+
* @returns durable references in the same order as `images`.
|
|
12
|
+
* @throws AttachmentError on a non-canonical payload or a refused batch.
|
|
13
|
+
*/
|
|
14
|
+
export declare function admitEncodedImages(attachments: AttachmentStore, images: readonly EncodedImageAttachment[]): Promise<readonly ImageAttachmentRef[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Admit one wire file upload: enforce canonical base64 (an empty file is a
|
|
17
|
+
* valid zero-byte payload), then delegate verbatim commit to
|
|
18
|
+
* {@link AttachmentStore.saveFile}. The shared entry for every RPC endpoint
|
|
19
|
+
* accepting browser file uploads.
|
|
20
|
+
* @param attachments - the deployment attachment store.
|
|
21
|
+
* @param file - base64-encoded upload and optional display name.
|
|
22
|
+
* @returns the durable content-addressed file reference.
|
|
23
|
+
* @throws AttachmentError on a non-canonical payload or a storage failure.
|
|
24
|
+
*/
|
|
25
|
+
export declare function admitEncodedFile(attachments: AttachmentStore, file: EncodedFileAttachment): Promise<FileAttachmentRef>;
|
|
26
|
+
//# sourceMappingURL=admission.d.ts.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Wire-form admission of base64-encoded image uploads. @module @qilin-agent/attachment/admission */
|
|
2
|
+
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { AttachmentError } from "./error.js";
|
|
4
|
+
/** Decode one upload payload while rejecting non-canonical base64 forms. */
|
|
5
|
+
function decodeCanonicalBase64(data, empty, code) {
|
|
6
|
+
const decoded = Buffer.from(data, 'base64');
|
|
7
|
+
if ((data.length === 0 && empty === 'reject') || decoded.toString('base64') !== data) {
|
|
8
|
+
throw new AttachmentError(code === 'INVALID_IMAGE_BASE64' ? 'Image upload is not canonical base64.' : 'File upload is not canonical base64.', code);
|
|
9
|
+
}
|
|
10
|
+
return new Uint8Array(decoded);
|
|
11
|
+
}
|
|
12
|
+
function decodeBase64(data) {
|
|
13
|
+
return decodeCanonicalBase64(data, 'reject', 'INVALID_IMAGE_BASE64');
|
|
14
|
+
}
|
|
15
|
+
/** Store input for one decoded upload. */
|
|
16
|
+
function saveInput(image) {
|
|
17
|
+
return {
|
|
18
|
+
data: decodeBase64(image.data),
|
|
19
|
+
mediaType: image.mediaType,
|
|
20
|
+
...image.name === undefined ? {} : { name: image.name },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Admit one wire image batch: enforce canonical base64 on every member, then
|
|
25
|
+
* delegate batch admission — count and aggregate-byte limits, media-type and
|
|
26
|
+
* per-image validation, ordered commit — to {@link AttachmentStore.saveImages}.
|
|
27
|
+
* The shared entry for every RPC endpoint accepting browser uploads.
|
|
28
|
+
* @param attachments - the deployment attachment store owning batch policy.
|
|
29
|
+
* @param images - base64-encoded uploads in caller order.
|
|
30
|
+
* @returns durable references in the same order as `images`.
|
|
31
|
+
* @throws AttachmentError on a non-canonical payload or a refused batch.
|
|
32
|
+
*/
|
|
33
|
+
export async function admitEncodedImages(attachments, images) {
|
|
34
|
+
return attachments.saveImages(images.map(saveInput));
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Admit one wire file upload: enforce canonical base64 (an empty file is a
|
|
38
|
+
* valid zero-byte payload), then delegate verbatim commit to
|
|
39
|
+
* {@link AttachmentStore.saveFile}. The shared entry for every RPC endpoint
|
|
40
|
+
* accepting browser file uploads.
|
|
41
|
+
* @param attachments - the deployment attachment store.
|
|
42
|
+
* @param file - base64-encoded upload and optional display name.
|
|
43
|
+
* @returns the durable content-addressed file reference.
|
|
44
|
+
* @throws AttachmentError on a non-canonical payload or a storage failure.
|
|
45
|
+
*/
|
|
46
|
+
export async function admitEncodedFile(attachments, file) {
|
|
47
|
+
return attachments.saveFile({
|
|
48
|
+
data: decodeCanonicalBase64(file.data, 'accept', 'INVALID_FILE_BASE64'),
|
|
49
|
+
...file.name === undefined ? {} : { name: file.name },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=admission.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Attachment identifier brand. @module @qilin-agent/attachment/brand */
|
|
2
|
+
import type { Branded } from '@qilin-agent/brand';
|
|
3
|
+
/** Opaque content-addressed identifier for one immutable attachment object. */
|
|
4
|
+
export type AttachmentId = Branded<'AttachmentId'>;
|
|
5
|
+
/**
|
|
6
|
+
* Brand a validated storage identifier.
|
|
7
|
+
* @param value - backend-produced opaque identifier.
|
|
8
|
+
* @returns the branded identifier.
|
|
9
|
+
*/
|
|
10
|
+
export declare function AttachmentId(value: string): AttachmentId;
|
|
11
|
+
/** Opaque deterministic identity for one request-image transformation. */
|
|
12
|
+
export type ImageVariantId = Branded<'ImageVariantId'>;
|
|
13
|
+
/**
|
|
14
|
+
* Brand a validated request-image transformation identifier.
|
|
15
|
+
* @param value - attachment-provider-produced opaque identifier.
|
|
16
|
+
* @returns the branded identifier.
|
|
17
|
+
*/
|
|
18
|
+
export declare function ImageVariantId(value: string): ImageVariantId;
|
|
19
|
+
//# sourceMappingURL=brand.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Attachment identifier brand. @module @qilin-agent/attachment/brand */
|
|
2
|
+
/**
|
|
3
|
+
* Brand a validated storage identifier.
|
|
4
|
+
* @param value - backend-produced opaque identifier.
|
|
5
|
+
* @returns the branded identifier.
|
|
6
|
+
*/
|
|
7
|
+
export function AttachmentId(value) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Brand a validated request-image transformation identifier.
|
|
12
|
+
* @param value - attachment-provider-produced opaque identifier.
|
|
13
|
+
* @returns the branded identifier.
|
|
14
|
+
*/
|
|
15
|
+
export function ImageVariantId(value) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Attachment failure class. @module @qilin-agent/attachment/error */
|
|
2
|
+
declare const IMAGE_ADMISSION_ERROR_CODES: readonly ["TOO_MANY_IMAGES", "IMAGES_TOO_LARGE", "UNSUPPORTED_IMAGE_TYPE", "INVALID_IMAGE_BASE64", "INVALID_IMAGE", "IMAGE_TYPE_MISMATCH", "IMAGE_TOO_LARGE", "IMAGE_TOO_MANY_PIXELS", "IMAGE_DIMENSION_TOO_LARGE"];
|
|
3
|
+
/** Caller-correctable attachment failure codes raised while admitting image input. */
|
|
4
|
+
export type ImageAdmissionErrorCode = typeof IMAGE_ADMISSION_ERROR_CODES[number];
|
|
5
|
+
declare const ATTACHMENT_ERROR_CODES: readonly ["TOO_MANY_IMAGES", "IMAGES_TOO_LARGE", "UNSUPPORTED_IMAGE_TYPE", "INVALID_IMAGE_BASE64", "INVALID_IMAGE", "IMAGE_TYPE_MISMATCH", "IMAGE_TOO_LARGE", "IMAGE_TOO_MANY_PIXELS", "IMAGE_DIMENSION_TOO_LARGE", "INVALID_FILE_BASE64", "INVALID_ATTACHMENT_REF", "ATTACHMENT_CORRUPT", "ATTACHMENT_WRITE_FAILED", "ATTACHMENT_NOT_FOUND", "ATTACHMENT_READ_FAILED", "ATTACHMENT_PROJECTION_UNSUPPORTED", "ATTACHMENT_FILES_UNSUPPORTED"];
|
|
6
|
+
/** Stable attachment failure codes used for protocol error routing. */
|
|
7
|
+
export type AttachmentErrorCode = typeof ATTACHMENT_ERROR_CODES[number];
|
|
8
|
+
/**
|
|
9
|
+
* Stable failures suitable for host RPC error mapping.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately re-implements the `HarnessError` shape instead of extending it:
|
|
12
|
+
* the base lives in `@qilin-agent/llm`, which itself depends on this
|
|
13
|
+
* package (`ImageBlock` references `ImageAttachmentRef`), so sharing the base
|
|
14
|
+
* would create a dependency cycle. Consumers route on `code`, never on the
|
|
15
|
+
* prototype chain, so the shapes stay interchangeable at the wire boundary.
|
|
16
|
+
*/
|
|
17
|
+
export declare class AttachmentError extends Error {
|
|
18
|
+
/** Stable machine-routing failure code. */
|
|
19
|
+
readonly code: AttachmentErrorCode;
|
|
20
|
+
/**
|
|
21
|
+
* @param message - human-readable failure description without raw bytes or host paths.
|
|
22
|
+
* @param code - stable machine-routing code.
|
|
23
|
+
* @param options - optional chained cause.
|
|
24
|
+
*/
|
|
25
|
+
constructor(message: string, code: AttachmentErrorCode, options?: ErrorOptions);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Identify attachment failures by their stable code across duplicate package installations.
|
|
29
|
+
* @param error - failure raised while validating, persisting, or reading an attachment.
|
|
30
|
+
* @returns whether the failure carries a recognized attachment error code.
|
|
31
|
+
*/
|
|
32
|
+
export declare function isAttachmentError(error: unknown): error is AttachmentError;
|
|
33
|
+
/**
|
|
34
|
+
* Distinguish caller-correctable image admission failures from storage faults.
|
|
35
|
+
* @param error - failure raised while validating or persisting an image batch.
|
|
36
|
+
* @returns whether the caller can correct the proposed image content or batch.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isImageAdmissionError(error: unknown): error is AttachmentError & {
|
|
39
|
+
readonly code: ImageAdmissionErrorCode;
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=error.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Attachment failure class. @module @qilin-agent/attachment/error */
|
|
2
|
+
const IMAGE_ADMISSION_ERROR_CODES = [
|
|
3
|
+
'TOO_MANY_IMAGES',
|
|
4
|
+
'IMAGES_TOO_LARGE',
|
|
5
|
+
'UNSUPPORTED_IMAGE_TYPE',
|
|
6
|
+
'INVALID_IMAGE_BASE64',
|
|
7
|
+
'INVALID_IMAGE',
|
|
8
|
+
'IMAGE_TYPE_MISMATCH',
|
|
9
|
+
'IMAGE_TOO_LARGE',
|
|
10
|
+
'IMAGE_TOO_MANY_PIXELS',
|
|
11
|
+
'IMAGE_DIMENSION_TOO_LARGE',
|
|
12
|
+
];
|
|
13
|
+
const ATTACHMENT_ERROR_CODES = [
|
|
14
|
+
...IMAGE_ADMISSION_ERROR_CODES,
|
|
15
|
+
'INVALID_FILE_BASE64',
|
|
16
|
+
'INVALID_ATTACHMENT_REF',
|
|
17
|
+
'ATTACHMENT_CORRUPT',
|
|
18
|
+
'ATTACHMENT_WRITE_FAILED',
|
|
19
|
+
'ATTACHMENT_NOT_FOUND',
|
|
20
|
+
'ATTACHMENT_READ_FAILED',
|
|
21
|
+
'ATTACHMENT_PROJECTION_UNSUPPORTED',
|
|
22
|
+
'ATTACHMENT_FILES_UNSUPPORTED',
|
|
23
|
+
];
|
|
24
|
+
/** Runtime membership for structurally compatible errors crossing package boundaries. */
|
|
25
|
+
const IMAGE_ADMISSION_ERROR_CODE_SET = new Set(IMAGE_ADMISSION_ERROR_CODES);
|
|
26
|
+
const ATTACHMENT_ERROR_CODE_SET = new Set(ATTACHMENT_ERROR_CODES);
|
|
27
|
+
/**
|
|
28
|
+
* Stable failures suitable for host RPC error mapping.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately re-implements the `HarnessError` shape instead of extending it:
|
|
31
|
+
* the base lives in `@qilin-agent/llm`, which itself depends on this
|
|
32
|
+
* package (`ImageBlock` references `ImageAttachmentRef`), so sharing the base
|
|
33
|
+
* would create a dependency cycle. Consumers route on `code`, never on the
|
|
34
|
+
* prototype chain, so the shapes stay interchangeable at the wire boundary.
|
|
35
|
+
*/
|
|
36
|
+
export class AttachmentError extends Error {
|
|
37
|
+
/** Stable machine-routing failure code. */
|
|
38
|
+
code;
|
|
39
|
+
/**
|
|
40
|
+
* @param message - human-readable failure description without raw bytes or host paths.
|
|
41
|
+
* @param code - stable machine-routing code.
|
|
42
|
+
* @param options - optional chained cause.
|
|
43
|
+
*/
|
|
44
|
+
constructor(message, code, options) {
|
|
45
|
+
super(message, options);
|
|
46
|
+
this.name = 'AttachmentError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Identify attachment failures by their stable code across duplicate package installations.
|
|
52
|
+
* @param error - failure raised while validating, persisting, or reading an attachment.
|
|
53
|
+
* @returns whether the failure carries a recognized attachment error code.
|
|
54
|
+
*/
|
|
55
|
+
export function isAttachmentError(error) {
|
|
56
|
+
return error instanceof Error
|
|
57
|
+
&& 'code' in error
|
|
58
|
+
&& typeof error.code === 'string'
|
|
59
|
+
&& ATTACHMENT_ERROR_CODE_SET.has(error.code);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Distinguish caller-correctable image admission failures from storage faults.
|
|
63
|
+
* @param error - failure raised while validating or persisting an image batch.
|
|
64
|
+
* @returns whether the caller can correct the proposed image content or batch.
|
|
65
|
+
*/
|
|
66
|
+
export function isImageAdmissionError(error) {
|
|
67
|
+
return error instanceof Error
|
|
68
|
+
&& 'code' in error
|
|
69
|
+
&& typeof error.code === 'string'
|
|
70
|
+
&& IMAGE_ADMISSION_ERROR_CODE_SET.has(error.code);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=error.js.map
|