@cjhyy/code-shell-server 0.9.9 → 0.9.11

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.
Files changed (35) hide show
  1. package/dist/index.panels.d.ts +8 -0
  2. package/dist/index.panels.js +8 -0
  3. package/dist/panels/bridge-contract.d.ts +72 -0
  4. package/dist/panels/bridge-contract.js +92 -0
  5. package/dist/panels/connections.d.ts +31 -0
  6. package/dist/panels/connections.js +118 -0
  7. package/dist/panels/process-entry.d.ts +2 -0
  8. package/dist/panels/process-entry.js +52 -0
  9. package/dist/panels/process-service.d.ts +61 -0
  10. package/dist/panels/process-service.js +367 -33
  11. package/dist/panels/process-state.d.ts +58 -0
  12. package/dist/panels/process-state.js +87 -0
  13. package/dist/panels/resources/directories.d.ts +12 -0
  14. package/dist/panels/resources/directories.js +89 -0
  15. package/dist/panels/resources/library.d.ts +63 -0
  16. package/dist/panels/resources/library.js +423 -0
  17. package/dist/panels/resources/service.d.ts +45 -0
  18. package/dist/panels/resources/service.js +317 -0
  19. package/dist/panels/resources/storage.d.ts +10 -0
  20. package/dist/panels/resources/storage.js +102 -0
  21. package/dist/panels/resources/types.d.ts +16 -0
  22. package/dist/panels/resources/types.js +1 -0
  23. package/dist/panels/resources/uploads.d.ts +64 -0
  24. package/dist/panels/resources/uploads.js +527 -0
  25. package/dist/panels/runtime.d.ts +1 -1
  26. package/dist/panels/runtime.js +137 -15
  27. package/dist/panels/tool-executor.d.ts +58 -0
  28. package/dist/panels/tool-executor.js +421 -0
  29. package/dist/panels/tool-jobs-storage.d.ts +16 -0
  30. package/dist/panels/tool-jobs-storage.js +169 -0
  31. package/dist/panels/tool-jobs.d.ts +103 -0
  32. package/dist/panels/tool-jobs.js +530 -0
  33. package/dist/sessions-service.d.ts +2 -0
  34. package/dist/sessions-service.js +8 -0
  35. package/package.json +5 -5
@@ -0,0 +1,45 @@
1
+ import { ResourceLibrary } from "./library.js";
2
+ import type { ResourceScope } from "./types.js";
3
+ export interface PanelResourceServiceOptions {
4
+ rootDirectory: string;
5
+ isScopeAuthorized(scope: ResourceScope): boolean | Promise<boolean>;
6
+ maxFileBytes?: number;
7
+ }
8
+ export interface PanelResourceCallContext {
9
+ /** Resolve a live, owner-authorized process directory grant; never accept a guest path. */
10
+ resolveDirectory?: (handle: string) => string | Promise<string>;
11
+ signal?: AbortSignal;
12
+ }
13
+ export declare class PanelResourceError extends Error {
14
+ readonly code: "INVALID_RESOURCE_REQUEST" | "RESOURCE_ACCESS_DENIED" | "RESOURCE_IO_FAILED";
15
+ constructor(code: "INVALID_RESOURCE_REQUEST" | "RESOURCE_ACCESS_DENIED" | "RESOURCE_IO_FAILED", message: string, options?: ErrorOptions);
16
+ }
17
+ /** Generic resource custody and byte transport; it never interprets a project or invokes a model. */
18
+ export declare class PanelResourceService {
19
+ private readonly options;
20
+ readonly library: ResourceLibrary;
21
+ private readonly uploads;
22
+ private readonly directoryIdentities;
23
+ private closed;
24
+ constructor(options: PanelResourceServiceOptions);
25
+ capabilities(): {
26
+ available: boolean;
27
+ maxChunkBytes: number;
28
+ maxFileBytes: number;
29
+ materialize: boolean;
30
+ capture: boolean;
31
+ resumableUploads: boolean;
32
+ };
33
+ initialize(): Promise<void>;
34
+ private authorize;
35
+ private directory;
36
+ private materialize;
37
+ private capture;
38
+ private read;
39
+ dispatch(scope: ResourceScope, method: string, raw?: unknown, context?: PanelResourceCallContext): Promise<any>;
40
+ /** Trusted lifecycle hook; call after a tool owner releases its directory grant. */
41
+ releaseDirectory(scope: ResourceScope, handle: string): void;
42
+ cancelScope(scope: ResourceScope): Promise<void>;
43
+ cancelApp(appId: string): Promise<void>;
44
+ shutdown(): Promise<void>;
45
+ }
@@ -0,0 +1,317 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { link, lstat, open, rm } from "node:fs/promises";
4
+ import { ResourceLibrary, mediaSourceIdentity } from "./library.js";
5
+ import { mediaScopeKey, normalizeMediaScope } from "./storage.js";
6
+ import { openResourceDirectory, resourceRelativePath, sameResourceIdentity, } from "./directories.js";
7
+ import { ResourceUploadIngest } from "./uploads.js";
8
+ const CHUNK_BYTES = 32768;
9
+ export class PanelResourceError extends Error {
10
+ code;
11
+ constructor(code, message, options) {
12
+ super(message, options);
13
+ this.code = code;
14
+ this.name = "PanelResourceError";
15
+ }
16
+ }
17
+ function object(value, keys) {
18
+ if (!value ||
19
+ typeof value !== "object" ||
20
+ Array.isArray(value) ||
21
+ Object.keys(value).some((key) => !keys.includes(key)))
22
+ throw new PanelResourceError("INVALID_RESOURCE_REQUEST", "Invalid resource request fields");
23
+ return value;
24
+ }
25
+ function integer(value, min, max) {
26
+ if (!Number.isSafeInteger(value) || Number(value) < min || Number(value) > max)
27
+ throw new PanelResourceError("INVALID_RESOURCE_REQUEST", "Invalid resource byte range or limit");
28
+ return Number(value);
29
+ }
30
+ function assetId(value) {
31
+ if (typeof value !== "string" || !/^asset-[a-f0-9]{64}$/.test(value))
32
+ throw new PanelResourceError("INVALID_RESOURCE_REQUEST", "Invalid resource ID");
33
+ return value;
34
+ }
35
+ /** Generic resource custody and byte transport; it never interprets a project or invokes a model. */
36
+ export class PanelResourceService {
37
+ options;
38
+ library;
39
+ uploads;
40
+ directoryIdentities = new Map();
41
+ closed = false;
42
+ constructor(options) {
43
+ this.options = options;
44
+ this.library = new ResourceLibrary(options);
45
+ this.uploads = new ResourceUploadIngest({ ...options, library: this.library });
46
+ }
47
+ capabilities() {
48
+ return {
49
+ available: true,
50
+ maxChunkBytes: CHUNK_BYTES,
51
+ maxFileBytes: this.options.maxFileBytes ?? 20 * 1024 ** 3,
52
+ materialize: true,
53
+ capture: true,
54
+ resumableUploads: true,
55
+ };
56
+ }
57
+ initialize() {
58
+ return this.uploads.initialize();
59
+ }
60
+ async authorize(scope, context = {}) {
61
+ normalizeMediaScope(scope);
62
+ context.signal?.throwIfAborted();
63
+ if (this.closed || !(await this.options.isScopeAuthorized(scope)))
64
+ throw new PanelResourceError("RESOURCE_ACCESS_DENIED", "Resource access is no longer authorized");
65
+ context.signal?.throwIfAborted();
66
+ }
67
+ async directory(scope, input, context, create) {
68
+ if (typeof input.directoryHandle !== "string" ||
69
+ !input.directoryHandle ||
70
+ input.directoryHandle.length > 256 ||
71
+ !context.resolveDirectory)
72
+ throw new PanelResourceError("INVALID_RESOURCE_REQUEST", "An authorized tool directory is required");
73
+ const path = resourceRelativePath(input.path);
74
+ const root = await context.resolveDirectory(input.directoryHandle);
75
+ const verify = async () => {
76
+ await this.authorize(scope, context);
77
+ if ((await context.resolveDirectory(input.directoryHandle)) !== root)
78
+ throw new PanelResourceError("RESOURCE_ACCESS_DENIED", "Tool directory authorization changed");
79
+ };
80
+ const held = await openResourceDirectory(root, path, create, verify);
81
+ const key = `${mediaScopeKey(scope)}:${input.directoryHandle}`;
82
+ const previous = this.directoryIdentities.get(key);
83
+ if (previous && !sameResourceIdentity(previous, held.rootIdentity)) {
84
+ await held.close();
85
+ throw new PanelResourceError("RESOURCE_ACCESS_DENIED", "Tool directory has been replaced");
86
+ }
87
+ // Grants are bounded by the Host; retain a bounded extra identity cache as defence in depth.
88
+ if (this.directoryIdentities.size >= 1024 && !previous) {
89
+ await held.close();
90
+ throw new Error("Too many resource directory grants; reopen the panel");
91
+ }
92
+ this.directoryIdentities.set(key, { dev: held.rootIdentity.dev, ino: held.rootIdentity.ino });
93
+ return held;
94
+ }
95
+ async materialize(scope, raw, context) {
96
+ const input = object(raw, ["assetId", "directoryHandle", "path"]);
97
+ const asset = await this.library.get(scope, assetId(input.assetId));
98
+ const directory = await this.directory(scope, input, context, true);
99
+ const temporary = directory.location(`.${randomUUID()}.resource-partial`);
100
+ let destination;
101
+ let published = false;
102
+ let identity;
103
+ try {
104
+ await directory.verify();
105
+ destination = await open(temporary, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600);
106
+ identity = await destination.stat();
107
+ const source = await this.library.openRead(scope, asset.id);
108
+ const hash = createHash("sha256");
109
+ let bytes = 0;
110
+ try {
111
+ for await (const chunk of source.body) {
112
+ await directory.verify();
113
+ if (bytes + chunk.length > asset.bytes)
114
+ throw new Error("Resource content changed");
115
+ hash.update(chunk);
116
+ let offset = 0;
117
+ while (offset < chunk.length) {
118
+ const written = await destination.write(chunk, offset, chunk.length - offset, bytes + offset);
119
+ if (!written.bytesWritten)
120
+ throw new Error("Resource copy stopped making progress");
121
+ offset += written.bytesWritten;
122
+ }
123
+ bytes += chunk.length;
124
+ }
125
+ }
126
+ finally {
127
+ source.body?.destroy();
128
+ }
129
+ if (bytes !== asset.bytes || hash.digest("hex") !== asset.sha256)
130
+ throw new Error("Resource content failed integrity validation");
131
+ await destination.sync();
132
+ // A tool may share the granted directory. Verify the actual destination,
133
+ // not only the source stream, before publishing the materialized file.
134
+ const completed = await destination.stat();
135
+ const copiedHash = createHash("sha256");
136
+ const buffer = Buffer.allocUnsafe(256 * 1024);
137
+ for (let offset = 0; offset < asset.bytes;) {
138
+ await directory.verify();
139
+ const read = await destination.read(buffer, 0, Math.min(buffer.length, asset.bytes - offset), offset);
140
+ if (!read.bytesRead)
141
+ throw new Error("Resource copy was truncated");
142
+ copiedHash.update(buffer.subarray(0, read.bytesRead));
143
+ offset += read.bytesRead;
144
+ }
145
+ const verified = await destination.stat();
146
+ if (verified.size !== asset.bytes ||
147
+ verified.mtimeMs !== completed.mtimeMs ||
148
+ verified.ctimeMs !== completed.ctimeMs ||
149
+ copiedHash.digest("hex") !== asset.sha256)
150
+ throw new Error("Materialized resource failed integrity verification");
151
+ await destination.close();
152
+ destination = undefined;
153
+ await directory.verify();
154
+ // Hard-link publication is atomic and refuses to overwrite any existing tool file.
155
+ await link(temporary, directory.path);
156
+ published = true;
157
+ await directory.verify();
158
+ const current = await lstat(directory.path);
159
+ if (!current.isFile() || current.isSymbolicLink() || !sameResourceIdentity(current, identity))
160
+ throw new Error("Resource output changed during publication");
161
+ return { assetId: asset.id, path: input.path, bytes: asset.bytes, sha256: asset.sha256 };
162
+ }
163
+ catch (error) {
164
+ if (published && identity) {
165
+ const current = await lstat(directory.path).catch(() => undefined);
166
+ if (current && sameResourceIdentity(current, identity))
167
+ await rm(directory.path, { force: true }).catch(() => { });
168
+ }
169
+ throw error;
170
+ }
171
+ finally {
172
+ await destination?.close().catch(() => { });
173
+ const current = await lstat(temporary).catch(() => undefined);
174
+ if (current && identity && sameResourceIdentity(current, identity))
175
+ await rm(temporary, { force: true }).catch(() => { });
176
+ await directory.close();
177
+ }
178
+ }
179
+ async capture(scope, raw, context) {
180
+ const input = object(raw, [
181
+ "directoryHandle",
182
+ "path",
183
+ "name",
184
+ "mimeType",
185
+ "expectedBytes",
186
+ "expectedSha256",
187
+ ]);
188
+ if (input.name !== undefined &&
189
+ (typeof input.name !== "string" || !input.name || input.name.length > 240))
190
+ throw new Error("Invalid resource name");
191
+ if (input.mimeType !== undefined &&
192
+ (typeof input.mimeType !== "string" || input.mimeType.length > 200))
193
+ throw new Error("Invalid resource MIME type");
194
+ const directory = await this.directory(scope, input, context, false);
195
+ let source;
196
+ try {
197
+ await directory.verify();
198
+ source = await open(directory.path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
199
+ const initial = await source.stat();
200
+ if (!initial.isFile())
201
+ throw new Error("Tool output must be a regular file");
202
+ const verify = async () => {
203
+ await directory.verify();
204
+ const current = await lstat(directory.path);
205
+ if (current.isSymbolicLink() || !sameResourceIdentity(initial, current))
206
+ throw new Error("Tool output was replaced");
207
+ };
208
+ return {
209
+ asset: await this.library.importFile(scope, directory.path, {
210
+ name: input.name ?? directory.name,
211
+ mimeType: input.mimeType,
212
+ expectedBytes: input.expectedBytes,
213
+ expectedSha256: input.expectedSha256,
214
+ expectedSource: mediaSourceIdentity(initial),
215
+ signal: context.signal,
216
+ assertAuthorized: verify,
217
+ }),
218
+ };
219
+ }
220
+ finally {
221
+ await source?.close().catch(() => { });
222
+ await directory.close();
223
+ }
224
+ }
225
+ async read(scope, raw, context) {
226
+ const input = object(raw, ["assetId", "offset", "length"]);
227
+ const asset = await this.library.get(scope, assetId(input.assetId));
228
+ const offset = integer(input.offset, 0, asset.bytes), length = integer(input.length, 1, CHUNK_BYTES);
229
+ const expected = Math.min(length, asset.bytes - offset);
230
+ const chunks = [];
231
+ let bytes = 0;
232
+ if (expected) {
233
+ const result = await this.library.openRead(scope, asset.id, {
234
+ range: `bytes=${offset}-${offset + expected - 1}`,
235
+ });
236
+ try {
237
+ for await (const chunk of result.body) {
238
+ await this.authorize(scope, context);
239
+ bytes += chunk.length;
240
+ if (bytes > expected)
241
+ throw new Error("Resource range exceeded its limit");
242
+ chunks.push(Buffer.from(chunk));
243
+ }
244
+ if (bytes !== expected)
245
+ throw new Error("Resource range was incomplete");
246
+ }
247
+ finally {
248
+ result.body?.destroy();
249
+ }
250
+ }
251
+ else
252
+ await this.library.resolvePath(scope, asset.id);
253
+ await this.authorize(scope, context);
254
+ return {
255
+ assetId: asset.id,
256
+ offset,
257
+ totalBytes: asset.bytes,
258
+ mimeType: asset.mimeType,
259
+ dataBase64: Buffer.concat(chunks).toString("base64"),
260
+ eof: offset + bytes === asset.bytes,
261
+ };
262
+ }
263
+ async dispatch(scope, method, raw = {}, context = {}) {
264
+ try {
265
+ await this.authorize(scope, context);
266
+ switch (method) {
267
+ case "resources.list": {
268
+ const input = object(raw, ["offset", "limit"]);
269
+ const offset = integer(input.offset ?? 0, 0, 100000), limit = integer(input.limit ?? 50, 1, 100);
270
+ const assets = await this.library.list(scope);
271
+ return { assets: assets.slice(offset, offset + limit), total: assets.length };
272
+ }
273
+ case "resources.get":
274
+ return { asset: await this.library.get(scope, assetId(object(raw, ["id"]).id)) };
275
+ case "resources.read":
276
+ return await this.read(scope, raw, context);
277
+ case "resources.materialize":
278
+ return await this.materialize(scope, raw, context);
279
+ case "resources.capture":
280
+ return await this.capture(scope, raw, context);
281
+ case "resources.upload.begin":
282
+ return await this.uploads.begin(scope, raw);
283
+ case "resources.upload.write":
284
+ return await this.uploads.write(scope, raw);
285
+ case "resources.upload.get":
286
+ return await this.uploads.get(scope, raw);
287
+ case "resources.upload.finish":
288
+ return await this.uploads.finish(scope, raw, context.signal);
289
+ case "resources.upload.cancel":
290
+ return await this.uploads.cancel(scope, raw);
291
+ default:
292
+ throw new PanelResourceError("INVALID_RESOURCE_REQUEST", "Unsupported resource method");
293
+ }
294
+ }
295
+ catch (error) {
296
+ if (error instanceof PanelResourceError ||
297
+ (error instanceof Error && error.name === "AbortError"))
298
+ throw error;
299
+ throw new PanelResourceError("RESOURCE_IO_FAILED", "Resource operation failed; check the file and its authorization, then retry", { cause: error });
300
+ }
301
+ }
302
+ /** Trusted lifecycle hook; call after a tool owner releases its directory grant. */
303
+ releaseDirectory(scope, handle) {
304
+ this.directoryIdentities.delete(`${mediaScopeKey(scope)}:${handle}`);
305
+ }
306
+ cancelScope(scope) {
307
+ return this.uploads.cancelScope(scope);
308
+ }
309
+ cancelApp(appId) {
310
+ return this.uploads.cancelApp(appId);
311
+ }
312
+ async shutdown() {
313
+ this.closed = true;
314
+ await this.uploads.shutdown();
315
+ this.directoryIdentities.clear();
316
+ }
317
+ }
@@ -0,0 +1,10 @@
1
+ import type { MediaScope } from "./types.js";
2
+ export declare const MAX_MEDIA_JSON_BYTES: number;
3
+ export declare function normalizeMediaScope(scope: MediaScope): MediaScope;
4
+ export declare function mediaScopeKey(scope: MediaScope): string;
5
+ export declare function prepareMediaRoot(root: string): Promise<string>;
6
+ /** Validate each managed descendant instead of following a replaced symlink. */
7
+ export declare function mediaDirectory(root: string, segments: string[], create?: boolean): Promise<string>;
8
+ export declare function cloneMediaJson<T>(value: T, maxBytes?: number): T;
9
+ export declare function readMediaJson(file: string): Promise<unknown>;
10
+ export declare function writeMediaJson(file: string, value: unknown): Promise<void>;
@@ -0,0 +1,102 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ export const MAX_MEDIA_JSON_BYTES = 4 * 1024 * 1024;
6
+ export function normalizeMediaScope(scope) {
7
+ if (!scope || !/^[a-z][a-z0-9-]{0,63}$/.test(scope.appId)) {
8
+ throw new Error("Media operations require a valid Host-bound app ID");
9
+ }
10
+ if (typeof scope.projectPath !== "string" ||
11
+ !isAbsolute(scope.projectPath) ||
12
+ scope.projectPath.length > 4096 ||
13
+ scope.projectPath.includes("\0")) {
14
+ throw new Error("Media operations require an absolute Host-bound project path");
15
+ }
16
+ return { appId: scope.appId, projectPath: resolve(scope.projectPath) };
17
+ }
18
+ export function mediaScopeKey(scope) {
19
+ const normalized = normalizeMediaScope(scope);
20
+ return createHash("sha256")
21
+ .update("codeshell-media-v1\0")
22
+ .update(normalized.appId)
23
+ .update("\0")
24
+ .update(normalized.projectPath)
25
+ .digest("hex");
26
+ }
27
+ export async function prepareMediaRoot(root) {
28
+ if (!isAbsolute(root))
29
+ throw new Error("Media storage root must be absolute");
30
+ await mkdir(root, { recursive: true, mode: 0o700 });
31
+ const info = await lstat(root);
32
+ if (!info.isDirectory() || info.isSymbolicLink())
33
+ throw new Error("Media root must be a real directory");
34
+ return realpath(root);
35
+ }
36
+ /** Validate each managed descendant instead of following a replaced symlink. */
37
+ export async function mediaDirectory(root, segments, create = true) {
38
+ let directory = await prepareMediaRoot(root);
39
+ for (const segment of segments) {
40
+ if (!/^[a-zA-Z0-9._-]+$/.test(segment) || segment === "." || segment === "..") {
41
+ throw new Error("Invalid managed media directory");
42
+ }
43
+ directory = join(directory, segment);
44
+ if (create)
45
+ await mkdir(directory, { mode: 0o700 }).catch((error) => {
46
+ if (error.code !== "EEXIST")
47
+ throw error;
48
+ });
49
+ const info = await lstat(directory);
50
+ if (!info.isDirectory() || info.isSymbolicLink())
51
+ throw new Error("Media directory must not be a symlink");
52
+ }
53
+ return directory;
54
+ }
55
+ export function cloneMediaJson(value, maxBytes = MAX_MEDIA_JSON_BYTES) {
56
+ let serialized;
57
+ try {
58
+ serialized = JSON.stringify(value);
59
+ }
60
+ catch {
61
+ throw new Error("Media data must be serializable JSON");
62
+ }
63
+ if (serialized === undefined || Buffer.byteLength(serialized) > maxBytes) {
64
+ throw new Error("Media JSON exceeds its storage budget");
65
+ }
66
+ return JSON.parse(serialized);
67
+ }
68
+ export async function readMediaJson(file) {
69
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
70
+ try {
71
+ const info = await handle.stat();
72
+ if (!info.isFile() || info.size > MAX_MEDIA_JSON_BYTES)
73
+ throw new Error("Invalid media metadata file");
74
+ return JSON.parse(await handle.readFile("utf8"));
75
+ }
76
+ finally {
77
+ await handle.close();
78
+ }
79
+ }
80
+ export async function writeMediaJson(file, value) {
81
+ const serialized = `${JSON.stringify(cloneMediaJson(value))}\n`;
82
+ const temporary = `${file}.${randomUUID()}.tmp`;
83
+ let handle;
84
+ try {
85
+ handle = await open(temporary, "wx", 0o600);
86
+ await handle.writeFile(serialized);
87
+ await handle.sync();
88
+ await handle.close();
89
+ handle = undefined;
90
+ await rename(temporary, file);
91
+ // Sync the rename on platforms that permit directory file descriptors.
92
+ const directory = await open(dirname(file), "r").catch(() => null);
93
+ if (directory) {
94
+ await directory.sync().catch(() => { });
95
+ await directory.close();
96
+ }
97
+ }
98
+ finally {
99
+ await handle?.close().catch(() => { });
100
+ await rm(temporary, { force: true }).catch(() => { });
101
+ }
102
+ }
@@ -0,0 +1,16 @@
1
+ /** Trusted Host binding. Resource requests never accept a guest-supplied scope. */
2
+ export interface ResourceScope {
3
+ appId: string;
4
+ projectPath: string;
5
+ }
6
+ /** Stable compatibility shape: existing asset IDs and records remain unchanged. */
7
+ export interface ResourceAsset {
8
+ id: string;
9
+ name: string;
10
+ mimeType: string;
11
+ bytes: number;
12
+ sha256: string;
13
+ createdAt: number;
14
+ }
15
+ export type MediaScope = ResourceScope;
16
+ export type MediaAsset = ResourceAsset;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { MediaLibrary } from "./library.js";
2
+ import type { MediaAsset, MediaScope } from "./types.js";
3
+ type State = "uploading" | "finishing" | "finished" | "cancelled" | "failed";
4
+ export interface ResourceUploadSession {
5
+ sessionId: string;
6
+ mimeType: string;
7
+ state: State;
8
+ receivedBytes: number;
9
+ nextSequence: number;
10
+ maxChunkBytes: number;
11
+ maxFileBytes: number;
12
+ expiresAt: number;
13
+ }
14
+ export interface ResourceUploadResult {
15
+ asset: MediaAsset;
16
+ }
17
+ export interface ResourceUploadOptions {
18
+ rootDirectory: string;
19
+ library: MediaLibrary;
20
+ isScopeAuthorized(scope: MediaScope): boolean | Promise<boolean>;
21
+ maxFileBytes?: number;
22
+ maxChunkBytes?: number;
23
+ maxActivePerScope?: number;
24
+ maxActiveSessions?: number;
25
+ ttlMs?: number;
26
+ finishTimeoutMs?: number;
27
+ now?: () => number;
28
+ }
29
+ /** Scope-bound, streaming resource uploads. Acknowledged chunks and final results survive Host restart. */
30
+ export declare class ResourceUploadIngest {
31
+ private readonly options;
32
+ private readonly records;
33
+ private readonly locks;
34
+ private readonly hashes;
35
+ private readonly finishes;
36
+ private readonly cancellations;
37
+ private initialization?;
38
+ private cleanupTimer?;
39
+ private stopping;
40
+ constructor(options: ResourceUploadOptions);
41
+ private key;
42
+ private authorize;
43
+ private directory;
44
+ private locked;
45
+ private view;
46
+ private save;
47
+ private validateRecord;
48
+ initialize(): Promise<void>;
49
+ private record;
50
+ private remove;
51
+ cleanupExpired(): Promise<void>;
52
+ begin(scope: MediaScope, raw: unknown): Promise<ResourceUploadSession>;
53
+ get(scope: MediaScope, raw: unknown): Promise<ResourceUploadSession>;
54
+ write(scope: MediaScope, raw: unknown): Promise<ResourceUploadSession>;
55
+ finish(scope: MediaScope, raw: unknown, externalSignal?: AbortSignal): Promise<ResourceUploadResult>;
56
+ cancel(scope: MediaScope, raw: unknown): Promise<{
57
+ cancelled: true;
58
+ }>;
59
+ cancelScope(scope: MediaScope): Promise<void>;
60
+ cancelApp(appId: string): Promise<void>;
61
+ private cancelWhere;
62
+ shutdown(): Promise<void>;
63
+ }
64
+ export {};