@alisio/server 0.1.0-alpha.5 → 0.1.0-alpha.7
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/auth/view-token.d.ts +16 -0
- package/dist/auth/view-token.js +36 -0
- package/dist/bridges/approval-bridge.js +4 -0
- package/dist/host/presets.js +4 -0
- package/dist/host/run-scheduler.d.ts +12 -2
- package/dist/host/run-scheduler.js +16 -7
- package/dist/host/sessions.js +1 -0
- package/dist/host/workspace-host.js +3 -3
- package/dist/http/errors.js +6 -0
- package/dist/http/router.d.ts +4 -1
- package/dist/http/router.js +30 -9
- package/dist/index.js +67 -5
- package/dist/routes/artifact-view.d.ts +17 -0
- package/dist/routes/artifact-view.js +78 -0
- package/dist/routes/artifacts.d.ts +26 -0
- package/dist/routes/artifacts.js +261 -0
- package/dist/routes/capabilities.d.ts +13 -0
- package/dist/routes/capabilities.js +20 -0
- package/dist/routes/commands.js +24 -0
- package/dist/routes/datasets.d.ts +14 -0
- package/dist/routes/datasets.js +239 -0
- package/dist/routes/files.js +24 -16
- package/dist/routes/management.js +25 -3
- package/dist/routes/prompts.js +26 -3
- package/dist/sse/inflight.js +4 -0
- package/dist/web/assets/AgentPicker-DB8mxbbL.js +1 -0
- package/dist/web/assets/AgentsModal-RvCfWb-2.js +2 -0
- package/dist/web/assets/ArtifactPanel-BO4wIe9q.js +2 -0
- package/dist/web/assets/ArtifactPanel-Cfi5Lgi8.css +1 -0
- package/dist/web/assets/BtwPanel-BQN5XxKF.js +1 -0
- package/dist/web/assets/SettingsModal--RT4zb3v.js +1 -0
- package/dist/web/assets/SpreadsheetPanel-C2XdUSSC.js +1 -0
- package/dist/web/assets/SpreadsheetPanel-CYyWl0en.css +1 -0
- package/dist/web/assets/{abnfDiagram-VCTEODGH-Cz5hn5ec.js → abnfDiagram-VCTEODGH-BNuKZo1p.js} +1 -1
- package/dist/web/assets/{agents.module-lYyQTI6Z.js → agents.module-D1wKvaD-.js} +1 -1
- package/dist/web/assets/app-DP2KtZ7m.js +2 -0
- package/dist/web/assets/architecture-7GRP2DOG-Bxp-PFvl.js +1 -0
- package/dist/web/assets/{architectureDiagram-5GKGNRK7-CSWMHXJ1.js → architectureDiagram-5GKGNRK7-Dt_V8yqa.js} +1 -1
- package/dist/web/assets/{blockDiagram-I7D4REHJ-CV034Tti.js → blockDiagram-I7D4REHJ-NxVOhPmn.js} +1 -1
- package/dist/web/assets/{c4Diagram-7LVT6UL2-DxMgBNf1.js → c4Diagram-7LVT6UL2-DRi00rDC.js} +1 -1
- package/dist/web/assets/channel-CNDX-cbg.js +1 -0
- package/dist/web/assets/{chunk-4HAMMTFA-CT8liele.js → chunk-4HAMMTFA-DmLMKdyN.js} +1 -1
- package/dist/web/assets/{chunk-75Z2AOVW-DFYQUeQA.js → chunk-75Z2AOVW-BR8-_T1K.js} +1 -1
- package/dist/web/assets/{chunk-DU6HZSFF-BJuBFcwd.js → chunk-DU6HZSFF-CgtANb4X.js} +1 -1
- package/dist/web/assets/{chunk-F27PBJKO-DQKhFLQx.js → chunk-F27PBJKO-CEslv91G.js} +1 -1
- package/dist/web/assets/{chunk-GMAD6QVW-DOqNcbFg.js → chunk-GMAD6QVW-ajMpCrE1.js} +1 -1
- package/dist/web/assets/{chunk-GVQU2GXP-uaJ9Q1Go.js → chunk-GVQU2GXP-Cxv3ClOW.js} +1 -1
- package/dist/web/assets/{chunk-IMKFNOWR-C85sNyla.js → chunk-IMKFNOWR-BrT27IpP.js} +1 -1
- package/dist/web/assets/{chunk-L3NEJ4N5-VqLWue5E.js → chunk-L3NEJ4N5-IF_NnfIM.js} +1 -1
- package/dist/web/assets/{chunk-OSK3NFVY-CpDZTLO2.js → chunk-OSK3NFVY-CTNQzsaZ.js} +1 -1
- package/dist/web/assets/{chunk-P2QGCYS3-8aJ3lrqy.js → chunk-P2QGCYS3-C8Mv-VXk.js} +1 -1
- package/dist/web/assets/{chunk-POPQ4Y6H-CCGI6bTT.js → chunk-POPQ4Y6H-DEyZliGx.js} +1 -1
- package/dist/web/assets/{chunk-PWAF6VOD-BzM7cl8Q.js → chunk-PWAF6VOD-DYoILyGR.js} +1 -1
- package/dist/web/assets/{chunk-SHT3W25Y-DB0C0Tk9.js → chunk-SHT3W25Y-BPA5CAVG.js} +1 -1
- package/dist/web/assets/{chunk-SVP7TREG-CaN0-mdN.js → chunk-SVP7TREG-DuiGU0RH.js} +1 -1
- package/dist/web/assets/{chunk-TICWLB2K-C0M4j05X.js → chunk-TICWLB2K-CPsLIDl-.js} +1 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-BfcenY86.js +1 -0
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-BfcenY86.js +1 -0
- package/dist/web/assets/clipboard-hOiC7lD8.js +1 -0
- package/dist/web/assets/{cynefin-OW5HDTMX-r5xVJ9v1.js → cynefin-OW5HDTMX-CfgsLEx3.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-5FMLGOSQ-CkEacFFF.js → cynefinDiagram-5FMLGOSQ-CF2Eh0TH.js} +1 -1
- package/dist/web/assets/{dagre-GXQ25YYZ-Cpg1w4D6.js → dagre-GXQ25YYZ-DYyidWnQ.js} +1 -1
- package/dist/web/assets/{diagram-S7CK7UJ4-CxzdVKBq.js → diagram-S7CK7UJ4-DSBPkUpu.js} +1 -1
- package/dist/web/assets/{diagram-UQ7AKVKN-BtZLF6HT.js → diagram-UQ7AKVKN-V5xEYTHc.js} +1 -1
- package/dist/web/assets/{diagram-VSXAHHWV-fSwq53_o.js → diagram-VSXAHHWV-BECaFuc3.js} +1 -1
- package/dist/web/assets/{diagram-VX7I27RA-p_z4ar7w.js → diagram-VX7I27RA-21c8snpi.js} +1 -1
- package/dist/web/assets/{diagram-Z3DM3KII-DvIxMDwh.js → diagram-Z3DM3KII-C35HmHNH.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-PWID7BFC-kUnSJleQ.js → ebnfDiagram-PWID7BFC-CITGP-Q9.js} +1 -1
- package/dist/web/assets/{engine-CuFplYJ5.js → engine-DUxyrccI.js} +4 -4
- package/dist/web/assets/{erDiagram-RLTQ6QDP-BRDWsV60.js → erDiagram-RLTQ6QDP-BP6F-Cwl.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-50pr2V7m.js +1 -0
- package/dist/web/assets/flowDiagram-HODETNUW-ftQQEWxR.js +1 -0
- package/dist/web/assets/{ganttDiagram-EL5Y4UJY-DUOezeyk.js → ganttDiagram-EL5Y4UJY-Cakc1RCe.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-DdGj4H6p.js → gitGraph-4MIJSDKK-CHB16fZf.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-WWUBYQGX-DQ_wu8-N.js → gitGraphDiagram-WWUBYQGX-CTd8KVPV.js} +1 -1
- package/dist/web/assets/{highlight-Coezwnqq.js → highlight-DJEaplpK.js} +1 -1
- package/dist/web/assets/icons-dCLC5DFa.js +1 -0
- package/dist/web/assets/index-CRb-14T4.css +1 -0
- package/dist/web/assets/index-lCAk_Rn7.js +74 -0
- package/dist/web/assets/{info-A6RAGUB7-bzlyOEbS.js → info-A6RAGUB7-BCmSXR4J.js} +1 -1
- package/dist/web/assets/{infoDiagram-27XIBGKW-DMPIkM67.js → infoDiagram-27XIBGKW-kmLXHL0B.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-5VMMS53U-Dn1_RFxe.js → ishikawaDiagram-5VMMS53U-T3d1OORv.js} +1 -1
- package/dist/web/assets/{journeyDiagram-3NMN7TZE-BZNgHBt7.js → journeyDiagram-3NMN7TZE-IRnndJxa.js} +1 -1
- package/dist/web/assets/jsxRuntime-DJRCfs_u.js +5 -0
- package/dist/web/assets/{kanban-definition-UXKFOSKX-5UeB0sIz.js → kanban-definition-UXKFOSKX-D81-nCTe.js} +1 -1
- package/dist/web/assets/{line-CESetX3B.js → line-BBeV3vfV.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-9beuOH8q.js → mermaid-parser.core-xZ2WHlRI.js} +3 -3
- package/dist/web/assets/{mindmap-definition-YA3MSWOX-BgWvbpzs.js → mindmap-definition-YA3MSWOX-Ho6e6jVr.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-Clm_VEbL.js → packet-AYTQ26CC-B9pSVRWc.js} +1 -1
- package/dist/web/assets/{pegDiagram-XKGWAZYB-D78fyGXR.js → pegDiagram-XKGWAZYB-CTXTBWF0.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-BjnIB8bI.js → pie-WAS4IAKB-Bga9YnnI.js} +1 -1
- package/dist/web/assets/{pieDiagram-E7YTZNPT-k3CAPrzW.js → pieDiagram-E7YTZNPT-DVVSLq99.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-AXDQQJYC-Cj3pONTC.js → quadrantDiagram-AXDQQJYC-B83E3PUh.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-BHTSDlOu.js → radar-RG4KPBEZ-u8zRwuLC.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-D2sY5w8h.js → railroad-74A4TZTK-D9v6vBft.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-DkFG-50O.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-CBTuWFTA.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-CdSnH_fA.js +1 -0
- package/dist/web/assets/{railroadDiagram-O6MQD6OU-De99_Cdp.js → railroadDiagram-O6MQD6OU-qU8L_T9G.js} +1 -1
- package/dist/web/assets/{requirementDiagram-BXWQKSXE-plwNdlUu.js → requirementDiagram-BXWQKSXE-DbGVadzP.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-P5KCCOFB-BLX5pbzb.js → sankeyDiagram-P5KCCOFB-DjF_6wxm.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-WJ2MYXX4-D5L3ResV.js → sequenceDiagram-WJ2MYXX4-ACVlOZ_E.js} +1 -1
- package/dist/web/assets/shared-BaCxhGEp.js +1 -0
- package/dist/web/assets/{stateDiagram-D77RDMKH-B_PLcigs.js → stateDiagram-D77RDMKH-gO6dN_6c.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-Bmk6g5Wx.js +1 -0
- package/dist/web/assets/{swimlanes-42K2YHIH-BCoy3JfH.js → swimlanes-42K2YHIH-DChgukPo.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-CPdgOMx5.js +8 -0
- package/dist/web/assets/{timeline-definition-24CTP7MA-Dz0_qjiL.js → timeline-definition-24CTP7MA-BOJKAXQm.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-rMSggC0A.js → treeView-Q6P3EWNA-DDk_V5T-.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-DSgqT8Si.js → treemap-WGGIJYW6-gSJ9NeJF.js} +1 -1
- package/dist/web/assets/{vennDiagram-4TSXK5OY-OZgwexj9.js → vennDiagram-4TSXK5OY-pztSs7q3.js} +1 -1
- package/dist/web/assets/view-BByXcA5W.js +1 -0
- package/dist/web/assets/view-BOCBCjVE.js +2 -0
- package/dist/web/assets/view-CuotKs3K.js +1 -0
- package/dist/web/assets/{view-DCdLCLXZ.js → view-D1FOOnH2.js} +2 -2
- package/dist/web/assets/{view-DCcRnXKN.js → view-DL9H44DY.js} +1 -1
- package/dist/web/assets/{view-PaHzu9kV.js → view-DzGOmq2Y.js} +2 -2
- package/dist/web/assets/view-Lqn7oIjv.js +1 -0
- package/dist/web/assets/{view-BYuuM7qn.js → view-wUX2CVZB.js} +1 -1
- package/dist/web/assets/view-xJC-nbhd.js +1 -0
- package/dist/web/assets/{wardley-WFR3VGLG-BXO-hnJx.js → wardley-WFR3VGLG-CZ8Fuv6x.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VM6X3IG4-CJ_kVeKc.js → wardleyDiagram-VM6X3IG4-BPDY9aCw.js} +1 -1
- package/dist/web/assets/{xychartDiagram-S5SC5T6Z-DULIcOoX.js → xychartDiagram-S5SC5T6Z-DTGZgbYf.js} +1 -1
- package/dist/web/index.html +6 -4
- package/package.json +3 -3
- package/dist/web/assets/AgentPicker-vmCkZAEg.js +0 -1
- package/dist/web/assets/AgentsModal-SDE7h8Ov.js +0 -2
- package/dist/web/assets/BtwPanel-C3is5T--.js +0 -1
- package/dist/web/assets/SettingsModal-CcH378D6.js +0 -1
- package/dist/web/assets/architecture-7GRP2DOG-Bsf7Btg2.js +0 -1
- package/dist/web/assets/channel-DagxYswy.js +0 -1
- package/dist/web/assets/classDiagram-ZZMXUADV-DKXTlMcs.js +0 -1
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-DKXTlMcs.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-B9LemJL3.js +0 -1
- package/dist/web/assets/flowDiagram-HODETNUW-Dr1bc3bC.js +0 -1
- package/dist/web/assets/icons-BuSI35ng.js +0 -1
- package/dist/web/assets/index-DFURIQeX.js +0 -75
- package/dist/web/assets/index-Duk7lhBO.css +0 -1
- package/dist/web/assets/jsxRuntime-B237YiZZ.js +0 -5
- package/dist/web/assets/railroad-abnf-HS5TGJTU-By7JhmIc.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-XSOf4PPv.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-Bj0KcXv3.js +0 -1
- package/dist/web/assets/shared-sr65xU-a.js +0 -1
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-Cgy-1bb9.js +0 -1
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-DECB5qyv.js +0 -8
- package/dist/web/assets/view-0w3H4M8J.js +0 -2
- package/dist/web/assets/view-CFknHmXR.js +0 -1
- package/dist/web/assets/view-CtWHa3Z4.js +0 -1
- package/dist/web/assets/view-DMJ8HX8Y.js +0 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Default lifetime of a view link (renewable with another `POST /api/artifacts/:aid/view`). */
|
|
2
|
+
export declare const VIEW_TOKEN_TTL_MS: number;
|
|
3
|
+
/** A token for `artifactId` valid until `expiresAt` (epoch ms). */
|
|
4
|
+
export declare function signViewToken(secret: string, artifactId: string, expiresAt: number): string;
|
|
5
|
+
export type ViewTokenCheck = {
|
|
6
|
+
ok: true;
|
|
7
|
+
artifactId: string;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: "malformed" | "signature" | "expired";
|
|
12
|
+
};
|
|
13
|
+
/** Verifies a token's signature (constant time) and expiry. */
|
|
14
|
+
export declare function verifyViewToken(secret: string, token: string, now?: number): ViewTokenCheck;
|
|
15
|
+
/** A request path with the view token replaced, for logs (tokens are never logged). */
|
|
16
|
+
export declare const redactViewPath: (pathname: string) => string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed view tokens of the isolated artifact viewer (spec §14.2): `base64url(artifactId ·
|
|
3
|
+
* expiresAt · HMAC-SHA256(secret, artifactId‖expiresAt))`. A token names one artifact, expires
|
|
4
|
+
* after a short time and is useless with any other secret (a fresh one per server process).
|
|
5
|
+
*/
|
|
6
|
+
import { createHmac } from "node:crypto";
|
|
7
|
+
import { safeEqual } from "./token.js";
|
|
8
|
+
/** Default lifetime of a view link (renewable with another `POST /api/artifacts/:aid/view`). */
|
|
9
|
+
export const VIEW_TOKEN_TTL_MS = 10 * 60_000;
|
|
10
|
+
const ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11
|
+
const mac = (secret, artifactId, expiresAt) => createHmac("sha256", secret).update(`${artifactId}\n${expiresAt}`).digest("base64url");
|
|
12
|
+
/** A token for `artifactId` valid until `expiresAt` (epoch ms). */
|
|
13
|
+
export function signViewToken(secret, artifactId, expiresAt) {
|
|
14
|
+
if (!ID.test(artifactId))
|
|
15
|
+
throw new Error("Invalid artifact id");
|
|
16
|
+
return Buffer.from(`${artifactId}.${expiresAt}.${mac(secret, artifactId, expiresAt)}`, "utf8").toString("base64url");
|
|
17
|
+
}
|
|
18
|
+
/** Verifies a token's signature (constant time) and expiry. */
|
|
19
|
+
export function verifyViewToken(secret, token, now = Date.now()) {
|
|
20
|
+
if (!/^[A-Za-z0-9_-]{1,512}$/.test(token))
|
|
21
|
+
return { ok: false, reason: "malformed" };
|
|
22
|
+
const parts = Buffer.from(token, "base64url").toString("utf8").split(".");
|
|
23
|
+
if (parts.length !== 3)
|
|
24
|
+
return { ok: false, reason: "malformed" };
|
|
25
|
+
const [artifactId = "", expires = "", signature = ""] = parts;
|
|
26
|
+
const expiresAt = Number(expires);
|
|
27
|
+
if (!ID.test(artifactId) || !/^\d{1,16}$/.test(expires) || !Number.isSafeInteger(expiresAt))
|
|
28
|
+
return { ok: false, reason: "malformed" };
|
|
29
|
+
if (!safeEqual(signature, mac(secret, artifactId, expiresAt)))
|
|
30
|
+
return { ok: false, reason: "signature" };
|
|
31
|
+
if (now >= expiresAt)
|
|
32
|
+
return { ok: false, reason: "expired" };
|
|
33
|
+
return { ok: true, artifactId, expiresAt };
|
|
34
|
+
}
|
|
35
|
+
/** A request path with the view token replaced, for logs (tokens are never logged). */
|
|
36
|
+
export const redactViewPath = (pathname) => pathname.replace(/^\/artifact-view\/[^/]*/, "/artifact-view/[token]");
|
|
@@ -40,6 +40,10 @@ export class ApprovalBridge {
|
|
|
40
40
|
effect: request.effect,
|
|
41
41
|
...(request.label ? { label: request.label } : {}),
|
|
42
42
|
input: pretty(request.input),
|
|
43
|
+
...(request.capability ? { capability: request.capability } : {}),
|
|
44
|
+
...(request.preview !== undefined ? { preview: request.preview } : {}),
|
|
45
|
+
...(request.runtime ? { runtime: request.runtime } : {}),
|
|
46
|
+
...(request.install ? { install: request.install } : {}),
|
|
43
47
|
}, request.signal);
|
|
44
48
|
};
|
|
45
49
|
/** Passed as `AppOptions.approveExternalDirectory` for every workspace app. */
|
package/dist/host/presets.js
CHANGED
|
@@ -16,6 +16,7 @@ const FLAG = {
|
|
|
16
16
|
write: "--allow-write",
|
|
17
17
|
process: "--allow-process",
|
|
18
18
|
external: "--allow-external",
|
|
19
|
+
analysis: "--allow-analysis",
|
|
19
20
|
};
|
|
20
21
|
/**
|
|
21
22
|
* A preset narrowed by the ceiling: an effect the launch flags do not allow degrades to asking
|
|
@@ -27,6 +28,9 @@ export function presetInfo(id, ceiling) {
|
|
|
27
28
|
write: wanted.policy.write && ceiling.policy.write,
|
|
28
29
|
process: wanted.policy.process && ceiling.policy.process,
|
|
29
30
|
external: wanted.policy.external && ceiling.policy.external,
|
|
31
|
+
// `alisio serve --allow-analysis` pre-allows Python analysis where the preset allows
|
|
32
|
+
// processes (full-access); elsewhere python_run asks.
|
|
33
|
+
...(wanted.policy.process && ceiling.policy.analysis ? { analysis: true } : {}),
|
|
30
34
|
};
|
|
31
35
|
if (ceiling.readOnly && id !== "read-only")
|
|
32
36
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Attachment } from "@alisio/sdk";
|
|
1
|
+
import type { Attachment, DatasetRef } from "@alisio/sdk";
|
|
2
2
|
import type { Application } from "./workspace-host.ts";
|
|
3
3
|
type RunOptions = NonNullable<Parameters<Application["runner"]["run"]>[3]>;
|
|
4
4
|
/** One prompt accepted by the server: a `runs` row in `queued` until a slot is free. */
|
|
@@ -10,9 +10,19 @@ export interface RunJob {
|
|
|
10
10
|
text: string;
|
|
11
11
|
correlationId: string;
|
|
12
12
|
/** Built when the run starts, so preset and effort changes while queued apply. */
|
|
13
|
-
options: () => Omit<RunOptions, "runId" | "correlationId" | "attachments">;
|
|
13
|
+
options: () => Omit<RunOptions, "runId" | "correlationId" | "attachments" | "datasets">;
|
|
14
14
|
attachments?: Attachment[];
|
|
15
|
+
/** Datasets attached to the prompt (chips); their summary is already part of `text`. */
|
|
16
|
+
datasets?: DatasetRef[];
|
|
15
17
|
display?: string;
|
|
18
|
+
/**
|
|
19
|
+
* A tool call started from the UI instead of a prompt (`runToolCall`: no model, same gates),
|
|
20
|
+
* e.g. copying an artifact into the workspace. `text` is ignored for these jobs.
|
|
21
|
+
*/
|
|
22
|
+
tool?: {
|
|
23
|
+
name: string;
|
|
24
|
+
input: Record<string, unknown>;
|
|
25
|
+
};
|
|
16
26
|
}
|
|
17
27
|
export interface RunSchedulerOptions {
|
|
18
28
|
/** Global semaphore across sessions and workspaces (RF-02, default 4). */
|
|
@@ -36,13 +36,22 @@ export class RunScheduler {
|
|
|
36
36
|
const { runner, store } = job.app;
|
|
37
37
|
let run;
|
|
38
38
|
try {
|
|
39
|
-
run =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
run = job.tool
|
|
40
|
+
? runner.runToolCall(job.sessionId, job.tool.name, job.tool.input, {
|
|
41
|
+
...job.options(),
|
|
42
|
+
runId: job.runId,
|
|
43
|
+
correlationId: job.correlationId,
|
|
44
|
+
signal: job.controller.signal,
|
|
45
|
+
...(job.display ? { display: job.display } : {}),
|
|
46
|
+
})
|
|
47
|
+
: runner.run(job.sessionId, job.text, job.controller.signal, {
|
|
48
|
+
...job.options(),
|
|
49
|
+
runId: job.runId,
|
|
50
|
+
correlationId: job.correlationId,
|
|
51
|
+
...(job.attachments?.length ? { attachments: job.attachments } : {}),
|
|
52
|
+
...(job.datasets?.length ? { datasets: job.datasets } : {}),
|
|
53
|
+
...(job.display ? { display: job.display } : {}),
|
|
54
|
+
});
|
|
46
55
|
}
|
|
47
56
|
catch (error) {
|
|
48
57
|
run = Promise.reject(error);
|
package/dist/host/sessions.js
CHANGED
|
@@ -21,6 +21,7 @@ export class SessionService {
|
|
|
21
21
|
write: !readOnly && !!base.allowWrite,
|
|
22
22
|
process: !readOnly && !!base.allowProcess,
|
|
23
23
|
external: !readOnly && (!!base.allowExternal || !!base.allowMcp || !!base.allowAgents),
|
|
24
|
+
...(!readOnly && base.allowAnalysis ? { analysis: true } : {}),
|
|
24
25
|
},
|
|
25
26
|
approvals: !readOnly,
|
|
26
27
|
readOnly,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { stat } from "node:fs/promises";
|
|
3
|
-
import { createApplication, findWorkspace, resolveTrust, } from "@alisio/core";
|
|
2
|
+
import { createApplication, findWorkspace, resolveTrust, workspaceKey, } from "@alisio/core";
|
|
4
3
|
import { HttpError } from "../http/errors.js";
|
|
5
4
|
const MISSING_ERRNO = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM", "ELOOP"]);
|
|
6
5
|
const missing = (path) => new HttpError("workspace_missing", `Workspace folder not found: ${path}`, { path });
|
|
@@ -14,7 +13,7 @@ export async function workspaceExists(path) {
|
|
|
14
13
|
}
|
|
15
14
|
}
|
|
16
15
|
/** Opaque, stable workspace id: a short sha256 of the canonical path (never a path in URLs). */
|
|
17
|
-
export const workspaceId = (path) =>
|
|
16
|
+
export const workspaceId = (path) => workspaceKey(path);
|
|
18
17
|
/**
|
|
19
18
|
* One lazily created `Application` per workspace (ADR-06), with LRU eviction of idle apps,
|
|
20
19
|
* a cap on open apps and trust resolved from the shared trust store (granted by the terminal
|
|
@@ -90,6 +89,7 @@ export class WorkspaceHost {
|
|
|
90
89
|
...base,
|
|
91
90
|
trustProject: explicit ? base.trustProject : trusted,
|
|
92
91
|
cwd: path,
|
|
92
|
+
approvalSource: "web",
|
|
93
93
|
...(this.options.wire?.(id, path) ?? {}),
|
|
94
94
|
});
|
|
95
95
|
}
|
package/dist/http/errors.js
CHANGED
|
@@ -28,6 +28,12 @@ export const STATUS = {
|
|
|
28
28
|
picker_unavailable: 503,
|
|
29
29
|
permission_denied: 403,
|
|
30
30
|
cancelled: 409,
|
|
31
|
+
artifact_not_found: 404,
|
|
32
|
+
artifact_too_large: 413,
|
|
33
|
+
runtime_unavailable: 503,
|
|
34
|
+
dataset_unsupported: 415,
|
|
35
|
+
query_rejected: 400,
|
|
36
|
+
query_timeout: 408,
|
|
31
37
|
internal: 500,
|
|
32
38
|
};
|
|
33
39
|
/** A typed API failure; routes throw it and the server renders it as an `ApiError` body. */
|
package/dist/http/router.d.ts
CHANGED
|
@@ -17,7 +17,10 @@ export type RouteResult = {
|
|
|
17
17
|
body: unknown;
|
|
18
18
|
} | undefined;
|
|
19
19
|
export type RouteHandler = (ctx: RouteContext) => Promise<RouteResult> | RouteResult;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Minimal method + path router with `:param` segments and an optional trailing `*` segment that
|
|
22
|
+
* captures the rest of the path (at least one segment) in `params["*"]`. No regex.
|
|
23
|
+
*/
|
|
21
24
|
export declare class Router {
|
|
22
25
|
private routes;
|
|
23
26
|
add(method: string, path: string, handler: RouteHandler): this;
|
package/dist/http/router.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Minimal method + path router with `:param` segments and an optional trailing `*` segment that
|
|
3
|
+
* captures the rest of the path (at least one segment) in `params["*"]`. No regex.
|
|
4
|
+
*/
|
|
2
5
|
export class Router {
|
|
3
6
|
routes = [];
|
|
4
7
|
add(method, path, handler) {
|
|
@@ -38,25 +41,43 @@ export class Router {
|
|
|
38
41
|
return pathMatched ? "method" : undefined;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
44
|
+
const decode = (segment) => {
|
|
45
|
+
try {
|
|
46
|
+
return decodeURIComponent(segment);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
41
52
|
function matchSegments(pattern, parts) {
|
|
42
|
-
|
|
53
|
+
const wildcard = pattern.at(-1) === "*";
|
|
54
|
+
const fixed = wildcard ? pattern.length - 1 : pattern.length;
|
|
55
|
+
if (wildcard ? parts.length <= fixed : parts.length !== fixed)
|
|
43
56
|
return undefined;
|
|
44
57
|
const params = {};
|
|
45
|
-
for (let i = 0; i <
|
|
58
|
+
for (let i = 0; i < fixed; i++) {
|
|
46
59
|
const expected = pattern[i];
|
|
47
60
|
const actual = parts[i];
|
|
48
61
|
if (expected.startsWith(":")) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
decoded = decodeURIComponent(actual);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
62
|
+
const decoded = decode(actual);
|
|
63
|
+
if (decoded === undefined)
|
|
54
64
|
return undefined;
|
|
55
|
-
}
|
|
56
65
|
params[expected.slice(1)] = decoded;
|
|
57
66
|
}
|
|
58
67
|
else if (expected !== actual)
|
|
59
68
|
return undefined;
|
|
60
69
|
}
|
|
70
|
+
if (wildcard) {
|
|
71
|
+
// Each segment is decoded on its own and the rest is joined with "/": confinement of the
|
|
72
|
+
// resulting path is the handler's job (it may contain "..").
|
|
73
|
+
const rest = [];
|
|
74
|
+
for (const part of parts.slice(fixed)) {
|
|
75
|
+
const decoded = decode(part);
|
|
76
|
+
if (decoded === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
rest.push(decoded);
|
|
79
|
+
}
|
|
80
|
+
params["*"] = rest.join("/");
|
|
81
|
+
}
|
|
61
82
|
return params;
|
|
62
83
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
-
import { BlobStore, SQLiteStore, stateHome } from "@alisio/core";
|
|
8
|
+
import { AnalysisJobs, ArtifactStore, BlobStore, CapabilityGrants, SQLiteStore, stateHome, } from "@alisio/core";
|
|
9
9
|
import { AuthGuard, isLoopbackHost } from "./auth/guard.js";
|
|
10
|
+
import { newSecret } from "./auth/token.js";
|
|
11
|
+
import { redactViewPath } from "./auth/view-token.js";
|
|
10
12
|
import { ApprovalBridge } from "./bridges/approval-bridge.js";
|
|
11
13
|
import { InteractionBridge } from "./bridges/interaction-bridge.js";
|
|
12
14
|
import { createNativePicker } from "./host/folder-picker.js";
|
|
@@ -19,8 +21,12 @@ import { StaticAssets } from "./http/static.js";
|
|
|
19
21
|
import { createLogger } from "./log.js";
|
|
20
22
|
import { registerAgentDefinitionRoutes } from "./routes/agent-definitions.js";
|
|
21
23
|
import { registerApprovalRoutes } from "./routes/approvals.js";
|
|
24
|
+
import { registerArtifactViewRoutes, VIEW_PREFIX, viewerBaseHeaders, } from "./routes/artifact-view.js";
|
|
25
|
+
import { registerArtifactRoutes } from "./routes/artifacts.js";
|
|
22
26
|
import { registerBlobRoutes } from "./routes/blobs.js";
|
|
27
|
+
import { registerCapabilityRoutes } from "./routes/capabilities.js";
|
|
23
28
|
import { registerCommandRoutes } from "./routes/commands.js";
|
|
29
|
+
import { registerDatasetRoutes } from "./routes/datasets.js";
|
|
24
30
|
import { registerEventRoutes } from "./routes/events.js";
|
|
25
31
|
import { registerFileRoutes } from "./routes/files.js";
|
|
26
32
|
import { registerFolderRoutes } from "./routes/folders.js";
|
|
@@ -74,6 +80,15 @@ export async function startServer(options = {}) {
|
|
|
74
80
|
root: dbPath !== ":memory:" ? join(dirname(dbPath), "blobs") : join(stateHome(), "blobs"),
|
|
75
81
|
db: catalog.db,
|
|
76
82
|
});
|
|
83
|
+
// Artifacts and capability grants of every workspace (same folders and rows the apps write).
|
|
84
|
+
const stateRoot = dbPath !== ":memory:" ? dirname(dbPath) : stateHome();
|
|
85
|
+
const artifacts = new ArtifactStore({ root: stateRoot, db: catalog.db });
|
|
86
|
+
const grants = new CapabilityGrants({ db: catalog.db, rootOf: (id) => catalog.rootOf(id) });
|
|
87
|
+
const jobs = new AnalysisJobs({ root: stateRoot, db: catalog.db });
|
|
88
|
+
// The isolated viewer (§14.2) lives outside /api, on its own router, signed per process.
|
|
89
|
+
const viewRouter = new Router();
|
|
90
|
+
const viewSecret = newSecret();
|
|
91
|
+
registerArtifactViewRoutes(viewRouter, { artifacts, secret: viewSecret });
|
|
77
92
|
// Startup reconciliation (RNF-09): runs and child sessions left queued/running by a dead
|
|
78
93
|
// process become interrupted before any client lists them.
|
|
79
94
|
catalog.interruptRuns();
|
|
@@ -140,6 +155,12 @@ export async function startServer(options = {}) {
|
|
|
140
155
|
onEvent: (event) => {
|
|
141
156
|
inflight.apply(event);
|
|
142
157
|
hub.publish(event);
|
|
158
|
+
// A persisted "allow for this session" grant: other tabs refresh their permissions.
|
|
159
|
+
if (event.type === "approval_resolved" &&
|
|
160
|
+
event.data?.persisted) {
|
|
161
|
+
const root = catalog.rootOf(event.sessionId);
|
|
162
|
+
hub.toSession(root, { t: "capabilities_changed", sessionId: root });
|
|
163
|
+
}
|
|
143
164
|
},
|
|
144
165
|
...(approvals
|
|
145
166
|
? { approve: approvals.handler, approveExternalDirectory: approvals.directoryHandler }
|
|
@@ -209,6 +230,26 @@ export async function startServer(options = {}) {
|
|
|
209
230
|
registerSessionViewRoutes(router, { catalog, sessions });
|
|
210
231
|
registerFileRoutes(router, { workspaces, catalog, sessions });
|
|
211
232
|
registerBlobRoutes(router, { blobs });
|
|
233
|
+
registerArtifactRoutes(router, {
|
|
234
|
+
artifacts,
|
|
235
|
+
sessions,
|
|
236
|
+
scheduler,
|
|
237
|
+
jobs,
|
|
238
|
+
stateRoot,
|
|
239
|
+
viewSecret,
|
|
240
|
+
});
|
|
241
|
+
registerDatasetRoutes(router, {
|
|
242
|
+
sessions,
|
|
243
|
+
catalog,
|
|
244
|
+
artifacts,
|
|
245
|
+
blobs,
|
|
246
|
+
toSession: (sessionId, frame) => hub.toSession(sessionId, frame),
|
|
247
|
+
});
|
|
248
|
+
registerCapabilityRoutes(router, {
|
|
249
|
+
grants,
|
|
250
|
+
sessions,
|
|
251
|
+
toSession: (sessionId, frame) => hub.toSession(sessionId, frame),
|
|
252
|
+
});
|
|
212
253
|
registerPromptRoutes(router, { sessions, scheduler });
|
|
213
254
|
registerCommandRoutes(router, { sessions, scheduler, workspaces });
|
|
214
255
|
registerSideQuestionRoutes(router, { sessions });
|
|
@@ -236,12 +277,32 @@ export async function startServer(options = {}) {
|
|
|
236
277
|
res.setHeader("X-Request-Id", correlationId);
|
|
237
278
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
238
279
|
const api = url.pathname === "/api" || url.pathname.startsWith("/api/");
|
|
239
|
-
|
|
280
|
+
// View tokens are capabilities: they never reach the logs.
|
|
281
|
+
const route = redactViewPath(url.pathname);
|
|
282
|
+
const viewer = url.pathname.startsWith(VIEW_PREFIX);
|
|
283
|
+
// The viewer replaces the app's headers (it must be framable by the app, and only by it).
|
|
284
|
+
if (viewer)
|
|
285
|
+
viewerBaseHeaders(res);
|
|
286
|
+
else
|
|
287
|
+
securityHeaders(res, assets.csp, api);
|
|
240
288
|
const method = req.method ?? "GET";
|
|
241
289
|
try {
|
|
242
290
|
if (!guard)
|
|
243
291
|
throw new HttpError("shutting_down", "Server is not ready");
|
|
244
292
|
guard.checkHost(req);
|
|
293
|
+
if (viewer) {
|
|
294
|
+
// No cookie (a sandboxed iframe never sends it) and no Origin check: GET/HEAD only,
|
|
295
|
+
// authorized by the signed token alone.
|
|
296
|
+
if (method !== "GET" && method !== "HEAD")
|
|
297
|
+
throw new HttpError("not_found", "Not found");
|
|
298
|
+
if (closing)
|
|
299
|
+
throw new HttpError("shutting_down", "Server is shutting down");
|
|
300
|
+
const matched = viewRouter.match(method, url.pathname);
|
|
301
|
+
if (!matched || matched === "method")
|
|
302
|
+
throw new HttpError("not_found", "Not found");
|
|
303
|
+
await matched.handler({ req, res, url, params: matched.params, correlationId });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
245
306
|
guard.checkOrigin(req);
|
|
246
307
|
if (!api) {
|
|
247
308
|
if (method !== "GET" && method !== "HEAD")
|
|
@@ -267,7 +328,8 @@ export async function startServer(options = {}) {
|
|
|
267
328
|
throw new HttpError("not_found", "Method not supported");
|
|
268
329
|
// Every write is JSON (it forces a CORS preflight) except the raw image upload, which
|
|
269
330
|
// checks its own image content types (also non-safelisted, so also preflighted).
|
|
270
|
-
const upload = method === "POST" &&
|
|
331
|
+
const upload = method === "POST" &&
|
|
332
|
+
(url.pathname === "/api/blobs" || /^\/api\/sessions\/[^/]+\/datasets$/.test(url.pathname));
|
|
271
333
|
if (!["GET", "HEAD", "OPTIONS"].includes(method) && !upload) {
|
|
272
334
|
const type = String(req.headers["content-type"] ?? "")
|
|
273
335
|
.split(";")[0]
|
|
@@ -287,7 +349,7 @@ export async function startServer(options = {}) {
|
|
|
287
349
|
if (status === 500)
|
|
288
350
|
logger.error("request failed", {
|
|
289
351
|
correlationId,
|
|
290
|
-
route
|
|
352
|
+
route,
|
|
291
353
|
error: error instanceof Error ? error.message : String(error),
|
|
292
354
|
});
|
|
293
355
|
if (!res.headersSent)
|
|
@@ -298,7 +360,7 @@ export async function startServer(options = {}) {
|
|
|
298
360
|
finally {
|
|
299
361
|
logger.debug("request", {
|
|
300
362
|
correlationId,
|
|
301
|
-
route: `${method} ${
|
|
363
|
+
route: `${method} ${route}`,
|
|
302
364
|
status: res.statusCode,
|
|
303
365
|
ms: Date.now() - started,
|
|
304
366
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ServerResponse } from "node:http";
|
|
2
|
+
import { type ArtifactStore } from "@alisio/core";
|
|
3
|
+
import type { Router } from "../http/router.ts";
|
|
4
|
+
/** Path prefix of the viewer route. */
|
|
5
|
+
export declare const VIEW_PREFIX = "/artifact-view/";
|
|
6
|
+
/**
|
|
7
|
+
* Headers every viewer response starts with (also errors): unframable, inert and uncached. A
|
|
8
|
+
* successful file response relaxes framing to the app origin with {@link viewerHeaders}.
|
|
9
|
+
*/
|
|
10
|
+
export declare function viewerBaseHeaders(res: ServerResponse): void;
|
|
11
|
+
/** The CSP of an HTML artifact (and its assets) under `origin`/`base` (§14.2). */
|
|
12
|
+
export declare function dashboardCsp(origin: string, base: string): string;
|
|
13
|
+
export declare function registerArtifactViewRoutes(router: Router, ctx: {
|
|
14
|
+
artifacts: ArtifactStore;
|
|
15
|
+
secret: string;
|
|
16
|
+
now?: () => number;
|
|
17
|
+
}): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The isolated artifact viewer (spec §14.2): `GET|HEAD /artifact-view/<token>/<path>`, outside
|
|
3
|
+
* `/api` and without the session cookie (a sandboxed iframe never sends it). The signed token
|
|
4
|
+
* names one artifact; only files listed in its manifest are served, with headers that replace
|
|
5
|
+
* the app's: framable by the app origin only, an opaque origin (`sandbox`) and no network.
|
|
6
|
+
*/
|
|
7
|
+
import { createReadStream } from "node:fs";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { viewerContentType } from "@alisio/core";
|
|
10
|
+
import { verifyViewToken } from "../auth/view-token.js";
|
|
11
|
+
import { HttpError } from "../http/errors.js";
|
|
12
|
+
import { viewable } from "./artifacts.js";
|
|
13
|
+
/** Path prefix of the viewer route. */
|
|
14
|
+
export const VIEW_PREFIX = "/artifact-view/";
|
|
15
|
+
/**
|
|
16
|
+
* Headers every viewer response starts with (also errors): unframable, inert and uncached. A
|
|
17
|
+
* successful file response relaxes framing to the app origin with {@link viewerHeaders}.
|
|
18
|
+
*/
|
|
19
|
+
export function viewerBaseHeaders(res) {
|
|
20
|
+
res.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; sandbox");
|
|
21
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
22
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
23
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
24
|
+
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
|
|
25
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
26
|
+
}
|
|
27
|
+
/** The CSP of an HTML artifact (and its assets) under `origin`/`base` (§14.2). */
|
|
28
|
+
export function dashboardCsp(origin, base) {
|
|
29
|
+
return [
|
|
30
|
+
"default-src 'none'",
|
|
31
|
+
`script-src ${base} 'unsafe-inline'`,
|
|
32
|
+
`style-src ${base} 'unsafe-inline'`,
|
|
33
|
+
`img-src ${base} data: blob:`,
|
|
34
|
+
`font-src ${base} data:`,
|
|
35
|
+
`media-src ${base} data: blob:`,
|
|
36
|
+
"connect-src 'none'",
|
|
37
|
+
"form-action 'none'",
|
|
38
|
+
"base-uri 'none'",
|
|
39
|
+
`frame-ancestors ${origin}`,
|
|
40
|
+
"sandbox allow-scripts allow-downloads",
|
|
41
|
+
].join("; ");
|
|
42
|
+
}
|
|
43
|
+
export function registerArtifactViewRoutes(router, ctx) {
|
|
44
|
+
const handler = async ({ req, res, params }) => {
|
|
45
|
+
const token = params.token ?? "";
|
|
46
|
+
const check = verifyViewToken(ctx.secret, token, (ctx.now ?? Date.now)());
|
|
47
|
+
if (!check.ok)
|
|
48
|
+
throw new HttpError("permission_denied", check.reason === "expired" ? "This view link expired" : "Invalid view link");
|
|
49
|
+
const record = ctx.artifacts.get(check.artifactId);
|
|
50
|
+
if (!record || record.status !== "ready" || !viewable(record))
|
|
51
|
+
throw new HttpError("artifact_not_found", "Artifact not found");
|
|
52
|
+
const file = await ctx.artifacts.resolveFile(record, params["*"] ?? "");
|
|
53
|
+
if (!file)
|
|
54
|
+
throw new HttpError("artifact_not_found", "File not found in this artifact");
|
|
55
|
+
// `checkHost` already validated the Host header: it names this server.
|
|
56
|
+
const origin = `http://${req.headers.host ?? ""}`;
|
|
57
|
+
const pdf = record.mimeType === "application/pdf" && file.path === (record.entry ?? record.fileName);
|
|
58
|
+
res.removeHeader("X-Frame-Options");
|
|
59
|
+
res.setHeader("Content-Security-Policy",
|
|
60
|
+
// Chromium's PDF viewer does not load in a sandboxed context (risk R6, decision D9).
|
|
61
|
+
pdf
|
|
62
|
+
? `default-src 'none'; frame-ancestors ${origin}`
|
|
63
|
+
: dashboardCsp(origin, `${origin}/artifact-view/${token}/`));
|
|
64
|
+
// The opaque-origin document must be able to load its own files.
|
|
65
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
66
|
+
res.writeHead(200, {
|
|
67
|
+
"Content-Type": pdf ? "application/pdf" : viewerContentType(file.path),
|
|
68
|
+
"Content-Length": file.bytes,
|
|
69
|
+
});
|
|
70
|
+
if (req.method === "HEAD")
|
|
71
|
+
res.end();
|
|
72
|
+
else
|
|
73
|
+
await pipeline(createReadStream(file.abs), res);
|
|
74
|
+
return undefined;
|
|
75
|
+
};
|
|
76
|
+
router.get(`${VIEW_PREFIX}:token/*`, handler);
|
|
77
|
+
router.add("HEAD", `${VIEW_PREFIX}:token/*`, handler);
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type AnalysisJobs, type ArtifactRecord, type ArtifactStore } from "@alisio/core";
|
|
2
|
+
import type { RunScheduler } from "../host/run-scheduler.ts";
|
|
3
|
+
import type { SessionService } from "../host/sessions.ts";
|
|
4
|
+
import type { Router } from "../http/router.ts";
|
|
5
|
+
/** `attachment; filename="…"; filename*=UTF-8''…` with an ASCII fallback (RFC 6266/5987). */
|
|
6
|
+
export declare function attachmentDisposition(fileName: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* CSP of files served to the panel on the app origin: inert when opened as a document (opaque
|
|
9
|
+
* origin, no scripts), harmless for `<img>` and `fetch()` which ignore it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const FILE_CSP = "default-src 'none'; img-src 'self' data:; style-src 'unsafe-inline'; sandbox";
|
|
12
|
+
/** `/artifact-view/<token>/<path>` with every path segment encoded. */
|
|
13
|
+
export declare const viewUrl: (token: string, path: string) => string;
|
|
14
|
+
/** Whether the isolated viewer serves an artifact (HTML dashboards and PDFs). */
|
|
15
|
+
export declare const viewable: (record: Pick<ArtifactRecord, "kind" | "mimeType">) => boolean;
|
|
16
|
+
export declare function registerArtifactRoutes(router: Router, ctx: {
|
|
17
|
+
artifacts: ArtifactStore;
|
|
18
|
+
sessions: SessionService;
|
|
19
|
+
scheduler?: RunScheduler;
|
|
20
|
+
jobs?: AnalysisJobs;
|
|
21
|
+
/** State root (job folders of `sources` are relative to it). */
|
|
22
|
+
stateRoot?: string;
|
|
23
|
+
/** Per-process secret that signs viewer links. */
|
|
24
|
+
viewSecret?: string;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
}): void;
|