@stackstackstack/dsh-attachment-local 0.1.5
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 +6 -0
- package/README.md +21 -0
- package/README.zh.md +21 -0
- package/lib/index.js +303 -0
- package/lib/invariant.js +19 -0
- package/lib/types/image.d.ts +25 -0
- package/lib/types/index.d.ts +41 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/store.d.ts +27 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/attachment/attachment-local/README.md
|
|
5
|
+
README.md: 0fe265bd50ca78e574e36f8145778d439b33f70e
|
|
6
|
+
README.zh.md: b482c6ae6b6586d24d29f3281d86d33892f658be
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @stackstackstack/dsh-attachment-local
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
The private local implementation of [`@stackstackstack/dsh-attachment`](../attachment). Objects land at `<DSH_HOME>/attachments/v1/objects/<sha256-prefix>/<sha256>` and are addressed by an opaque `sha256:` id. Each process proves a home durable once by syncing every ancestor entry to the filesystem root, so a directory another process created but has not yet synced is never mistaken for a safe boundary. Writes then use a private staging directory, owner-only files, a synced temporary file, an atomic exclusive hard-link publish, and directory syncs on the publication path (POSIX; Windows relies on filesystem metadata journaling) so the reported reference survives a crash. Write admission and reads fully decode the raster before accepting its format and dimensions; reads also re-check the digest and logged metadata. Byte and pixel limits are write-time admission policy, so a later policy reduction does not make already-admitted history unreadable.
|
|
6
|
+
|
|
7
|
+
`DSH_HOME` resolves through the shared path policy: explicit config, `$DSH_HOME`, then `~/.dsh`. Session logs contain only the reference and verified metadata, never this host path. `readImage` forwards optional cancellation into the filesystem read, observes it around verification, and preserves it instead of wrapping it as `ATTACHMENT_READ_FAILED`.
|
|
8
|
+
|
|
9
|
+
## Model Experience
|
|
10
|
+
|
|
11
|
+
Indirectly, through durable replay of historical user images and structured model image output after restart and fork.
|
|
12
|
+
|
|
13
|
+
#### KV Cache effect
|
|
14
|
+
|
|
15
|
+
None beyond the image block owned by the requesting adapter.
|
|
16
|
+
|
|
17
|
+
## Known Limitations and Deferred Work
|
|
18
|
+
|
|
19
|
+
- Objects are retained indefinitely; reference-aware garbage collection is deferred.
|
|
20
|
+
- The local backend assumes the host and provider adapter share this filesystem service.
|
|
21
|
+
- Animated GIF metadata is validated from the logical screen; frame-level decoding policy is provider-owned.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @stackstackstack/dsh-attachment-local
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
这是 [`@stackstackstack/dsh-attachment`](../attachment) 的私有本地实现。对象存放在 `<DSH_HOME>/attachments/v1/objects/<sha256-prefix>/<sha256>`,并通过不透明的 `sha256:` 标识符寻址。每个进程都会通过将每个祖先目录项逐级同步到文件系统根目录,为某个 home 一次性证明其持久性,因此绝不会把另一个进程已经创建但尚未同步的目录误认为安全边界。随后,写入过程使用私有暂存目录、仅所有者可访问的文件、经过同步的临时文件、原子且排他的硬链接发布,并对发布路径执行目录同步(适用于 POSIX;Windows 依赖文件系统元数据日志),确保已报告的引用能够在崩溃后继续存在。写入准入与读取都会完整解码光栅图片,之后才接受其格式和尺寸;读取还会重新校验摘要和已记录的元数据。字节和像素限制属于写入时的准入策略,因此后续收紧限制不会导致已经接纳的历史记录变得不可读。
|
|
6
|
+
|
|
7
|
+
`DSH_HOME` 按共享路径策略解析:显式配置、`$DSH_HOME`,最后是 `~/.dsh`。会话日志只包含引用和经过校验的元数据,绝不包含这个宿主路径。`readImage` 会把可选取消信号传入文件系统读取、在校验前后观察该信号,并保留取消语义,而不会将其包装成 `ATTACHMENT_READ_FAILED`。
|
|
8
|
+
|
|
9
|
+
## 模型体验
|
|
10
|
+
|
|
11
|
+
该包通过重启和 fork 后对历史用户图片与结构化模型图片输出的持久回放间接影响模型。
|
|
12
|
+
|
|
13
|
+
#### KV 缓存影响
|
|
14
|
+
|
|
15
|
+
除发起请求的适配器所持有的图片块外,不产生其他影响。
|
|
16
|
+
|
|
17
|
+
## 已知限制与待完成工作
|
|
18
|
+
|
|
19
|
+
- 对象会无限期保留;基于引用的垃圾回收尚未实现。
|
|
20
|
+
- 本地后端假定宿主与提供方适配器共享同一个文件系统服务。
|
|
21
|
+
- 动态 GIF 的元数据根据逻辑屏幕进行校验;逐帧解码策略由提供方持有。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { dirname, join, parse, resolve } from "node:path";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { AttachmentError, AttachmentId, AttachmentStore } from "@stackstackstack/dsh-attachment";
|
|
4
|
+
import { resolveDshHome } from "@stackstackstack/dsh-home-paths";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { chmod, link, mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
8
|
+
import sharp from "sharp";
|
|
9
|
+
//#region lib/types/image.js
|
|
10
|
+
/** Raster inspection: full decode at admission, header-only probe on verified reads. */
|
|
11
|
+
const MEDIA_TYPES = {
|
|
12
|
+
png: "image/png",
|
|
13
|
+
jpeg: "image/jpeg",
|
|
14
|
+
webp: "image/webp",
|
|
15
|
+
gif: "image/gif"
|
|
16
|
+
};
|
|
17
|
+
async function imageMetadata(image) {
|
|
18
|
+
const metadata = await image.metadata();
|
|
19
|
+
const mediaType = MEDIA_TYPES[metadata.format];
|
|
20
|
+
if (mediaType === void 0) throw new AttachmentError("Unsupported or malformed image data.", "INVALID_IMAGE");
|
|
21
|
+
return {
|
|
22
|
+
mediaType,
|
|
23
|
+
width: metadata.width,
|
|
24
|
+
height: metadata.height
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a supported raster's header and return its intrinsic metadata without
|
|
29
|
+
* decoding pixels. Digest-verified reads use this: admission already proved
|
|
30
|
+
* that these exact bytes decode completely, so the read path only re-derives
|
|
31
|
+
* the reference fields instead of paying the full-raster decode again.
|
|
32
|
+
* @param data - complete encoded image bytes.
|
|
33
|
+
* @returns verified format and dimensions.
|
|
34
|
+
*/
|
|
35
|
+
async function probeImage(data) {
|
|
36
|
+
try {
|
|
37
|
+
return await imageMetadata(sharp(data, {
|
|
38
|
+
failOn: "error",
|
|
39
|
+
limitInputPixels: false
|
|
40
|
+
}));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof AttachmentError) throw error;
|
|
43
|
+
throw new AttachmentError("Unsupported or malformed image data.", "INVALID_IMAGE", { cause: error });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Fully decode a supported raster and return its intrinsic metadata.
|
|
48
|
+
* @param data - complete encoded image bytes.
|
|
49
|
+
* @param maxPixels - decoded-pixel admission limit.
|
|
50
|
+
* @returns verified format and dimensions.
|
|
51
|
+
*/
|
|
52
|
+
async function detectImage(data, maxPixels) {
|
|
53
|
+
try {
|
|
54
|
+
const image = sharp(data, {
|
|
55
|
+
failOn: "error",
|
|
56
|
+
limitInputPixels: false
|
|
57
|
+
});
|
|
58
|
+
const detected = await imageMetadata(image);
|
|
59
|
+
if (maxPixels !== void 0 && detected.width * detected.height > maxPixels) throw new AttachmentError("Image exceeds the configured decoded-pixel limit.", "IMAGE_TOO_MANY_PIXELS");
|
|
60
|
+
await image.raw().toBuffer();
|
|
61
|
+
return detected;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof AttachmentError) throw error;
|
|
64
|
+
throw new AttachmentError("Unsupported or malformed image data.", "INVALID_IMAGE", { cause: error });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region lib/types/store.js
|
|
69
|
+
/** Content-addressed, owner-private local attachment storage. */
|
|
70
|
+
const ID_PATTERN = /^sha256:([a-f0-9]{64})$/;
|
|
71
|
+
const durableHomes = /* @__PURE__ */ new Set();
|
|
72
|
+
function digest(data) {
|
|
73
|
+
return createHash("sha256").update(data).digest("hex");
|
|
74
|
+
}
|
|
75
|
+
function displayName(value) {
|
|
76
|
+
if (value === void 0) return void 0;
|
|
77
|
+
const clean = value.slice(Math.max(value.lastIndexOf("/"), value.lastIndexOf("\\")) + 1).replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 255);
|
|
78
|
+
return clean === "" ? void 0 : clean;
|
|
79
|
+
}
|
|
80
|
+
function objectPath(root, sha256) {
|
|
81
|
+
return join(root, "objects", sha256.slice(0, 2), sha256);
|
|
82
|
+
}
|
|
83
|
+
function ensureReference(ref) {
|
|
84
|
+
const match = ID_PATTERN.exec(String(ref.attachmentId));
|
|
85
|
+
if (match?.[1] === void 0) throw new AttachmentError("Attachment reference is invalid.", "INVALID_ATTACHMENT_REF");
|
|
86
|
+
return match[1];
|
|
87
|
+
}
|
|
88
|
+
async function inspectMetadata(data, declaredMediaType, maxPixels) {
|
|
89
|
+
if (data.byteLength === 0) throw new AttachmentError("Image is empty.", "INVALID_IMAGE");
|
|
90
|
+
const detected = await detectImage(data, maxPixels);
|
|
91
|
+
if (detected.mediaType !== declaredMediaType) throw new AttachmentError("Declared image type does not match its bytes.", "IMAGE_TYPE_MISMATCH");
|
|
92
|
+
return {
|
|
93
|
+
...detected,
|
|
94
|
+
bytes: data.byteLength
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Run the full admission policy for one image without touching storage.
|
|
99
|
+
* @param input - encoded bytes and declared metadata.
|
|
100
|
+
* @param limits - resolved storage policy.
|
|
101
|
+
* @returns completion after the encoded raster has been fully decoded.
|
|
102
|
+
*/
|
|
103
|
+
async function validateImageFile(input, limits) {
|
|
104
|
+
if (input.data.byteLength > limits.maxImageBytes) throw new AttachmentError("Image exceeds the configured byte limit.", "IMAGE_TOO_LARGE");
|
|
105
|
+
await inspectMetadata(input.data, input.mediaType, limits.maxImagePixels);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Make a directory's entries durable (fsync on a read-only directory handle).
|
|
109
|
+
* A synced file alone does not survive a crash when its directory entry never
|
|
110
|
+
* reached storage, so the publication directory is synced before a durable
|
|
111
|
+
* reference is reported.
|
|
112
|
+
*/
|
|
113
|
+
async function syncDirectory(path) {
|
|
114
|
+
/* v8 ignore next -- Windows cannot open directory handles; NTFS metadata journaling owns entry durability there. */
|
|
115
|
+
if (process.platform === "win32") return;
|
|
116
|
+
/* v8 ignore start -- Windows cannot exercise directory fsync; POSIX behavior tests enforce this peer. */
|
|
117
|
+
const handle = await open(path, constants.O_RDONLY);
|
|
118
|
+
try {
|
|
119
|
+
await handle.sync();
|
|
120
|
+
} finally {
|
|
121
|
+
await handle.close();
|
|
122
|
+
}
|
|
123
|
+
/* v8 ignore stop */
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Create one private directory tree and persist every ancestor entry up to a
|
|
127
|
+
* caller-vouched durable boundary. The walk deliberately ignores what mkdir
|
|
128
|
+
* reports as newly created: a concurrent first save can create a level this
|
|
129
|
+
* process then merely observes, so "already existed" is not "already durable"
|
|
130
|
+
* — the entry may still be unsynced in the creator, and a crash would drop a
|
|
131
|
+
* directory the session checkpoint already references. Re-syncing a durable
|
|
132
|
+
* entry is harmless; skipping an unsynced one is not.
|
|
133
|
+
* @param path - absolute directory to create.
|
|
134
|
+
* @param boundary - absolute ancestor the caller vouches is already durable.
|
|
135
|
+
*/
|
|
136
|
+
async function ensureDurableDirectory(path, boundary) {
|
|
137
|
+
const target = resolve(path);
|
|
138
|
+
const stop = resolve(boundary);
|
|
139
|
+
await mkdir(target, {
|
|
140
|
+
recursive: true,
|
|
141
|
+
mode: 448
|
|
142
|
+
});
|
|
143
|
+
await chmod(target, 448);
|
|
144
|
+
let level = target;
|
|
145
|
+
while (level !== stop) {
|
|
146
|
+
const parent = dirname(level);
|
|
147
|
+
await syncDirectory(parent);
|
|
148
|
+
/* v8 ignore next -- filesystem-root guard: callers pass a boundary that is an ancestor of path, so the walk reaches it first. */
|
|
149
|
+
if (parent === level) return;
|
|
150
|
+
level = parent;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Establish this process's proof that one DSH_HOME entry and every ancestor
|
|
155
|
+
* below the filesystem root are durable. Mere existence is insufficient: a
|
|
156
|
+
* concurrent process may have created the directory but not synced its parent.
|
|
157
|
+
*/
|
|
158
|
+
async function ensureDurableHome(path) {
|
|
159
|
+
const home = resolve(path);
|
|
160
|
+
if (!durableHomes.has(home)) {
|
|
161
|
+
await ensureDurableDirectory(home, parse(home).root);
|
|
162
|
+
durableHomes.add(home);
|
|
163
|
+
}
|
|
164
|
+
return home;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Save and verify immutable image bytes below a versioned attachment root.
|
|
168
|
+
* @param root - absolute `DSH_HOME/attachments/v1` root.
|
|
169
|
+
* @param input - encoded bytes and declared metadata.
|
|
170
|
+
* @param limits - resolved storage policy.
|
|
171
|
+
* @returns durable content-addressed reference.
|
|
172
|
+
*/
|
|
173
|
+
async function saveImageFile(root, input, limits) {
|
|
174
|
+
if (input.data.byteLength > limits.maxImageBytes) throw new AttachmentError("Image exceeds the configured byte limit.", "IMAGE_TOO_LARGE");
|
|
175
|
+
const metadata = await inspectMetadata(input.data, input.mediaType, limits.maxImagePixels);
|
|
176
|
+
const sha256 = digest(input.data);
|
|
177
|
+
const bucket = join(root, "objects", sha256.slice(0, 2));
|
|
178
|
+
const staging = join(root, "tmp");
|
|
179
|
+
const boundary = await ensureDurableHome(dirname(dirname(resolve(root))));
|
|
180
|
+
await ensureDurableDirectory(bucket, boundary);
|
|
181
|
+
await ensureDurableDirectory(staging, boundary);
|
|
182
|
+
const temporary = join(staging, randomUUID());
|
|
183
|
+
const target = objectPath(root, sha256);
|
|
184
|
+
let handle;
|
|
185
|
+
try {
|
|
186
|
+
handle = await open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
|
|
187
|
+
await handle.writeFile(input.data);
|
|
188
|
+
await handle.sync();
|
|
189
|
+
await handle.close();
|
|
190
|
+
handle = void 0;
|
|
191
|
+
try {
|
|
192
|
+
await link(temporary, target);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
/* v8 ignore next -- Private same-filesystem directories make EEXIST the only recoverable link race. */
|
|
195
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
196
|
+
if (digest(new Uint8Array(await readFile(target))) !== sha256) throw new AttachmentError("Stored attachment failed integrity verification.", "ATTACHMENT_CORRUPT");
|
|
197
|
+
}
|
|
198
|
+
await syncDirectory(bucket);
|
|
199
|
+
await syncDirectory(join(root, "objects"));
|
|
200
|
+
await unlink(temporary);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
/* v8 ignore next -- A descriptor can remain open only when the underlying write/sync/close operation fails. */
|
|
203
|
+
if (handle !== void 0) await handle.close().catch(
|
|
204
|
+
/* v8 ignore next -- Close failure is superseded by the storage operation that entered cleanup. */
|
|
205
|
+
() => {}
|
|
206
|
+
);
|
|
207
|
+
await unlink(temporary).catch(
|
|
208
|
+
/* v8 ignore next -- The callback requires a second independent staging-unlink failure. */
|
|
209
|
+
(cleanupError) => {
|
|
210
|
+
/* v8 ignore next -- Cleanup is best-effort only for a staging file already removed by a failed operation. */
|
|
211
|
+
if (!(cleanupError instanceof Error && "code" in cleanupError && cleanupError.code === "ENOENT")) throw cleanupError;
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
if (error instanceof AttachmentError) throw error;
|
|
215
|
+
throw new AttachmentError("Unable to persist image attachment.", "ATTACHMENT_WRITE_FAILED", { cause: error });
|
|
216
|
+
}
|
|
217
|
+
const name = displayName(input.name);
|
|
218
|
+
return {
|
|
219
|
+
attachmentId: AttachmentId(`sha256:${sha256}`),
|
|
220
|
+
...metadata,
|
|
221
|
+
...name !== void 0 ? { name } : {}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Read and verify one content-addressed image.
|
|
226
|
+
* @param root - absolute `DSH_HOME/attachments/v1` root.
|
|
227
|
+
* @param ref - reference recorded in the session log.
|
|
228
|
+
* @param signal - optional cancellation for filesystem and verification work.
|
|
229
|
+
* @returns verified bytes and reference.
|
|
230
|
+
* @throws the signal reason when aborted, or an AttachmentError when verification fails.
|
|
231
|
+
*/
|
|
232
|
+
async function readImageFile(root, ref, signal) {
|
|
233
|
+
signal?.throwIfAborted();
|
|
234
|
+
const sha256 = ensureReference(ref);
|
|
235
|
+
let data;
|
|
236
|
+
try {
|
|
237
|
+
data = new Uint8Array(await readFile(objectPath(root, sha256), { signal }));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
signal?.throwIfAborted();
|
|
240
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") throw new AttachmentError("Attachment object is missing.", "ATTACHMENT_NOT_FOUND");
|
|
241
|
+
throw new AttachmentError("Unable to read image attachment.", "ATTACHMENT_READ_FAILED", { cause: error });
|
|
242
|
+
}
|
|
243
|
+
signal?.throwIfAborted();
|
|
244
|
+
if (digest(data) !== sha256) throw new AttachmentError("Stored attachment failed integrity verification.", "ATTACHMENT_CORRUPT");
|
|
245
|
+
const metadata = await probeImage(data);
|
|
246
|
+
signal?.throwIfAborted();
|
|
247
|
+
if (metadata.mediaType !== ref.mediaType || data.byteLength !== ref.bytes || metadata.width !== ref.width || metadata.height !== ref.height) throw new AttachmentError("Stored attachment metadata does not match its reference.", "ATTACHMENT_CORRUPT");
|
|
248
|
+
return {
|
|
249
|
+
ref,
|
|
250
|
+
data
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region lib/types/index.js
|
|
255
|
+
/** Local durable attachment backend rooted below `DSH_HOME`. @module @stackstackstack/dsh-attachment-local */
|
|
256
|
+
/** Default maximum encoded bytes for one image. */
|
|
257
|
+
const DEFAULT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
258
|
+
/** Default maximum images in one prompt. */
|
|
259
|
+
const DEFAULT_MAX_IMAGES_PER_MESSAGE = 20;
|
|
260
|
+
/** Default maximum aggregate image bytes in one prompt. */
|
|
261
|
+
const DEFAULT_MAX_MESSAGE_IMAGE_BYTES = 100 * 1024 * 1024;
|
|
262
|
+
/** Default maximum intrinsic pixels for one image. */
|
|
263
|
+
const DEFAULT_MAX_IMAGE_PIXELS = 4e7;
|
|
264
|
+
/** Persistent content-addressed local attachment store. */
|
|
265
|
+
var LocalAttachmentStore = class extends AttachmentStore {
|
|
266
|
+
static Config = z.object({
|
|
267
|
+
dshHome: z.string(),
|
|
268
|
+
maxImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGE_BYTES),
|
|
269
|
+
maxImagesPerMessage: z.number().step(1).min(1).default(20),
|
|
270
|
+
maxMessageImageBytes: z.number().step(1).min(1).default(DEFAULT_MAX_MESSAGE_IMAGE_BYTES),
|
|
271
|
+
maxImagePixels: z.number().step(1).min(1).default(DEFAULT_MAX_IMAGE_PIXELS)
|
|
272
|
+
});
|
|
273
|
+
/** Absolute versioned storage root. */
|
|
274
|
+
root;
|
|
275
|
+
imageLimits;
|
|
276
|
+
constructor(ctx, config) {
|
|
277
|
+
super(ctx);
|
|
278
|
+
this.root = resolve(join(resolveDshHome(config.dshHome), "attachments", "v1"));
|
|
279
|
+
this.imageLimits = Object.freeze({
|
|
280
|
+
maxImageBytes: config.maxImageBytes ?? 5242880,
|
|
281
|
+
maxImagesPerMessage: config.maxImagesPerMessage ?? 20,
|
|
282
|
+
maxMessageImageBytes: config.maxMessageImageBytes ?? 104857600,
|
|
283
|
+
maxImagePixels: config.maxImagePixels ?? 4e7,
|
|
284
|
+
mediaTypes: Object.freeze([
|
|
285
|
+
"image/png",
|
|
286
|
+
"image/jpeg",
|
|
287
|
+
"image/webp",
|
|
288
|
+
"image/gif"
|
|
289
|
+
])
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async validateImage(input) {
|
|
293
|
+
await validateImageFile(input, this.imageLimits);
|
|
294
|
+
}
|
|
295
|
+
async saveImage(input) {
|
|
296
|
+
return saveImageFile(this.root, input, this.imageLimits);
|
|
297
|
+
}
|
|
298
|
+
async readImage(ref, signal) {
|
|
299
|
+
return readImageFile(this.root, ref, signal);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
//#endregion
|
|
303
|
+
export { DEFAULT_MAX_IMAGES_PER_MESSAGE, DEFAULT_MAX_IMAGE_BYTES, DEFAULT_MAX_IMAGE_PIXELS, DEFAULT_MAX_MESSAGE_IMAGE_BYTES, LocalAttachmentStore, LocalAttachmentStore as default, detectImage, readImageFile, saveImageFile, validateImageFile };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned invariant companion for `@stackstackstack/dsh-attachment-local`.
|
|
3
|
+
* @module @stackstackstack/dsh-attachment-local/invariant
|
|
4
|
+
*/
|
|
5
|
+
const PACKAGE_NAME = "@stackstackstack/dsh-attachment-local";
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
const name = "attachment-local-invariant";
|
|
8
|
+
/** Services required before package ownership can be reserved. */
|
|
9
|
+
const inject = ["invariants", "attachments"];
|
|
10
|
+
/** No runtime invariant: immutable writes and verified reads are enforced directly at the backend boundary. */
|
|
11
|
+
const install = () => {};
|
|
12
|
+
/**
|
|
13
|
+
* Register the package invariant companion.
|
|
14
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
15
|
+
* @returns the registration disposer.
|
|
16
|
+
*/
|
|
17
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
18
|
+
//#endregion
|
|
19
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Raster inspection: full decode at admission, header-only probe on verified reads. */
|
|
2
|
+
import type { ImageMediaType } from '@stackstackstack/dsh-attachment';
|
|
3
|
+
/** Decoded metadata from a supported image. */
|
|
4
|
+
export interface DetectedImage {
|
|
5
|
+
mediaType: ImageMediaType;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parse a supported raster's header and return its intrinsic metadata without
|
|
11
|
+
* decoding pixels. Digest-verified reads use this: admission already proved
|
|
12
|
+
* that these exact bytes decode completely, so the read path only re-derives
|
|
13
|
+
* the reference fields instead of paying the full-raster decode again.
|
|
14
|
+
* @param data - complete encoded image bytes.
|
|
15
|
+
* @returns verified format and dimensions.
|
|
16
|
+
*/
|
|
17
|
+
export declare function probeImage(data: Uint8Array): Promise<DetectedImage>;
|
|
18
|
+
/**
|
|
19
|
+
* Fully decode a supported raster and return its intrinsic metadata.
|
|
20
|
+
* @param data - complete encoded image bytes.
|
|
21
|
+
* @param maxPixels - decoded-pixel admission limit.
|
|
22
|
+
* @returns verified format and dimensions.
|
|
23
|
+
*/
|
|
24
|
+
export declare function detectImage(data: Uint8Array, maxPixels?: number): Promise<DetectedImage>;
|
|
25
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Local durable attachment backend rooted below `DSH_HOME`. @module @stackstackstack/dsh-attachment-local */
|
|
2
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
import z from '@deepseek-ai/schemastery';
|
|
4
|
+
import { AttachmentStore } from '@stackstackstack/dsh-attachment';
|
|
5
|
+
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@stackstackstack/dsh-attachment';
|
|
6
|
+
export { detectImage } from './image.ts';
|
|
7
|
+
export { readImageFile, saveImageFile, validateImageFile } from './store.ts';
|
|
8
|
+
/** Default maximum encoded bytes for one image. */
|
|
9
|
+
export declare const DEFAULT_MAX_IMAGE_BYTES: number;
|
|
10
|
+
/** Default maximum images in one prompt. */
|
|
11
|
+
export declare const DEFAULT_MAX_IMAGES_PER_MESSAGE = 20;
|
|
12
|
+
/** Default maximum aggregate image bytes in one prompt. */
|
|
13
|
+
export declare const DEFAULT_MAX_MESSAGE_IMAGE_BYTES: number;
|
|
14
|
+
/** Default maximum intrinsic pixels for one image. */
|
|
15
|
+
export declare const DEFAULT_MAX_IMAGE_PIXELS = 40000000;
|
|
16
|
+
/** Local attachment backend configuration. */
|
|
17
|
+
export interface Config {
|
|
18
|
+
/** Explicit harness home; omitted follows `DSH_HOME`, then `~/.dsh`. */
|
|
19
|
+
dshHome?: string;
|
|
20
|
+
/** Maximum encoded bytes accepted for one image. */
|
|
21
|
+
maxImageBytes?: number;
|
|
22
|
+
/** Maximum image count accepted in one submitted message. */
|
|
23
|
+
maxImagesPerMessage?: number;
|
|
24
|
+
/** Maximum aggregate encoded image bytes accepted in one submitted message. */
|
|
25
|
+
maxMessageImageBytes?: number;
|
|
26
|
+
/** Maximum intrinsic width multiplied by height accepted for one image. */
|
|
27
|
+
maxImagePixels?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Persistent content-addressed local attachment store. */
|
|
30
|
+
export declare class LocalAttachmentStore extends AttachmentStore {
|
|
31
|
+
static Config: z<Config>;
|
|
32
|
+
/** Absolute versioned storage root. */
|
|
33
|
+
readonly root: string;
|
|
34
|
+
readonly imageLimits: ImageAttachmentLimits;
|
|
35
|
+
constructor(ctx: Context, config: Config);
|
|
36
|
+
validateImage(input: SaveImageAttachment): Promise<void>;
|
|
37
|
+
saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>;
|
|
38
|
+
readImage(ref: ImageAttachmentRef, signal?: AbortSignal): Promise<StoredImageAttachment>;
|
|
39
|
+
}
|
|
40
|
+
export default LocalAttachmentStore;
|
|
41
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Package-owned invariant companion for `@stackstackstack/dsh-attachment-local`.
|
|
2
|
+
* @module @stackstackstack/dsh-attachment-local/invariant
|
|
3
|
+
*/
|
|
4
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
5
|
+
/** Cordis companion plugin name. */
|
|
6
|
+
export declare const name = "attachment-local-invariant";
|
|
7
|
+
/** Services required before package ownership can be reserved. */
|
|
8
|
+
export declare const inject: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Register the package invariant companion.
|
|
11
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
12
|
+
* @returns the registration disposer.
|
|
13
|
+
*/
|
|
14
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
15
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Content-addressed, owner-private local attachment storage. */
|
|
2
|
+
import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from '@stackstackstack/dsh-attachment';
|
|
3
|
+
/**
|
|
4
|
+
* Run the full admission policy for one image without touching storage.
|
|
5
|
+
* @param input - encoded bytes and declared metadata.
|
|
6
|
+
* @param limits - resolved storage policy.
|
|
7
|
+
* @returns completion after the encoded raster has been fully decoded.
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateImageFile(input: SaveImageAttachment, limits: ImageAttachmentLimits): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Save and verify immutable image bytes below a versioned attachment root.
|
|
12
|
+
* @param root - absolute `DSH_HOME/attachments/v1` root.
|
|
13
|
+
* @param input - encoded bytes and declared metadata.
|
|
14
|
+
* @param limits - resolved storage policy.
|
|
15
|
+
* @returns durable content-addressed reference.
|
|
16
|
+
*/
|
|
17
|
+
export declare function saveImageFile(root: string, input: SaveImageAttachment, limits: ImageAttachmentLimits): Promise<ImageAttachmentRef>;
|
|
18
|
+
/**
|
|
19
|
+
* Read and verify one content-addressed image.
|
|
20
|
+
* @param root - absolute `DSH_HOME/attachments/v1` root.
|
|
21
|
+
* @param ref - reference recorded in the session log.
|
|
22
|
+
* @param signal - optional cancellation for filesystem and verification work.
|
|
23
|
+
* @returns verified bytes and reference.
|
|
24
|
+
* @throws the signal reason when aborted, or an AttachmentError when verification fails.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readImageFile(root: string, ref: ImageAttachmentRef, signal?: AbortSignal): Promise<StoredImageAttachment>;
|
|
27
|
+
//# sourceMappingURL=store.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stackstackstack/dsh-attachment-local",
|
|
3
|
+
"description": "Private content-addressed DSH_HOME attachment storage",
|
|
4
|
+
"version": "0.1.5",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/attachment/attachment-local"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@stackstackstack/dsh-attachment": "^0.1.5",
|
|
36
|
+
"@stackstackstack/dsh-invariants": "^0.1.5",
|
|
37
|
+
"@stackstackstack/dsh-home-paths": "^0.1.5",
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"sharp": "^0.35.3",
|
|
42
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@stackstackstack/dsh-attachment": "^0.1.5",
|
|
46
|
+
"@stackstackstack/dsh-invariants": "^0.1.5",
|
|
47
|
+
"@stackstackstack/dsh-home-paths": "^0.1.5",
|
|
48
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
49
|
+
}
|
|
50
|
+
}
|