@agimon-ai/po-artifact 0.0.0-alpha.1 → 0.0.0-alpha.2
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/dist/apiContracts.cjs +1 -1
- package/dist/apiContracts.cjs.map +1 -1
- package/dist/apiContracts.mjs +1 -1
- package/dist/apiContracts.mjs.map +1 -1
- package/dist/artifactFiles-B8w_HQXv.mjs +197 -0
- package/dist/artifactFiles-B8w_HQXv.mjs.map +1 -0
- package/dist/artifactFiles-Bo5Mhwq0.cjs +280 -0
- package/dist/artifactFiles-Bo5Mhwq0.cjs.map +1 -0
- package/dist/artifactFiles.cjs +6 -0
- package/dist/artifactFiles.d.mts +20 -0
- package/dist/artifactFiles.d.mts.map +1 -0
- package/dist/artifactFiles.mjs +1 -0
- package/dist/extensions/server.cjs +27 -225
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.mjs +14 -189
- package/dist/extensions/server.mjs.map +1 -1
- package/package.json +17 -12
package/dist/apiContracts.cjs
CHANGED
|
@@ -97,7 +97,7 @@ const apiContracts = (0, _agimon_ai_po_core_apiContracts.defineApiContract)({
|
|
|
97
97
|
sockets: ["global", "workspace"].map((scope) => ({
|
|
98
98
|
id: "artifacts.channel",
|
|
99
99
|
scope,
|
|
100
|
-
service: "po.hub
|
|
100
|
+
service: "po.hub",
|
|
101
101
|
member: "artifacts",
|
|
102
102
|
kind: "channel",
|
|
103
103
|
direction: "server-to-client",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiContracts.cjs","names":["Type","defineApiContract","jsonApiResponses"],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract, jsonApiResponses } from '@agimon-ai/po-core/apiContracts';\nimport { Type } from 'typebox';\n\nconst S = Type.String();\nconst N = Type.Number();\nexport const ArtifactsPayloadSchema = Type.Object({\n items: Type.Array(Type.Object({ path: S, name: S, size: N, modifiedAt: N })),\n error: Type.Optional(S),\n});\nconst query = [{ name: 'path', in: 'query' as const, required: true, schema: Type.String({ minLength: 1 }) }];\nexport const apiContracts = defineApiContract({\n version: 1,\n dynamic: [],\n http: [\n {\n id: 'artifacts.list',\n scope: 'session',\n basePath: 'artifacts',\n path: '/',\n method: 'GET',\n authentication: 'owner',\n description: 'List regular artifacts in the current session folder by modification time.',\n responses: jsonApiResponses(ArtifactsPayloadSchema),\n },\n {\n id: 'artifacts.preview',\n scope: 'session',\n basePath: 'artifacts',\n path: '/preview',\n method: 'GET',\n authentication: 'owner',\n description: 'Read bounded text preview from the current session artifact folder.',\n parameters: query,\n responses: jsonApiResponses(\n Type.Object({\n path: S,\n content: S,\n mimeType: S,\n unavailable: Type.Optional(Type.Boolean()),\n reason: Type.Optional(S),\n }),\n ),\n },\n {\n id: 'artifacts.content',\n scope: 'session',\n basePath: 'artifacts',\n path: '/content',\n method: 'GET',\n authentication: 'owner',\n description: 'Read bounded artifact bytes, with range and version checks.',\n parameters: [\n ...query,\n { name: 'Range', in: 'header', required: false, schema: S },\n { name: 'If-Match', in: 'header', required: false, schema: S },\n ],\n responses: {\n '200': { description: 'Artifact bytes.', contentType: 'application/octet-stream' },\n '206': { description: 'Artifact byte range.', contentType: 'application/octet-stream' },\n '400': { description: 'Invalid artifact path.' },\n '403': { description: 'Artifact access rejected.' },\n '404': { description: 'Artifact missing.' },\n '412': { description: 'Artifact changed.' },\n '413': { description: 'Artifact too large.' },\n '416': { description: 'Invalid byte range.' },\n },\n },\n ],\n sockets: (['global', 'workspace'] as const).map((scope) => ({\n id: 'artifacts.channel',\n scope,\n service: 'po.hub
|
|
1
|
+
{"version":3,"file":"apiContracts.cjs","names":["Type","defineApiContract","jsonApiResponses"],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract, jsonApiResponses } from '@agimon-ai/po-core/apiContracts';\nimport { Type } from 'typebox';\n\nconst S = Type.String();\nconst N = Type.Number();\nexport const ArtifactsPayloadSchema = Type.Object({\n items: Type.Array(Type.Object({ path: S, name: S, size: N, modifiedAt: N })),\n error: Type.Optional(S),\n});\nconst query = [{ name: 'path', in: 'query' as const, required: true, schema: Type.String({ minLength: 1 }) }];\nexport const apiContracts = defineApiContract({\n version: 1,\n dynamic: [],\n http: [\n {\n id: 'artifacts.list',\n scope: 'session',\n basePath: 'artifacts',\n path: '/',\n method: 'GET',\n authentication: 'owner',\n description: 'List regular artifacts in the current session folder by modification time.',\n responses: jsonApiResponses(ArtifactsPayloadSchema),\n },\n {\n id: 'artifacts.preview',\n scope: 'session',\n basePath: 'artifacts',\n path: '/preview',\n method: 'GET',\n authentication: 'owner',\n description: 'Read bounded text preview from the current session artifact folder.',\n parameters: query,\n responses: jsonApiResponses(\n Type.Object({\n path: S,\n content: S,\n mimeType: S,\n unavailable: Type.Optional(Type.Boolean()),\n reason: Type.Optional(S),\n }),\n ),\n },\n {\n id: 'artifacts.content',\n scope: 'session',\n basePath: 'artifacts',\n path: '/content',\n method: 'GET',\n authentication: 'owner',\n description: 'Read bounded artifact bytes, with range and version checks.',\n parameters: [\n ...query,\n { name: 'Range', in: 'header', required: false, schema: S },\n { name: 'If-Match', in: 'header', required: false, schema: S },\n ],\n responses: {\n '200': { description: 'Artifact bytes.', contentType: 'application/octet-stream' },\n '206': { description: 'Artifact byte range.', contentType: 'application/octet-stream' },\n '400': { description: 'Invalid artifact path.' },\n '403': { description: 'Artifact access rejected.' },\n '404': { description: 'Artifact missing.' },\n '412': { description: 'Artifact changed.' },\n '413': { description: 'Artifact too large.' },\n '416': { description: 'Invalid byte range.' },\n },\n },\n ],\n sockets: (['global', 'workspace'] as const).map((scope) => ({\n id: 'artifacts.channel',\n scope,\n service: 'po.hub',\n member: 'artifacts',\n kind: 'channel',\n direction: 'server-to-client',\n description: 'Current-session artifact metadata in the hub frame payload.',\n input: ArtifactsPayloadSchema,\n })),\n});\nexport default apiContracts;\n"],"mappings":";;;;;;;AAGA,MAAM,IAAIA,QAAAA,KAAK,OAAO;AACtB,MAAM,IAAIA,QAAAA,KAAK,OAAO;AACtB,MAAa,yBAAyBA,QAAAA,KAAK,OAAO;CAChD,OAAOA,QAAAA,KAAK,MAAMA,QAAAA,KAAK,OAAO;EAAE,MAAM;EAAG,MAAM;EAAG,MAAM;EAAG,YAAY;CAAE,CAAC,CAAC;CAC3E,OAAOA,QAAAA,KAAK,SAAS,CAAC;AACxB,CAAC;AACD,MAAM,QAAQ,CAAC;CAAE,MAAM;CAAQ,IAAI;CAAkB,UAAU;CAAM,QAAQA,QAAAA,KAAK,OAAO,EAAE,WAAW,EAAE,CAAC;AAAE,CAAC;AAC5G,MAAa,gBAAA,GAAeC,gCAAAA,kBAAAA,CAAkB;CAC5C,SAAS;CACT,SAAS,CAAC;CACV,MAAM;EACJ;GACE,IAAI;GACJ,OAAO;GACP,UAAU;GACV,MAAM;GACN,QAAQ;GACR,gBAAgB;GAChB,aAAa;GACb,YAAA,GAAWC,gCAAAA,iBAAAA,CAAiB,sBAAsB;EACpD;EACA;GACE,IAAI;GACJ,OAAO;GACP,UAAU;GACV,MAAM;GACN,QAAQ;GACR,gBAAgB;GAChB,aAAa;GACb,YAAY;GACZ,YAAA,GAAWA,gCAAAA,iBAAAA,CACTF,QAAAA,KAAK,OAAO;IACV,MAAM;IACN,SAAS;IACT,UAAU;IACV,aAAaA,QAAAA,KAAK,SAASA,QAAAA,KAAK,QAAQ,CAAC;IACzC,QAAQA,QAAAA,KAAK,SAAS,CAAC;GACzB,CAAC,CACH;EACF;EACA;GACE,IAAI;GACJ,OAAO;GACP,UAAU;GACV,MAAM;GACN,QAAQ;GACR,gBAAgB;GAChB,aAAa;GACb,YAAY;IACV,GAAG;IACH;KAAE,MAAM;KAAS,IAAI;KAAU,UAAU;KAAO,QAAQ;IAAE;IAC1D;KAAE,MAAM;KAAY,IAAI;KAAU,UAAU;KAAO,QAAQ;IAAE;GAC/D;GACA,WAAW;IACT,OAAO;KAAE,aAAa;KAAmB,aAAa;IAA2B;IACjF,OAAO;KAAE,aAAa;KAAwB,aAAa;IAA2B;IACtF,OAAO,EAAE,aAAa,yBAAyB;IAC/C,OAAO,EAAE,aAAa,4BAA4B;IAClD,OAAO,EAAE,aAAa,oBAAoB;IAC1C,OAAO,EAAE,aAAa,oBAAoB;IAC1C,OAAO,EAAE,aAAa,sBAAsB;IAC5C,OAAO,EAAE,aAAa,sBAAsB;GAC9C;EACF;CACF;CACA,SAAU,CAAC,UAAU,WAAW,CAAC,CAAW,KAAK,WAAW;EAC1D,IAAI;EACJ;EACA,SAAS;EACT,QAAQ;EACR,MAAM;EACN,WAAW;EACX,aAAa;EACb,OAAO;CACT,EAAE;AACJ,CAAC"}
|
package/dist/apiContracts.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiContracts.mjs","names":[],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract, jsonApiResponses } from '@agimon-ai/po-core/apiContracts';\nimport { Type } from 'typebox';\n\nconst S = Type.String();\nconst N = Type.Number();\nexport const ArtifactsPayloadSchema = Type.Object({\n items: Type.Array(Type.Object({ path: S, name: S, size: N, modifiedAt: N })),\n error: Type.Optional(S),\n});\nconst query = [{ name: 'path', in: 'query' as const, required: true, schema: Type.String({ minLength: 1 }) }];\nexport const apiContracts = defineApiContract({\n version: 1,\n dynamic: [],\n http: [\n {\n id: 'artifacts.list',\n scope: 'session',\n basePath: 'artifacts',\n path: '/',\n method: 'GET',\n authentication: 'owner',\n description: 'List regular artifacts in the current session folder by modification time.',\n responses: jsonApiResponses(ArtifactsPayloadSchema),\n },\n {\n id: 'artifacts.preview',\n scope: 'session',\n basePath: 'artifacts',\n path: '/preview',\n method: 'GET',\n authentication: 'owner',\n description: 'Read bounded text preview from the current session artifact folder.',\n parameters: query,\n responses: jsonApiResponses(\n Type.Object({\n path: S,\n content: S,\n mimeType: S,\n unavailable: Type.Optional(Type.Boolean()),\n reason: Type.Optional(S),\n }),\n ),\n },\n {\n id: 'artifacts.content',\n scope: 'session',\n basePath: 'artifacts',\n path: '/content',\n method: 'GET',\n authentication: 'owner',\n description: 'Read bounded artifact bytes, with range and version checks.',\n parameters: [\n ...query,\n { name: 'Range', in: 'header', required: false, schema: S },\n { name: 'If-Match', in: 'header', required: false, schema: S },\n ],\n responses: {\n '200': { description: 'Artifact bytes.', contentType: 'application/octet-stream' },\n '206': { description: 'Artifact byte range.', contentType: 'application/octet-stream' },\n '400': { description: 'Invalid artifact path.' },\n '403': { description: 'Artifact access rejected.' },\n '404': { description: 'Artifact missing.' },\n '412': { description: 'Artifact changed.' },\n '413': { description: 'Artifact too large.' },\n '416': { description: 'Invalid byte range.' },\n },\n },\n ],\n sockets: (['global', 'workspace'] as const).map((scope) => ({\n id: 'artifacts.channel',\n scope,\n service: 'po.hub
|
|
1
|
+
{"version":3,"file":"apiContracts.mjs","names":[],"sources":["../src/schemas/apiContracts.ts"],"sourcesContent":["import { defineApiContract, jsonApiResponses } from '@agimon-ai/po-core/apiContracts';\nimport { Type } from 'typebox';\n\nconst S = Type.String();\nconst N = Type.Number();\nexport const ArtifactsPayloadSchema = Type.Object({\n items: Type.Array(Type.Object({ path: S, name: S, size: N, modifiedAt: N })),\n error: Type.Optional(S),\n});\nconst query = [{ name: 'path', in: 'query' as const, required: true, schema: Type.String({ minLength: 1 }) }];\nexport const apiContracts = defineApiContract({\n version: 1,\n dynamic: [],\n http: [\n {\n id: 'artifacts.list',\n scope: 'session',\n basePath: 'artifacts',\n path: '/',\n method: 'GET',\n authentication: 'owner',\n description: 'List regular artifacts in the current session folder by modification time.',\n responses: jsonApiResponses(ArtifactsPayloadSchema),\n },\n {\n id: 'artifacts.preview',\n scope: 'session',\n basePath: 'artifacts',\n path: '/preview',\n method: 'GET',\n authentication: 'owner',\n description: 'Read bounded text preview from the current session artifact folder.',\n parameters: query,\n responses: jsonApiResponses(\n Type.Object({\n path: S,\n content: S,\n mimeType: S,\n unavailable: Type.Optional(Type.Boolean()),\n reason: Type.Optional(S),\n }),\n ),\n },\n {\n id: 'artifacts.content',\n scope: 'session',\n basePath: 'artifacts',\n path: '/content',\n method: 'GET',\n authentication: 'owner',\n description: 'Read bounded artifact bytes, with range and version checks.',\n parameters: [\n ...query,\n { name: 'Range', in: 'header', required: false, schema: S },\n { name: 'If-Match', in: 'header', required: false, schema: S },\n ],\n responses: {\n '200': { description: 'Artifact bytes.', contentType: 'application/octet-stream' },\n '206': { description: 'Artifact byte range.', contentType: 'application/octet-stream' },\n '400': { description: 'Invalid artifact path.' },\n '403': { description: 'Artifact access rejected.' },\n '404': { description: 'Artifact missing.' },\n '412': { description: 'Artifact changed.' },\n '413': { description: 'Artifact too large.' },\n '416': { description: 'Invalid byte range.' },\n },\n },\n ],\n sockets: (['global', 'workspace'] as const).map((scope) => ({\n id: 'artifacts.channel',\n scope,\n service: 'po.hub',\n member: 'artifacts',\n kind: 'channel',\n direction: 'server-to-client',\n description: 'Current-session artifact metadata in the hub frame payload.',\n input: ArtifactsPayloadSchema,\n })),\n});\nexport default apiContracts;\n"],"mappings":";;;AAGA,MAAM,IAAI,KAAK,OAAO;AACtB,MAAM,IAAI,KAAK,OAAO;AACtB,MAAa,yBAAyB,KAAK,OAAO;CAChD,OAAO,KAAK,MAAM,KAAK,OAAO;EAAE,MAAM;EAAG,MAAM;EAAG,MAAM;EAAG,YAAY;CAAE,CAAC,CAAC;CAC3E,OAAO,KAAK,SAAS,CAAC;AACxB,CAAC;AACD,MAAM,QAAQ,CAAC;CAAE,MAAM;CAAQ,IAAI;CAAkB,UAAU;CAAM,QAAQ,KAAK,OAAO,EAAE,WAAW,EAAE,CAAC;AAAE,CAAC;AAC5G,MAAa,eAAe,kBAAkB;CAC5C,SAAS;CACT,SAAS,CAAC;CACV,MAAM;EACJ;GACE,IAAI;GACJ,OAAO;GACP,UAAU;GACV,MAAM;GACN,QAAQ;GACR,gBAAgB;GAChB,aAAa;GACb,WAAW,iBAAiB,sBAAsB;EACpD;EACA;GACE,IAAI;GACJ,OAAO;GACP,UAAU;GACV,MAAM;GACN,QAAQ;GACR,gBAAgB;GAChB,aAAa;GACb,YAAY;GACZ,WAAW,iBACT,KAAK,OAAO;IACV,MAAM;IACN,SAAS;IACT,UAAU;IACV,aAAa,KAAK,SAAS,KAAK,QAAQ,CAAC;IACzC,QAAQ,KAAK,SAAS,CAAC;GACzB,CAAC,CACH;EACF;EACA;GACE,IAAI;GACJ,OAAO;GACP,UAAU;GACV,MAAM;GACN,QAAQ;GACR,gBAAgB;GAChB,aAAa;GACb,YAAY;IACV,GAAG;IACH;KAAE,MAAM;KAAS,IAAI;KAAU,UAAU;KAAO,QAAQ;IAAE;IAC1D;KAAE,MAAM;KAAY,IAAI;KAAU,UAAU;KAAO,QAAQ;IAAE;GAC/D;GACA,WAAW;IACT,OAAO;KAAE,aAAa;KAAmB,aAAa;IAA2B;IACjF,OAAO;KAAE,aAAa;KAAwB,aAAa;IAA2B;IACtF,OAAO,EAAE,aAAa,yBAAyB;IAC/C,OAAO,EAAE,aAAa,4BAA4B;IAClD,OAAO,EAAE,aAAa,oBAAoB;IAC1C,OAAO,EAAE,aAAa,oBAAoB;IAC1C,OAAO,EAAE,aAAa,sBAAsB;IAC5C,OAAO,EAAE,aAAa,sBAAsB;GAC9C;EACF;CACF;CACA,SAAU,CAAC,UAAU,WAAW,CAAC,CAAW,KAAK,WAAW;EAC1D,IAAI;EACJ;EACA,SAAS;EACT,QAAQ;EACR,MAAM;EACN,WAAW;EACX,aAAa;EACb,OAAO;CACT,EAAE;AACJ,CAAC"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { piAgentDirectory } from "@agimon-ai/po-core/piSettings";
|
|
5
|
+
//#region src/constants/artifacts.ts
|
|
6
|
+
const ARTIFACT_DIRECTORY = "doom-artifacts";
|
|
7
|
+
const MAX_PREVIEW_BYTES = 1048576;
|
|
8
|
+
const MAX_CONTENT_BYTES = 26214400;
|
|
9
|
+
const ARTIFACT_REFRESH_MS = 2e3;
|
|
10
|
+
const ARTIFACT_CONTENT_TYPES = {
|
|
11
|
+
".avif": "image/avif",
|
|
12
|
+
".bmp": "image/bmp",
|
|
13
|
+
".gif": "image/gif",
|
|
14
|
+
".jpeg": "image/jpeg",
|
|
15
|
+
".jpg": "image/jpeg",
|
|
16
|
+
".png": "image/png",
|
|
17
|
+
".svg": "image/svg+xml",
|
|
18
|
+
".webp": "image/webp",
|
|
19
|
+
".ico": "image/vnd.microsoft.icon",
|
|
20
|
+
".mp3": "audio/mpeg",
|
|
21
|
+
".wav": "audio/wav",
|
|
22
|
+
".m4a": "audio/mp4",
|
|
23
|
+
".ogg": "audio/ogg",
|
|
24
|
+
".m4v": "video/mp4",
|
|
25
|
+
".mov": "video/quicktime",
|
|
26
|
+
".mp4": "video/mp4",
|
|
27
|
+
".webm": "video/webm",
|
|
28
|
+
".pdf": "application/pdf",
|
|
29
|
+
".md": "text/markdown; charset=utf-8",
|
|
30
|
+
".markdown": "text/markdown; charset=utf-8",
|
|
31
|
+
".txt": "text/plain; charset=utf-8",
|
|
32
|
+
".html": "text/html; charset=utf-8",
|
|
33
|
+
".htm": "text/html; charset=utf-8"
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/services/artifactFiles/artifactPaths.ts
|
|
37
|
+
var ArtifactAccessError = class extends Error {
|
|
38
|
+
status;
|
|
39
|
+
constructor(message, status) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.status = status;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function artifactRoot(sessionId, environment) {
|
|
45
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) throw new ArtifactAccessError("Invalid artifact session identity.", 400);
|
|
46
|
+
return path.join(piAgentDirectory(environment, environment.HOME), ARTIFACT_DIRECTORY, sessionId);
|
|
47
|
+
}
|
|
48
|
+
function artifactMimeType(relativePath) {
|
|
49
|
+
return ARTIFACT_CONTENT_TYPES[path.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/services/artifactFiles/index.ts
|
|
53
|
+
function inside(root, candidate) {
|
|
54
|
+
const relative = path.relative(root, candidate);
|
|
55
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
56
|
+
}
|
|
57
|
+
function codeOf(error) {
|
|
58
|
+
return error instanceof Error && "code" in error ? String(error.code) : void 0;
|
|
59
|
+
}
|
|
60
|
+
/** The owned directories must not redirect artifact access through a symlink. */
|
|
61
|
+
async function checkedRoot(root) {
|
|
62
|
+
const base = path.dirname(root);
|
|
63
|
+
for (const directory of [base, root]) {
|
|
64
|
+
const stat = await fs.lstat(directory);
|
|
65
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new ArtifactAccessError("The artifact folder is unavailable.", 403);
|
|
66
|
+
}
|
|
67
|
+
const realBase = await fs.realpath(base);
|
|
68
|
+
const realRoot = await fs.realpath(root);
|
|
69
|
+
if (!inside(realBase, realRoot)) throw new ArtifactAccessError("The artifact folder is outside its storage directory.", 403);
|
|
70
|
+
return realRoot;
|
|
71
|
+
}
|
|
72
|
+
async function ensureArtifactRoot(root) {
|
|
73
|
+
await fs.mkdir(path.dirname(root), {
|
|
74
|
+
recursive: true,
|
|
75
|
+
mode: 448
|
|
76
|
+
});
|
|
77
|
+
await fs.mkdir(root, {
|
|
78
|
+
recursive: true,
|
|
79
|
+
mode: 448
|
|
80
|
+
});
|
|
81
|
+
await checkedRoot(root);
|
|
82
|
+
}
|
|
83
|
+
/** Open without following mutable ancestors. Node exposes no portable openat API. */
|
|
84
|
+
async function openContained(root, segments) {
|
|
85
|
+
if (process.platform === "darwin") return fs.open(path.join(root, ...segments), constants.O_RDONLY | constants.O_NONBLOCK | 536870912);
|
|
86
|
+
if (process.platform !== "linux") throw new ArtifactAccessError("Secure artifact reads require macOS or Linux.", 403);
|
|
87
|
+
const flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;
|
|
88
|
+
let directory = await fs.open(path.dirname(root), flags);
|
|
89
|
+
try {
|
|
90
|
+
for (const segment of [path.basename(root), ...segments.slice(0, -1)]) {
|
|
91
|
+
const child = await fs.open(`/proc/self/fd/${directory.fd}/${segment}`, flags);
|
|
92
|
+
await directory.close();
|
|
93
|
+
directory = child;
|
|
94
|
+
}
|
|
95
|
+
return await fs.open(`/proc/self/fd/${directory.fd}/${segments.at(-1)}`, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
96
|
+
} finally {
|
|
97
|
+
await directory.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** One guarded open shared by metadata, text and byte reads. The caller closes the handle. */
|
|
101
|
+
async function openArtifact(root, requested) {
|
|
102
|
+
if (!requested || requested.includes("\0") || requested.includes("\\") || path.isAbsolute(requested) || requested.split("/").some((part) => part === ".." || part === "." || part === "")) throw new ArtifactAccessError("Invalid artifact path.", 400);
|
|
103
|
+
try {
|
|
104
|
+
const realRoot = await checkedRoot(root);
|
|
105
|
+
const candidate = path.resolve(realRoot, requested);
|
|
106
|
+
if (!inside(realRoot, candidate)) throw new ArtifactAccessError("The artifact is outside this session folder.", 403);
|
|
107
|
+
let current = realRoot;
|
|
108
|
+
const segments = requested.split("/");
|
|
109
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
110
|
+
current = path.join(current, segments[index]);
|
|
111
|
+
const stat = await fs.lstat(current);
|
|
112
|
+
if (stat.isSymbolicLink()) throw new ArtifactAccessError("Artifact symlinks cannot be read.", 403);
|
|
113
|
+
if (index < segments.length - 1 ? !stat.isDirectory() : !stat.isFile()) throw new ArtifactAccessError("The artifact is not a regular file.", 404);
|
|
114
|
+
}
|
|
115
|
+
const real = await fs.realpath(candidate);
|
|
116
|
+
if (!inside(realRoot, real)) throw new ArtifactAccessError("The artifact is outside this session folder.", 403);
|
|
117
|
+
const expected = await fs.lstat(real);
|
|
118
|
+
const handle = await openContained(realRoot, segments);
|
|
119
|
+
try {
|
|
120
|
+
const stat = await handle.stat();
|
|
121
|
+
if (!stat.isFile() || stat.dev !== expected.dev || stat.ino !== expected.ino) throw new ArtifactAccessError("The artifact changed while it was opened. Try again.", 403);
|
|
122
|
+
return {
|
|
123
|
+
handle,
|
|
124
|
+
stat,
|
|
125
|
+
path: requested,
|
|
126
|
+
mimeType: artifactMimeType(requested)
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await handle.close();
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (error instanceof ArtifactAccessError) throw error;
|
|
134
|
+
if (codeOf(error) === "ENOENT" || codeOf(error) === "ENOTDIR") throw new ArtifactAccessError("The artifact no longer exists.", 404);
|
|
135
|
+
if (codeOf(error) === "EACCES" || codeOf(error) === "EPERM" || codeOf(error) === "ELOOP") throw new ArtifactAccessError("The artifact cannot be read.", 403);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Bound allocation and read length even when the file grows after its metadata was read. */
|
|
140
|
+
async function readArtifactBytes(handle, size, offset = 0) {
|
|
141
|
+
const bytes = new Uint8Array(size);
|
|
142
|
+
let read = 0;
|
|
143
|
+
while (read < size) {
|
|
144
|
+
const result = await handle.read(bytes, read, size - read, offset + read);
|
|
145
|
+
if (result.bytesRead === 0) break;
|
|
146
|
+
read += result.bytesRead;
|
|
147
|
+
}
|
|
148
|
+
return bytes.subarray(0, read);
|
|
149
|
+
}
|
|
150
|
+
async function listArtifacts(root) {
|
|
151
|
+
const items = [];
|
|
152
|
+
try {
|
|
153
|
+
const realRoot = await checkedRoot(root);
|
|
154
|
+
const visit = async (directory) => {
|
|
155
|
+
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
156
|
+
const location = path.join(directory, entry.name);
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
const stat = await fs.lstat(location);
|
|
159
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
160
|
+
const real = await fs.realpath(location);
|
|
161
|
+
if (inside(realRoot, real)) await visit(real);
|
|
162
|
+
} else if (entry.isFile()) {
|
|
163
|
+
const relativePath = path.relative(realRoot, location).split(path.sep).join("/");
|
|
164
|
+
try {
|
|
165
|
+
const file = await openArtifact(root, relativePath);
|
|
166
|
+
try {
|
|
167
|
+
items.push({
|
|
168
|
+
path: relativePath,
|
|
169
|
+
name: entry.name,
|
|
170
|
+
size: file.stat.size,
|
|
171
|
+
modifiedAt: file.stat.mtimeMs
|
|
172
|
+
});
|
|
173
|
+
} finally {
|
|
174
|
+
await file.handle.close();
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error instanceof ArtifactAccessError && (error.status === 404 || error.status === 403)) continue;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
await visit(realRoot);
|
|
184
|
+
items.sort((left, right) => right.modifiedAt - left.modifiedAt || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
185
|
+
return { items };
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (codeOf(error) === "ENOENT") return { items: [] };
|
|
188
|
+
return {
|
|
189
|
+
items: [],
|
|
190
|
+
error: error instanceof Error ? error.message : "The artifact folder could not be read."
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
export { ArtifactAccessError as a, MAX_CONTENT_BYTES as c, readArtifactBytes as i, MAX_PREVIEW_BYTES as l, listArtifacts as n, artifactRoot as o, openArtifact as r, ARTIFACT_REFRESH_MS as s, ensureArtifactRoot as t };
|
|
196
|
+
|
|
197
|
+
//# sourceMappingURL=artifactFiles-B8w_HQXv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifactFiles-B8w_HQXv.mjs","names":[],"sources":["../src/constants/artifacts.ts","../src/services/artifactFiles/artifactPaths.ts","../src/services/artifactFiles/index.ts"],"sourcesContent":["export const ARTIFACT_DIRECTORY = 'doom-artifacts';\nexport const MAX_PREVIEW_BYTES = 1024 * 1024;\nexport const MAX_CONTENT_BYTES = 25 * 1024 * 1024;\nexport const ARTIFACT_REFRESH_MS = 2000;\n\nexport const ARTIFACT_CONTENT_TYPES: Readonly<Record<string, string>> = {\n '.avif': 'image/avif',\n '.bmp': 'image/bmp',\n '.gif': 'image/gif',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.webp': 'image/webp',\n '.ico': 'image/vnd.microsoft.icon',\n '.mp3': 'audio/mpeg',\n '.wav': 'audio/wav',\n '.m4a': 'audio/mp4',\n '.ogg': 'audio/ogg',\n '.m4v': 'video/mp4',\n '.mov': 'video/quicktime',\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n '.pdf': 'application/pdf',\n '.md': 'text/markdown; charset=utf-8',\n '.markdown': 'text/markdown; charset=utf-8',\n '.txt': 'text/plain; charset=utf-8',\n '.html': 'text/html; charset=utf-8',\n '.htm': 'text/html; charset=utf-8',\n};\n","import path from 'node:path';\n\nimport { piAgentDirectory } from '@agimon-ai/po-core/piSettings';\n\nimport { ARTIFACT_CONTENT_TYPES, ARTIFACT_DIRECTORY } from '../../constants/artifacts';\n\nexport class ArtifactAccessError extends Error {\n constructor(\n message: string,\n readonly status: 400 | 403 | 404 | 413,\n ) {\n super(message);\n }\n}\n\nexport function artifactRoot(sessionId: string, environment: Readonly<Record<string, string | undefined>>): string {\n if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) throw new ArtifactAccessError('Invalid artifact session identity.', 400);\n return path.join(piAgentDirectory(environment, environment.HOME), ARTIFACT_DIRECTORY, sessionId);\n}\n\nexport function artifactMimeType(relativePath: string): string {\n return ARTIFACT_CONTENT_TYPES[path.extname(relativePath).toLowerCase()] ?? 'application/octet-stream';\n}\n","import { constants } from 'node:fs';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { ArtifactItem, ArtifactSnapshot } from '../../types/artifacts';\nimport { ArtifactAccessError, artifactMimeType } from './artifactPaths';\n\nexport { ArtifactAccessError, artifactMimeType, artifactRoot } from './artifactPaths';\n\nfunction inside(root: string, candidate: string): boolean {\n const relative = path.relative(root, candidate);\n return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);\n}\n\nfunction codeOf(error: unknown): string | undefined {\n return error instanceof Error && 'code' in error ? String(error.code) : undefined;\n}\n\n/** The owned directories must not redirect artifact access through a symlink. */\nasync function checkedRoot(root: string): Promise<string> {\n const base = path.dirname(root);\n for (const directory of [base, root]) {\n const stat = await fs.lstat(directory);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new ArtifactAccessError('The artifact folder is unavailable.', 403);\n }\n const realBase = await fs.realpath(base);\n const realRoot = await fs.realpath(root);\n if (!inside(realBase, realRoot))\n throw new ArtifactAccessError('The artifact folder is outside its storage directory.', 403);\n return realRoot;\n}\n\nexport async function ensureArtifactRoot(root: string): Promise<void> {\n await fs.mkdir(path.dirname(root), { recursive: true, mode: 0o700 });\n await fs.mkdir(root, { recursive: true, mode: 0o700 });\n await checkedRoot(root);\n}\n\n/** Open without following mutable ancestors. Node exposes no portable openat API. */\nasync function openContained(root: string, segments: string[]): Promise<fs.FileHandle> {\n if (process.platform === 'darwin') {\n // Darwin O_NOFOLLOW_ANY checks every component atomically. Do not combine it with O_NOFOLLOW.\n return fs.open(path.join(root, ...segments), constants.O_RDONLY | constants.O_NONBLOCK | 0x20000000);\n }\n if (process.platform !== 'linux') throw new ArtifactAccessError('Secure artifact reads require macOS or Linux.', 403);\n const flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;\n let directory = await fs.open(path.dirname(root), flags);\n try {\n for (const segment of [path.basename(root), ...segments.slice(0, -1)]) {\n const child = await fs.open(`/proc/self/fd/${directory.fd}/${segment}`, flags);\n await directory.close();\n directory = child;\n }\n return await fs.open(\n `/proc/self/fd/${directory.fd}/${segments.at(-1)!}`,\n constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,\n );\n } finally {\n await directory.close();\n }\n}\n\n/** One guarded open shared by metadata, text and byte reads. The caller closes the handle. */\nexport async function openArtifact(root: string, requested: string | undefined) {\n if (\n !requested ||\n requested.includes('\\0') ||\n requested.includes('\\\\') ||\n path.isAbsolute(requested) ||\n requested.split('/').some((part) => part === '..' || part === '.' || part === '')\n )\n throw new ArtifactAccessError('Invalid artifact path.', 400);\n try {\n const realRoot = await checkedRoot(root);\n const candidate = path.resolve(realRoot, requested);\n if (!inside(realRoot, candidate))\n throw new ArtifactAccessError('The artifact is outside this session folder.', 403);\n let current = realRoot;\n const segments = requested.split('/');\n for (let index = 0; index < segments.length; index += 1) {\n current = path.join(current, segments[index]!);\n const stat = await fs.lstat(current);\n if (stat.isSymbolicLink()) throw new ArtifactAccessError('Artifact symlinks cannot be read.', 403);\n if (index < segments.length - 1 ? !stat.isDirectory() : !stat.isFile())\n throw new ArtifactAccessError('The artifact is not a regular file.', 404);\n }\n const real = await fs.realpath(candidate);\n if (!inside(realRoot, real)) throw new ArtifactAccessError('The artifact is outside this session folder.', 403);\n const expected = await fs.lstat(real);\n const handle = await openContained(realRoot, segments);\n try {\n const stat = await handle.stat();\n if (!stat.isFile() || stat.dev !== expected.dev || stat.ino !== expected.ino)\n throw new ArtifactAccessError('The artifact changed while it was opened. Try again.', 403);\n return { handle, stat, path: requested, mimeType: artifactMimeType(requested) };\n } catch (error) {\n await handle.close();\n throw error;\n }\n } catch (error) {\n if (error instanceof ArtifactAccessError) throw error;\n if (codeOf(error) === 'ENOENT' || codeOf(error) === 'ENOTDIR')\n throw new ArtifactAccessError('The artifact no longer exists.', 404);\n if (codeOf(error) === 'EACCES' || codeOf(error) === 'EPERM' || codeOf(error) === 'ELOOP')\n throw new ArtifactAccessError('The artifact cannot be read.', 403);\n throw error;\n }\n}\n\n/** Bound allocation and read length even when the file grows after its metadata was read. */\nexport async function readArtifactBytes(handle: fs.FileHandle, size: number, offset = 0): Promise<Uint8Array> {\n const bytes = new Uint8Array(size);\n let read = 0;\n while (read < size) {\n const result = await handle.read(bytes, read, size - read, offset + read);\n if (result.bytesRead === 0) break;\n read += result.bytesRead;\n }\n return bytes.subarray(0, read);\n}\n\nexport async function listArtifacts(root: string): Promise<ArtifactSnapshot> {\n const items: ArtifactItem[] = [];\n try {\n const realRoot = await checkedRoot(root);\n const visit = async (directory: string): Promise<void> => {\n for (const entry of await fs.readdir(directory, { withFileTypes: true })) {\n const location = path.join(directory, entry.name);\n if (entry.isDirectory()) {\n // Recheck each directory rather than trusting a readdir entry across a rename.\n const stat = await fs.lstat(location);\n if (!stat.isDirectory() || stat.isSymbolicLink()) continue;\n const real = await fs.realpath(location);\n if (inside(realRoot, real)) await visit(real);\n } else if (entry.isFile()) {\n const relativePath = path.relative(realRoot, location).split(path.sep).join('/');\n try {\n const file = await openArtifact(root, relativePath);\n try {\n items.push({ path: relativePath, name: entry.name, size: file.stat.size, modifiedAt: file.stat.mtimeMs });\n } finally {\n await file.handle.close();\n }\n } catch (error) {\n if (error instanceof ArtifactAccessError && (error.status === 404 || error.status === 403)) continue;\n throw error;\n }\n }\n }\n };\n await visit(realRoot);\n items.sort(\n (left, right) =>\n right.modifiedAt - left.modifiedAt || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0),\n );\n return { items };\n } catch (error) {\n if (codeOf(error) === 'ENOENT') return { items: [] };\n return { items: [], error: error instanceof Error ? error.message : 'The artifact folder could not be read.' };\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
//#endregion
|
|
23
|
+
let node_fs = require("node:fs");
|
|
24
|
+
let node_fs_promises = require("node:fs/promises");
|
|
25
|
+
node_fs_promises = __toESM(node_fs_promises, 1);
|
|
26
|
+
let node_path = require("node:path");
|
|
27
|
+
node_path = __toESM(node_path, 1);
|
|
28
|
+
let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
|
|
29
|
+
//#region src/constants/artifacts.ts
|
|
30
|
+
const ARTIFACT_DIRECTORY = "doom-artifacts";
|
|
31
|
+
const MAX_PREVIEW_BYTES = 1048576;
|
|
32
|
+
const MAX_CONTENT_BYTES = 26214400;
|
|
33
|
+
const ARTIFACT_REFRESH_MS = 2e3;
|
|
34
|
+
const ARTIFACT_CONTENT_TYPES = {
|
|
35
|
+
".avif": "image/avif",
|
|
36
|
+
".bmp": "image/bmp",
|
|
37
|
+
".gif": "image/gif",
|
|
38
|
+
".jpeg": "image/jpeg",
|
|
39
|
+
".jpg": "image/jpeg",
|
|
40
|
+
".png": "image/png",
|
|
41
|
+
".svg": "image/svg+xml",
|
|
42
|
+
".webp": "image/webp",
|
|
43
|
+
".ico": "image/vnd.microsoft.icon",
|
|
44
|
+
".mp3": "audio/mpeg",
|
|
45
|
+
".wav": "audio/wav",
|
|
46
|
+
".m4a": "audio/mp4",
|
|
47
|
+
".ogg": "audio/ogg",
|
|
48
|
+
".m4v": "video/mp4",
|
|
49
|
+
".mov": "video/quicktime",
|
|
50
|
+
".mp4": "video/mp4",
|
|
51
|
+
".webm": "video/webm",
|
|
52
|
+
".pdf": "application/pdf",
|
|
53
|
+
".md": "text/markdown; charset=utf-8",
|
|
54
|
+
".markdown": "text/markdown; charset=utf-8",
|
|
55
|
+
".txt": "text/plain; charset=utf-8",
|
|
56
|
+
".html": "text/html; charset=utf-8",
|
|
57
|
+
".htm": "text/html; charset=utf-8"
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/services/artifactFiles/artifactPaths.ts
|
|
61
|
+
var ArtifactAccessError = class extends Error {
|
|
62
|
+
status;
|
|
63
|
+
constructor(message, status) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.status = status;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
function artifactRoot(sessionId, environment) {
|
|
69
|
+
if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) throw new ArtifactAccessError("Invalid artifact session identity.", 400);
|
|
70
|
+
return node_path.default.join((0, _agimon_ai_po_core_piSettings.piAgentDirectory)(environment, environment.HOME), ARTIFACT_DIRECTORY, sessionId);
|
|
71
|
+
}
|
|
72
|
+
function artifactMimeType(relativePath) {
|
|
73
|
+
return ARTIFACT_CONTENT_TYPES[node_path.default.extname(relativePath).toLowerCase()] ?? "application/octet-stream";
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/services/artifactFiles/index.ts
|
|
77
|
+
function inside(root, candidate) {
|
|
78
|
+
const relative = node_path.default.relative(root, candidate);
|
|
79
|
+
return relative !== ".." && !relative.startsWith(`..${node_path.default.sep}`) && !node_path.default.isAbsolute(relative);
|
|
80
|
+
}
|
|
81
|
+
function codeOf(error) {
|
|
82
|
+
return error instanceof Error && "code" in error ? String(error.code) : void 0;
|
|
83
|
+
}
|
|
84
|
+
/** The owned directories must not redirect artifact access through a symlink. */
|
|
85
|
+
async function checkedRoot(root) {
|
|
86
|
+
const base = node_path.default.dirname(root);
|
|
87
|
+
for (const directory of [base, root]) {
|
|
88
|
+
const stat = await node_fs_promises.default.lstat(directory);
|
|
89
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new ArtifactAccessError("The artifact folder is unavailable.", 403);
|
|
90
|
+
}
|
|
91
|
+
const realBase = await node_fs_promises.default.realpath(base);
|
|
92
|
+
const realRoot = await node_fs_promises.default.realpath(root);
|
|
93
|
+
if (!inside(realBase, realRoot)) throw new ArtifactAccessError("The artifact folder is outside its storage directory.", 403);
|
|
94
|
+
return realRoot;
|
|
95
|
+
}
|
|
96
|
+
async function ensureArtifactRoot(root) {
|
|
97
|
+
await node_fs_promises.default.mkdir(node_path.default.dirname(root), {
|
|
98
|
+
recursive: true,
|
|
99
|
+
mode: 448
|
|
100
|
+
});
|
|
101
|
+
await node_fs_promises.default.mkdir(root, {
|
|
102
|
+
recursive: true,
|
|
103
|
+
mode: 448
|
|
104
|
+
});
|
|
105
|
+
await checkedRoot(root);
|
|
106
|
+
}
|
|
107
|
+
/** Open without following mutable ancestors. Node exposes no portable openat API. */
|
|
108
|
+
async function openContained(root, segments) {
|
|
109
|
+
if (process.platform === "darwin") return node_fs_promises.default.open(node_path.default.join(root, ...segments), node_fs.constants.O_RDONLY | node_fs.constants.O_NONBLOCK | 536870912);
|
|
110
|
+
if (process.platform !== "linux") throw new ArtifactAccessError("Secure artifact reads require macOS or Linux.", 403);
|
|
111
|
+
const flags = node_fs.constants.O_RDONLY | node_fs.constants.O_DIRECTORY | node_fs.constants.O_NOFOLLOW;
|
|
112
|
+
let directory = await node_fs_promises.default.open(node_path.default.dirname(root), flags);
|
|
113
|
+
try {
|
|
114
|
+
for (const segment of [node_path.default.basename(root), ...segments.slice(0, -1)]) {
|
|
115
|
+
const child = await node_fs_promises.default.open(`/proc/self/fd/${directory.fd}/${segment}`, flags);
|
|
116
|
+
await directory.close();
|
|
117
|
+
directory = child;
|
|
118
|
+
}
|
|
119
|
+
return await node_fs_promises.default.open(`/proc/self/fd/${directory.fd}/${segments.at(-1)}`, node_fs.constants.O_RDONLY | node_fs.constants.O_NOFOLLOW | node_fs.constants.O_NONBLOCK);
|
|
120
|
+
} finally {
|
|
121
|
+
await directory.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** One guarded open shared by metadata, text and byte reads. The caller closes the handle. */
|
|
125
|
+
async function openArtifact(root, requested) {
|
|
126
|
+
if (!requested || requested.includes("\0") || requested.includes("\\") || node_path.default.isAbsolute(requested) || requested.split("/").some((part) => part === ".." || part === "." || part === "")) throw new ArtifactAccessError("Invalid artifact path.", 400);
|
|
127
|
+
try {
|
|
128
|
+
const realRoot = await checkedRoot(root);
|
|
129
|
+
const candidate = node_path.default.resolve(realRoot, requested);
|
|
130
|
+
if (!inside(realRoot, candidate)) throw new ArtifactAccessError("The artifact is outside this session folder.", 403);
|
|
131
|
+
let current = realRoot;
|
|
132
|
+
const segments = requested.split("/");
|
|
133
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
134
|
+
current = node_path.default.join(current, segments[index]);
|
|
135
|
+
const stat = await node_fs_promises.default.lstat(current);
|
|
136
|
+
if (stat.isSymbolicLink()) throw new ArtifactAccessError("Artifact symlinks cannot be read.", 403);
|
|
137
|
+
if (index < segments.length - 1 ? !stat.isDirectory() : !stat.isFile()) throw new ArtifactAccessError("The artifact is not a regular file.", 404);
|
|
138
|
+
}
|
|
139
|
+
const real = await node_fs_promises.default.realpath(candidate);
|
|
140
|
+
if (!inside(realRoot, real)) throw new ArtifactAccessError("The artifact is outside this session folder.", 403);
|
|
141
|
+
const expected = await node_fs_promises.default.lstat(real);
|
|
142
|
+
const handle = await openContained(realRoot, segments);
|
|
143
|
+
try {
|
|
144
|
+
const stat = await handle.stat();
|
|
145
|
+
if (!stat.isFile() || stat.dev !== expected.dev || stat.ino !== expected.ino) throw new ArtifactAccessError("The artifact changed while it was opened. Try again.", 403);
|
|
146
|
+
return {
|
|
147
|
+
handle,
|
|
148
|
+
stat,
|
|
149
|
+
path: requested,
|
|
150
|
+
mimeType: artifactMimeType(requested)
|
|
151
|
+
};
|
|
152
|
+
} catch (error) {
|
|
153
|
+
await handle.close();
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof ArtifactAccessError) throw error;
|
|
158
|
+
if (codeOf(error) === "ENOENT" || codeOf(error) === "ENOTDIR") throw new ArtifactAccessError("The artifact no longer exists.", 404);
|
|
159
|
+
if (codeOf(error) === "EACCES" || codeOf(error) === "EPERM" || codeOf(error) === "ELOOP") throw new ArtifactAccessError("The artifact cannot be read.", 403);
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Bound allocation and read length even when the file grows after its metadata was read. */
|
|
164
|
+
async function readArtifactBytes(handle, size, offset = 0) {
|
|
165
|
+
const bytes = new Uint8Array(size);
|
|
166
|
+
let read = 0;
|
|
167
|
+
while (read < size) {
|
|
168
|
+
const result = await handle.read(bytes, read, size - read, offset + read);
|
|
169
|
+
if (result.bytesRead === 0) break;
|
|
170
|
+
read += result.bytesRead;
|
|
171
|
+
}
|
|
172
|
+
return bytes.subarray(0, read);
|
|
173
|
+
}
|
|
174
|
+
async function listArtifacts(root) {
|
|
175
|
+
const items = [];
|
|
176
|
+
try {
|
|
177
|
+
const realRoot = await checkedRoot(root);
|
|
178
|
+
const visit = async (directory) => {
|
|
179
|
+
for (const entry of await node_fs_promises.default.readdir(directory, { withFileTypes: true })) {
|
|
180
|
+
const location = node_path.default.join(directory, entry.name);
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
const stat = await node_fs_promises.default.lstat(location);
|
|
183
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
184
|
+
const real = await node_fs_promises.default.realpath(location);
|
|
185
|
+
if (inside(realRoot, real)) await visit(real);
|
|
186
|
+
} else if (entry.isFile()) {
|
|
187
|
+
const relativePath = node_path.default.relative(realRoot, location).split(node_path.default.sep).join("/");
|
|
188
|
+
try {
|
|
189
|
+
const file = await openArtifact(root, relativePath);
|
|
190
|
+
try {
|
|
191
|
+
items.push({
|
|
192
|
+
path: relativePath,
|
|
193
|
+
name: entry.name,
|
|
194
|
+
size: file.stat.size,
|
|
195
|
+
modifiedAt: file.stat.mtimeMs
|
|
196
|
+
});
|
|
197
|
+
} finally {
|
|
198
|
+
await file.handle.close();
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (error instanceof ArtifactAccessError && (error.status === 404 || error.status === 403)) continue;
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
await visit(realRoot);
|
|
208
|
+
items.sort((left, right) => right.modifiedAt - left.modifiedAt || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
209
|
+
return { items };
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (codeOf(error) === "ENOENT") return { items: [] };
|
|
212
|
+
return {
|
|
213
|
+
items: [],
|
|
214
|
+
error: error instanceof Error ? error.message : "The artifact folder could not be read."
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
Object.defineProperty(exports, "ARTIFACT_REFRESH_MS", {
|
|
220
|
+
enumerable: true,
|
|
221
|
+
get: function() {
|
|
222
|
+
return ARTIFACT_REFRESH_MS;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
Object.defineProperty(exports, "ArtifactAccessError", {
|
|
226
|
+
enumerable: true,
|
|
227
|
+
get: function() {
|
|
228
|
+
return ArtifactAccessError;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
Object.defineProperty(exports, "MAX_CONTENT_BYTES", {
|
|
232
|
+
enumerable: true,
|
|
233
|
+
get: function() {
|
|
234
|
+
return MAX_CONTENT_BYTES;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
Object.defineProperty(exports, "MAX_PREVIEW_BYTES", {
|
|
238
|
+
enumerable: true,
|
|
239
|
+
get: function() {
|
|
240
|
+
return MAX_PREVIEW_BYTES;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
Object.defineProperty(exports, "__toESM", {
|
|
244
|
+
enumerable: true,
|
|
245
|
+
get: function() {
|
|
246
|
+
return __toESM;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
Object.defineProperty(exports, "artifactRoot", {
|
|
250
|
+
enumerable: true,
|
|
251
|
+
get: function() {
|
|
252
|
+
return artifactRoot;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
Object.defineProperty(exports, "ensureArtifactRoot", {
|
|
256
|
+
enumerable: true,
|
|
257
|
+
get: function() {
|
|
258
|
+
return ensureArtifactRoot;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
Object.defineProperty(exports, "listArtifacts", {
|
|
262
|
+
enumerable: true,
|
|
263
|
+
get: function() {
|
|
264
|
+
return listArtifacts;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
Object.defineProperty(exports, "openArtifact", {
|
|
268
|
+
enumerable: true,
|
|
269
|
+
get: function() {
|
|
270
|
+
return openArtifact;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
Object.defineProperty(exports, "readArtifactBytes", {
|
|
274
|
+
enumerable: true,
|
|
275
|
+
get: function() {
|
|
276
|
+
return readArtifactBytes;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
//# sourceMappingURL=artifactFiles-Bo5Mhwq0.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifactFiles-Bo5Mhwq0.cjs","names":["path","piAgentDirectory","path","fs","constants"],"sources":["../src/constants/artifacts.ts","../src/services/artifactFiles/artifactPaths.ts","../src/services/artifactFiles/index.ts"],"sourcesContent":["export const ARTIFACT_DIRECTORY = 'doom-artifacts';\nexport const MAX_PREVIEW_BYTES = 1024 * 1024;\nexport const MAX_CONTENT_BYTES = 25 * 1024 * 1024;\nexport const ARTIFACT_REFRESH_MS = 2000;\n\nexport const ARTIFACT_CONTENT_TYPES: Readonly<Record<string, string>> = {\n '.avif': 'image/avif',\n '.bmp': 'image/bmp',\n '.gif': 'image/gif',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.webp': 'image/webp',\n '.ico': 'image/vnd.microsoft.icon',\n '.mp3': 'audio/mpeg',\n '.wav': 'audio/wav',\n '.m4a': 'audio/mp4',\n '.ogg': 'audio/ogg',\n '.m4v': 'video/mp4',\n '.mov': 'video/quicktime',\n '.mp4': 'video/mp4',\n '.webm': 'video/webm',\n '.pdf': 'application/pdf',\n '.md': 'text/markdown; charset=utf-8',\n '.markdown': 'text/markdown; charset=utf-8',\n '.txt': 'text/plain; charset=utf-8',\n '.html': 'text/html; charset=utf-8',\n '.htm': 'text/html; charset=utf-8',\n};\n","import path from 'node:path';\n\nimport { piAgentDirectory } from '@agimon-ai/po-core/piSettings';\n\nimport { ARTIFACT_CONTENT_TYPES, ARTIFACT_DIRECTORY } from '../../constants/artifacts';\n\nexport class ArtifactAccessError extends Error {\n constructor(\n message: string,\n readonly status: 400 | 403 | 404 | 413,\n ) {\n super(message);\n }\n}\n\nexport function artifactRoot(sessionId: string, environment: Readonly<Record<string, string | undefined>>): string {\n if (!/^[a-zA-Z0-9_-]+$/u.test(sessionId)) throw new ArtifactAccessError('Invalid artifact session identity.', 400);\n return path.join(piAgentDirectory(environment, environment.HOME), ARTIFACT_DIRECTORY, sessionId);\n}\n\nexport function artifactMimeType(relativePath: string): string {\n return ARTIFACT_CONTENT_TYPES[path.extname(relativePath).toLowerCase()] ?? 'application/octet-stream';\n}\n","import { constants } from 'node:fs';\nimport fs from 'node:fs/promises';\nimport path from 'node:path';\n\nimport type { ArtifactItem, ArtifactSnapshot } from '../../types/artifacts';\nimport { ArtifactAccessError, artifactMimeType } from './artifactPaths';\n\nexport { ArtifactAccessError, artifactMimeType, artifactRoot } from './artifactPaths';\n\nfunction inside(root: string, candidate: string): boolean {\n const relative = path.relative(root, candidate);\n return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);\n}\n\nfunction codeOf(error: unknown): string | undefined {\n return error instanceof Error && 'code' in error ? String(error.code) : undefined;\n}\n\n/** The owned directories must not redirect artifact access through a symlink. */\nasync function checkedRoot(root: string): Promise<string> {\n const base = path.dirname(root);\n for (const directory of [base, root]) {\n const stat = await fs.lstat(directory);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new ArtifactAccessError('The artifact folder is unavailable.', 403);\n }\n const realBase = await fs.realpath(base);\n const realRoot = await fs.realpath(root);\n if (!inside(realBase, realRoot))\n throw new ArtifactAccessError('The artifact folder is outside its storage directory.', 403);\n return realRoot;\n}\n\nexport async function ensureArtifactRoot(root: string): Promise<void> {\n await fs.mkdir(path.dirname(root), { recursive: true, mode: 0o700 });\n await fs.mkdir(root, { recursive: true, mode: 0o700 });\n await checkedRoot(root);\n}\n\n/** Open without following mutable ancestors. Node exposes no portable openat API. */\nasync function openContained(root: string, segments: string[]): Promise<fs.FileHandle> {\n if (process.platform === 'darwin') {\n // Darwin O_NOFOLLOW_ANY checks every component atomically. Do not combine it with O_NOFOLLOW.\n return fs.open(path.join(root, ...segments), constants.O_RDONLY | constants.O_NONBLOCK | 0x20000000);\n }\n if (process.platform !== 'linux') throw new ArtifactAccessError('Secure artifact reads require macOS or Linux.', 403);\n const flags = constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW;\n let directory = await fs.open(path.dirname(root), flags);\n try {\n for (const segment of [path.basename(root), ...segments.slice(0, -1)]) {\n const child = await fs.open(`/proc/self/fd/${directory.fd}/${segment}`, flags);\n await directory.close();\n directory = child;\n }\n return await fs.open(\n `/proc/self/fd/${directory.fd}/${segments.at(-1)!}`,\n constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,\n );\n } finally {\n await directory.close();\n }\n}\n\n/** One guarded open shared by metadata, text and byte reads. The caller closes the handle. */\nexport async function openArtifact(root: string, requested: string | undefined) {\n if (\n !requested ||\n requested.includes('\\0') ||\n requested.includes('\\\\') ||\n path.isAbsolute(requested) ||\n requested.split('/').some((part) => part === '..' || part === '.' || part === '')\n )\n throw new ArtifactAccessError('Invalid artifact path.', 400);\n try {\n const realRoot = await checkedRoot(root);\n const candidate = path.resolve(realRoot, requested);\n if (!inside(realRoot, candidate))\n throw new ArtifactAccessError('The artifact is outside this session folder.', 403);\n let current = realRoot;\n const segments = requested.split('/');\n for (let index = 0; index < segments.length; index += 1) {\n current = path.join(current, segments[index]!);\n const stat = await fs.lstat(current);\n if (stat.isSymbolicLink()) throw new ArtifactAccessError('Artifact symlinks cannot be read.', 403);\n if (index < segments.length - 1 ? !stat.isDirectory() : !stat.isFile())\n throw new ArtifactAccessError('The artifact is not a regular file.', 404);\n }\n const real = await fs.realpath(candidate);\n if (!inside(realRoot, real)) throw new ArtifactAccessError('The artifact is outside this session folder.', 403);\n const expected = await fs.lstat(real);\n const handle = await openContained(realRoot, segments);\n try {\n const stat = await handle.stat();\n if (!stat.isFile() || stat.dev !== expected.dev || stat.ino !== expected.ino)\n throw new ArtifactAccessError('The artifact changed while it was opened. Try again.', 403);\n return { handle, stat, path: requested, mimeType: artifactMimeType(requested) };\n } catch (error) {\n await handle.close();\n throw error;\n }\n } catch (error) {\n if (error instanceof ArtifactAccessError) throw error;\n if (codeOf(error) === 'ENOENT' || codeOf(error) === 'ENOTDIR')\n throw new ArtifactAccessError('The artifact no longer exists.', 404);\n if (codeOf(error) === 'EACCES' || codeOf(error) === 'EPERM' || codeOf(error) === 'ELOOP')\n throw new ArtifactAccessError('The artifact cannot be read.', 403);\n throw error;\n }\n}\n\n/** Bound allocation and read length even when the file grows after its metadata was read. */\nexport async function readArtifactBytes(handle: fs.FileHandle, size: number, offset = 0): Promise<Uint8Array> {\n const bytes = new Uint8Array(size);\n let read = 0;\n while (read < size) {\n const result = await handle.read(bytes, read, size - read, offset + read);\n if (result.bytesRead === 0) break;\n read += result.bytesRead;\n }\n return bytes.subarray(0, read);\n}\n\nexport async function listArtifacts(root: string): Promise<ArtifactSnapshot> {\n const items: ArtifactItem[] = [];\n try {\n const realRoot = await checkedRoot(root);\n const visit = async (directory: string): Promise<void> => {\n for (const entry of await fs.readdir(directory, { withFileTypes: true })) {\n const location = path.join(directory, entry.name);\n if (entry.isDirectory()) {\n // Recheck each directory rather than trusting a readdir entry across a rename.\n const stat = await fs.lstat(location);\n if (!stat.isDirectory() || stat.isSymbolicLink()) continue;\n const real = await fs.realpath(location);\n if (inside(realRoot, real)) await visit(real);\n } else if (entry.isFile()) {\n const relativePath = path.relative(realRoot, location).split(path.sep).join('/');\n try {\n const file = await openArtifact(root, relativePath);\n try {\n items.push({ path: relativePath, name: entry.name, size: file.stat.size, modifiedAt: file.stat.mtimeMs });\n } finally {\n await file.handle.close();\n }\n } catch (error) {\n if (error instanceof ArtifactAccessError && (error.status === 404 || error.status === 403)) continue;\n throw error;\n }\n }\n }\n };\n await visit(realRoot);\n items.sort(\n (left, right) =>\n right.modifiedAt - left.modifiedAt || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0),\n );\n return { items };\n } catch (error) {\n if (codeOf(error) === 'ENOENT') return { items: [] };\n return { items: [], error: error instanceof Error ? error.message : 'The artifact folder could not be read.' };\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_artifactFiles = require("./artifactFiles-Bo5Mhwq0.cjs");
|
|
3
|
+
exports.ArtifactAccessError = require_artifactFiles.ArtifactAccessError;
|
|
4
|
+
exports.artifactRoot = require_artifactFiles.artifactRoot;
|
|
5
|
+
exports.openArtifact = require_artifactFiles.openArtifact;
|
|
6
|
+
exports.readArtifactBytes = require_artifactFiles.readArtifactBytes;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
//#region src/services/artifactFiles/artifactPaths.d.ts
|
|
3
|
+
export declare class ArtifactAccessError extends Error {
|
|
4
|
+
readonly status: 400 | 403 | 404 | 413;
|
|
5
|
+
constructor(message: string, status: 400 | 403 | 404 | 413);
|
|
6
|
+
}
|
|
7
|
+
export declare function artifactRoot(sessionId: string, environment: Readonly<Record<string, string | undefined>>): string;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/services/artifactFiles/index.d.ts
|
|
10
|
+
/** One guarded open shared by metadata, text and byte reads. The caller closes the handle. */
|
|
11
|
+
export declare function openArtifact(root: string, requested: string | undefined): Promise<{
|
|
12
|
+
handle: fs.FileHandle;
|
|
13
|
+
stat: import("node:fs").Stats;
|
|
14
|
+
path: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** Bound allocation and read length even when the file grows after its metadata was read. */
|
|
18
|
+
export declare function readArtifactBytes(handle: fs.FileHandle, size: number, offset?: number): Promise<Uint8Array>;
|
|
19
|
+
//#endregion
|
|
20
|
+
//# sourceMappingURL=artifactFiles.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifactFiles.d.mts","names":[],"sources":["../src/services/artifactFiles/artifactPaths.ts","../src/services/artifactFiles/index.ts"],"mappings":";;qBAMa,4BAA4B;WAG5B;EAFX,YACE,iBACS;;wBAMG,aAAa,mBAAmB,aAAa,SAAS;;;;wBCiDhD,aAAa,cAAc,gCAA6B;UAAxD,GAAA;;;;;;wBA+CA,kBAAkB,QAAQ,GAAG,YAAY,cAAc,kBAAa,QAAQ"}
|