@alisio/server 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/auth/guard.d.ts +37 -0
- package/dist/auth/guard.js +83 -0
- package/dist/auth/token.d.ts +6 -0
- package/dist/auth/token.js +22 -0
- package/dist/bridges/approval-bridge.d.ts +53 -0
- package/dist/bridges/approval-bridge.js +162 -0
- package/dist/bridges/interaction-bridge.d.ts +52 -0
- package/dist/bridges/interaction-bridge.js +163 -0
- package/dist/host/git.d.ts +22 -0
- package/dist/host/git.js +99 -0
- package/dist/host/presets.d.ts +21 -0
- package/dist/host/presets.js +60 -0
- package/dist/host/run-scheduler.d.ts +67 -0
- package/dist/host/run-scheduler.js +143 -0
- package/dist/host/sessions.d.ts +81 -0
- package/dist/host/sessions.js +211 -0
- package/dist/host/workspace-host.d.ts +87 -0
- package/dist/host/workspace-host.js +195 -0
- package/dist/http/body.d.ts +7 -0
- package/dist/http/body.js +33 -0
- package/dist/http/errors.d.ts +15 -0
- package/dist/http/errors.js +58 -0
- package/dist/http/router.d.ts +37 -0
- package/dist/http/router.js +62 -0
- package/dist/http/static.d.ts +27 -0
- package/dist/http/static.js +132 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +350 -0
- package/dist/log.d.ts +21 -0
- package/dist/log.js +19 -0
- package/dist/routes/approvals.d.ts +8 -0
- package/dist/routes/approvals.js +33 -0
- package/dist/routes/blobs.d.ts +13 -0
- package/dist/routes/blobs.js +46 -0
- package/dist/routes/commands.d.ts +21 -0
- package/dist/routes/commands.js +149 -0
- package/dist/routes/events.d.ts +14 -0
- package/dist/routes/events.js +32 -0
- package/dist/routes/files.d.ts +21 -0
- package/dist/routes/files.js +262 -0
- package/dist/routes/health.d.ts +18 -0
- package/dist/routes/health.js +38 -0
- package/dist/routes/images.d.ts +12 -0
- package/dist/routes/images.js +60 -0
- package/dist/routes/management.d.ts +32 -0
- package/dist/routes/management.js +326 -0
- package/dist/routes/prompts.d.ts +22 -0
- package/dist/routes/prompts.js +153 -0
- package/dist/routes/providers.d.ts +3 -0
- package/dist/routes/providers.js +230 -0
- package/dist/routes/session-views.d.ts +12 -0
- package/dist/routes/session-views.js +76 -0
- package/dist/routes/sessions.d.ts +16 -0
- package/dist/routes/sessions.js +160 -0
- package/dist/routes/workspaces.d.ts +7 -0
- package/dist/routes/workspaces.js +32 -0
- package/dist/schemas.d.ts +25 -0
- package/dist/schemas.js +54 -0
- package/dist/sse/hub.d.ts +80 -0
- package/dist/sse/hub.js +287 -0
- package/dist/sse/inflight.d.ts +16 -0
- package/dist/sse/inflight.js +84 -0
- package/dist/web/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/web/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/web/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/web/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/web/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/web/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/web/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/web/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/web/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/web/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/web/assets/KaTeX_Size3-Regular-gV2CO0n9.woff2 +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/web/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/web/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/web/assets/SettingsModal-Dj5G3JIz.js +1 -0
- package/dist/web/assets/SettingsModal-YCahs6O5.css +1 -0
- package/dist/web/assets/abnfDiagram-VCTEODGH-Ct0-FsLe.js +1 -0
- package/dist/web/assets/arc-Pry-7G1_.js +1 -0
- package/dist/web/assets/architecture-7GRP2DOG-B5Qnm7io.js +1 -0
- package/dist/web/assets/architectureDiagram-5GKGNRK7-BvjnUlvj.js +36 -0
- package/dist/web/assets/array-BifhSqXX.js +1 -0
- package/dist/web/assets/blockDiagram-I7D4REHJ-CBHCj2Mn.js +129 -0
- package/dist/web/assets/c4Diagram-7LVT6UL2-9I13K7Yy.js +38 -0
- package/dist/web/assets/channel-B-TykkBN.js +1 -0
- package/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/web/assets/chunk-4HAMMTFA-Du4xf36t.js +62 -0
- package/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/web/assets/chunk-75Z2AOVW-93KpRXCT.js +2 -0
- package/dist/web/assets/chunk-DU6HZSFF-DP6dsL6K.js +125 -0
- package/dist/web/assets/chunk-F27PBJKO-CxLLRIN4.js +1 -0
- package/dist/web/assets/chunk-FOHPRMQF-DzxwRta7.js +161 -0
- package/dist/web/assets/chunk-GMAD6QVW-BGv500CR.js +72 -0
- package/dist/web/assets/chunk-GVQU2GXP-BmXv3St8.js +1 -0
- package/dist/web/assets/chunk-IMKFNOWR-tmbCLM1n.js +231 -0
- package/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/web/assets/chunk-L3NEJ4N5-BULbKNtl.js +1 -0
- package/dist/web/assets/chunk-OSK3NFVY-DyvADXPn.js +10 -0
- package/dist/web/assets/chunk-P2QGCYS3-YhA2sAhR.js +1 -0
- package/dist/web/assets/chunk-POPQ4Y6H-BLr1KktA.js +1 -0
- package/dist/web/assets/chunk-PWAF6VOD-DJCZC9ie.js +1 -0
- package/dist/web/assets/chunk-SHT3W25Y-B8xH9z5t.js +168 -0
- package/dist/web/assets/chunk-SVP7TREG-DxynkM9y.js +88 -0
- package/dist/web/assets/chunk-TICWLB2K-CZPXqwaW.js +206 -0
- package/dist/web/assets/chunk-XXDRQBXY-DqAYeljN.js +1 -0
- package/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/web/assets/classDiagram-ZZMXUADV-B6LKfKSk.js +1 -0
- package/dist/web/assets/classDiagram-v2-VYDZK3BY-B6LKfKSk.js +1 -0
- package/dist/web/assets/cose-bilkent-JH36ORCC-BEpcWcmK.js +1 -0
- package/dist/web/assets/css-BsVw1vtW.js +1 -0
- package/dist/web/assets/cynefin-OW5HDTMX-rct1-Bf_.js +1 -0
- package/dist/web/assets/cynefinDiagram-5FMLGOSQ-CuBJbnvx.js +62 -0
- package/dist/web/assets/cytoscape.esm-Yq6u8L66.js +321 -0
- package/dist/web/assets/dagre-C-rnJpZF.js +1 -0
- package/dist/web/assets/dagre-GXQ25YYZ-w8zs2moB.js +4 -0
- package/dist/web/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/web/assets/diagram-S7CK7UJ4-BMBhFewM.js +30 -0
- package/dist/web/assets/diagram-UQ7AKVKN-C7BN6534.js +41 -0
- package/dist/web/assets/diagram-VSXAHHWV-Dd8_wR6U.js +3 -0
- package/dist/web/assets/diagram-VX7I27RA-DR3aOC1b.js +24 -0
- package/dist/web/assets/diagram-Z3DM3KII-5aBBRZ2Z.js +24 -0
- package/dist/web/assets/diff-woXpYk--.js +1 -0
- package/dist/web/assets/dist-v5Q1xZ2K.js +1 -0
- package/dist/web/assets/ebnfDiagram-PWID7BFC-46Gc2APT.js +1 -0
- package/dist/web/assets/engine-CknQqOvU.js +44 -0
- package/dist/web/assets/erDiagram-RLTQ6QDP-CNfUFQmD.js +99 -0
- package/dist/web/assets/eventmodeling-NTZA5JFV-Dx_J4ait.js +1 -0
- package/dist/web/assets/flowDiagram-HODETNUW-Bs7QheBl.js +1 -0
- package/dist/web/assets/ganttDiagram-EL5Y4UJY-B1RLLE0F.js +292 -0
- package/dist/web/assets/gitGraph-4MIJSDKK-DmzdoCuA.js +1 -0
- package/dist/web/assets/gitGraphDiagram-WWUBYQGX-DN5-fMOS.js +106 -0
- package/dist/web/assets/github-dark-C-LZuMrd.js +1 -0
- package/dist/web/assets/github-light-EUqPIrTm.js +1 -0
- package/dist/web/assets/go-rLFTqkRN.js +1 -0
- package/dist/web/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/web/assets/highlight-C1LfZ1aI.js +152 -0
- package/dist/web/assets/index-CGolUSWa.js +75 -0
- package/dist/web/assets/index-vUqwyH-2.css +1 -0
- package/dist/web/assets/info-A6RAGUB7-CebZRx1l.js +1 -0
- package/dist/web/assets/infoDiagram-27XIBGKW-BeY-irSX.js +2 -0
- package/dist/web/assets/init-C-OQMol4.js +1 -0
- package/dist/web/assets/ishikawaDiagram-5VMMS53U-Djn1dIqp.js +70 -0
- package/dist/web/assets/javascript-BgS3c2Ky.js +1 -0
- package/dist/web/assets/journeyDiagram-3NMN7TZE-CV_b2MnE.js +139 -0
- package/dist/web/assets/json-qhed-kSA.js +1 -0
- package/dist/web/assets/jsx-Bz0zcwM4.js +1 -0
- package/dist/web/assets/jsxRuntime-D9k5B_Bf.js +1 -0
- package/dist/web/assets/kanban-definition-UXKFOSKX-CXTukynL.js +89 -0
- package/dist/web/assets/katex-ZlcWpGUi.js +257 -0
- package/dist/web/assets/line-DIt63x6x.js +1 -0
- package/dist/web/assets/linear-SzVmGSw4.js +1 -0
- package/dist/web/assets/mermaid-parser.core-UsYAxtjN.js +7 -0
- package/dist/web/assets/mindmap-definition-YA3MSWOX-G0K_7S5N.js +96 -0
- package/dist/web/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/web/assets/packet-AYTQ26CC-D3_L-O-_.js +1 -0
- package/dist/web/assets/path-fybaL0A-.js +1 -0
- package/dist/web/assets/pegDiagram-XKGWAZYB-BoSJtVsR.js +1 -0
- package/dist/web/assets/pie-WAS4IAKB-DUARjLYc.js +1 -0
- package/dist/web/assets/pieDiagram-E7YTZNPT-DaFXBymx.js +39 -0
- package/dist/web/assets/purify.es--06brOip.js +3 -0
- package/dist/web/assets/python-gzcpVVnB.js +1 -0
- package/dist/web/assets/quadrantDiagram-AXDQQJYC-DvJJGxS3.js +7 -0
- package/dist/web/assets/radar-RG4KPBEZ-CoTA2z0h.js +1 -0
- package/dist/web/assets/railroad-74A4TZTK-D22_YeTS.js +1 -0
- package/dist/web/assets/railroad-abnf-HS5TGJTU-Mvl9LvJX.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-hvVPkAs6.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-XX_5aYEK.js +1 -0
- package/dist/web/assets/railroadDiagram-O6MQD6OU-Cxnojv6Q.js +1 -0
- package/dist/web/assets/requirementDiagram-BXWQKSXE-gW2PlZJn.js +84 -0
- package/dist/web/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/web/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/web/assets/rust-Cfkwpbl8.js +1 -0
- package/dist/web/assets/sankeyDiagram-P5KCCOFB-DcoJ0sYw.js +40 -0
- package/dist/web/assets/sequenceDiagram-WJ2MYXX4-kRLbkAfp.js +162 -0
- package/dist/web/assets/shellscript-CLZ0U2zV.js +1 -0
- package/dist/web/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/web/assets/sql-BsFa4tDR.js +1 -0
- package/dist/web/assets/src-B6xuSHsQ.js +1 -0
- package/dist/web/assets/stateDiagram-D77RDMKH-CEWdwxTm.js +1 -0
- package/dist/web/assets/stateDiagram-v2-MP3YSRHH-DZ8dTBXf.js +1 -0
- package/dist/web/assets/swimlanes-42K2YHIH-oyoSxsbH.js +1 -0
- package/dist/web/assets/swimlanesDiagram-VR7AAH4N-McbEeiYz.js +8 -0
- package/dist/web/assets/timeline-definition-24CTP7MA-D97QYUq5.js +120 -0
- package/dist/web/assets/toml-CcmNWLt0.js +1 -0
- package/dist/web/assets/treeView-Q6P3EWNA-FwhRnXTt.js +1 -0
- package/dist/web/assets/treemap-WGGIJYW6-ByFNzzzU.js +1 -0
- package/dist/web/assets/tsx-udAQXfEw.js +1 -0
- package/dist/web/assets/typescript-bsJCZSQ-.js +1 -0
- package/dist/web/assets/vennDiagram-4TSXK5OY-CcqjaQy9.js +34 -0
- package/dist/web/assets/view-0zE8CdnP.js +1 -0
- package/dist/web/assets/view-BQZA99Bp.js +1 -0
- package/dist/web/assets/view-BqpAWkcP.css +1 -0
- package/dist/web/assets/view-C9vvQ0AK.js +1 -0
- package/dist/web/assets/view-CS-jPsy6.js +2 -0
- package/dist/web/assets/view-CYRKKlxP.js +1 -0
- package/dist/web/assets/view-CyO_m5dR.css +1 -0
- package/dist/web/assets/view-D7KCHoy1.css +1 -0
- package/dist/web/assets/view-DE2OI6Vv.js +5 -0
- package/dist/web/assets/view-DPqI4qBt.css +1 -0
- package/dist/web/assets/view-DWcZ1gKW.css +1 -0
- package/dist/web/assets/view-Dl8ihdis.css +1 -0
- package/dist/web/assets/view-DrJSW55A.css +1 -0
- package/dist/web/assets/view-DzlpughI.js +257 -0
- package/dist/web/assets/view-NDb0VvVU.js +6 -0
- package/dist/web/assets/wardley-WFR3VGLG-Bt4CePrl.js +1 -0
- package/dist/web/assets/wardleyDiagram-VM6X3IG4-CcgXimi3.js +78 -0
- package/dist/web/assets/xychartDiagram-S5SC5T6Z-0Jx20wNz.js +7 -0
- package/dist/web/assets/yaml-rwi0_p6S.js +1 -0
- package/dist/web/index.html +17 -0
- package/package.json +55 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Providers, models and credentials (RF-15, §8.2). Credentials are write-only: requests may set
|
|
3
|
+
* or delete them, but no response carries a value, only `{configured, source, tail?}`. Request
|
|
4
|
+
* bodies are never logged (the server logs routes and status codes only). Activating a profile
|
|
5
|
+
* changes the provider of one workspace application, so it is refused while that workspace has
|
|
6
|
+
* runs (P-01: 409 `runs_active`).
|
|
7
|
+
*/
|
|
8
|
+
import { maskSecret, ProviderSettingsStore } from "@alisio/core";
|
|
9
|
+
import { readJson } from "../http/body.js";
|
|
10
|
+
import { HttpError } from "../http/errors.js";
|
|
11
|
+
import { is, validate } from "../schemas.js";
|
|
12
|
+
import { workspaceApp } from "./management.js";
|
|
13
|
+
/** Credential keys the web may write (RF-15). */
|
|
14
|
+
const CREDENTIAL_KEYS = ["apiKey", "bearerToken"];
|
|
15
|
+
const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
16
|
+
const secretValue = (v) => typeof v === "string" && /^\S{1,4096}$/.test(v);
|
|
17
|
+
function typesOf(opened) {
|
|
18
|
+
return (opened?.app.providers.list().map((r) => ({
|
|
19
|
+
id: r.id,
|
|
20
|
+
name: r.name,
|
|
21
|
+
...(r.description ? { description: r.description } : {}),
|
|
22
|
+
fields: r.fields.map((field) => {
|
|
23
|
+
// A secret field never carries a default value to the client.
|
|
24
|
+
const { defaultValue, ...rest } = field;
|
|
25
|
+
return field.kind === "secret" || defaultValue === undefined
|
|
26
|
+
? { ...rest }
|
|
27
|
+
: { ...rest, defaultValue };
|
|
28
|
+
}),
|
|
29
|
+
})) ?? []);
|
|
30
|
+
}
|
|
31
|
+
async function credentialStatus(store, name, values, type) {
|
|
32
|
+
const stored = await store.credentialStatus(name);
|
|
33
|
+
const keys = new Set([
|
|
34
|
+
...Object.keys(stored),
|
|
35
|
+
...(type?.fields.filter((f) => f.kind === "secret").map((f) => f.key) ?? []),
|
|
36
|
+
...CREDENTIAL_KEYS.filter((key) => `${key}Env` in values),
|
|
37
|
+
]);
|
|
38
|
+
if (!type && !keys.size)
|
|
39
|
+
keys.add("apiKey");
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
const file = stored[key];
|
|
43
|
+
const env = values[`${key}Env`];
|
|
44
|
+
if (file)
|
|
45
|
+
out[key] = { configured: true, source: "file", ...(file.tail ? { tail: file.tail } : {}) };
|
|
46
|
+
else if (typeof env === "string" && env && process.env[env])
|
|
47
|
+
out[key] = { configured: true, source: "env" };
|
|
48
|
+
else
|
|
49
|
+
out[key] = { configured: false };
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** Non-secret profile values only (a hand-edited file could hold a secret field). */
|
|
54
|
+
function publicValues(values, type) {
|
|
55
|
+
const secret = new Set([
|
|
56
|
+
...CREDENTIAL_KEYS,
|
|
57
|
+
...(type?.fields.filter((f) => f.kind === "secret").map((f) => f.key) ?? []),
|
|
58
|
+
]);
|
|
59
|
+
return Object.fromEntries(Object.entries(values).filter(([key]) => !secret.has(key)));
|
|
60
|
+
}
|
|
61
|
+
function validValue(field, value) {
|
|
62
|
+
switch (field.kind) {
|
|
63
|
+
case "boolean":
|
|
64
|
+
return typeof value === "boolean";
|
|
65
|
+
case "select":
|
|
66
|
+
return typeof value === "string" && (field.options ?? []).some((o) => o.value === value);
|
|
67
|
+
case "url": {
|
|
68
|
+
if (typeof value !== "string" || value.length > 2_000)
|
|
69
|
+
return false;
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(value);
|
|
72
|
+
return /^https?:$/.test(url.protocol) && !url.username && !url.password;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
case "text":
|
|
79
|
+
return ((typeof value === "string" && value.length <= 2_000) ||
|
|
80
|
+
(typeof value === "number" && Number.isFinite(value)));
|
|
81
|
+
default:
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function registerProviderRoutes(router, ctx, recycler) {
|
|
86
|
+
/** Any open app serves the provider types when no workspace is named. */
|
|
87
|
+
const optionalApp = async (reference) => reference ? workspaceApp(ctx.workspaces, reference) : ctx.workspaces.entries()[0];
|
|
88
|
+
/** The global profile/credential files (the same ones every workspace app reads). */
|
|
89
|
+
const store = () => new ProviderSettingsStore();
|
|
90
|
+
async function profileInfo(name, types) {
|
|
91
|
+
const settings = await store().load();
|
|
92
|
+
const profile = settings.profiles[name];
|
|
93
|
+
if (!profile)
|
|
94
|
+
return undefined;
|
|
95
|
+
const type = types.find((t) => t.id === profile.provider);
|
|
96
|
+
return {
|
|
97
|
+
name,
|
|
98
|
+
provider: profile.provider,
|
|
99
|
+
model: profile.model,
|
|
100
|
+
values: publicValues(profile.values ?? {}, type),
|
|
101
|
+
active: settings.active === name,
|
|
102
|
+
credentials: await credentialStatus(store(), name, profile.values ?? {}, type),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const profileParam = (value) => {
|
|
106
|
+
if (!value || !PROFILE_NAME.test(value))
|
|
107
|
+
throw new HttpError("validation_failed", "Invalid profile name", { fields: ["profile"] });
|
|
108
|
+
return value;
|
|
109
|
+
};
|
|
110
|
+
router.get("/api/providers", async ({ url }) => {
|
|
111
|
+
const opened = await optionalApp(url.searchParams.get("workspace"));
|
|
112
|
+
const settings = await store().load();
|
|
113
|
+
const types = typesOf(opened);
|
|
114
|
+
const profiles = [];
|
|
115
|
+
for (const name of Object.keys(settings.profiles).sort()) {
|
|
116
|
+
const info = await profileInfo(name, types);
|
|
117
|
+
if (info)
|
|
118
|
+
profiles.push(info);
|
|
119
|
+
}
|
|
120
|
+
const info = opened?.app.providerInfo;
|
|
121
|
+
const body = {
|
|
122
|
+
...(settings.active ? { active: settings.active } : {}),
|
|
123
|
+
profiles,
|
|
124
|
+
types,
|
|
125
|
+
...(opened && info
|
|
126
|
+
? {
|
|
127
|
+
current: {
|
|
128
|
+
provider: info.id,
|
|
129
|
+
model: opened.app.provider.model,
|
|
130
|
+
...(info.profileName ? { profile: info.profileName } : {}),
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
return { body };
|
|
136
|
+
});
|
|
137
|
+
router.put("/api/providers/:profile", async ({ req, params }) => {
|
|
138
|
+
const name = profileParam(params.profile);
|
|
139
|
+
const input = validate(await readJson(req), {
|
|
140
|
+
workspace: { check: is.nonEmpty(4096), required: true },
|
|
141
|
+
provider: { check: is.nonEmpty(64), required: true },
|
|
142
|
+
values: { check: is.object(), required: true },
|
|
143
|
+
model: { check: is.nonEmpty(300), required: true },
|
|
144
|
+
});
|
|
145
|
+
const opened = await workspaceApp(ctx.workspaces, input.workspace);
|
|
146
|
+
const types = typesOf(opened);
|
|
147
|
+
const type = types.find((t) => t.id === input.provider);
|
|
148
|
+
if (!type)
|
|
149
|
+
throw new HttpError("validation_failed", "Unknown provider type", { fields: ["provider"] });
|
|
150
|
+
const invalid = [];
|
|
151
|
+
for (const [key, value] of Object.entries(input.values)) {
|
|
152
|
+
const field = type.fields.find((f) => f.key === key);
|
|
153
|
+
// Secrets go through the write-only credentials route, never into providers.json.
|
|
154
|
+
if (!field || field.kind === "secret" || !validValue(field, value))
|
|
155
|
+
invalid.push(`values.${key}`);
|
|
156
|
+
}
|
|
157
|
+
if (invalid.length)
|
|
158
|
+
throw new HttpError("validation_failed", "Invalid profile values", { fields: invalid });
|
|
159
|
+
await store().saveProfile(name, {
|
|
160
|
+
provider: type.id,
|
|
161
|
+
values: input.values,
|
|
162
|
+
model: input.model.trim(),
|
|
163
|
+
});
|
|
164
|
+
recycler.announce(opened.id, ["models"]);
|
|
165
|
+
return { body: await profileInfo(name, types) };
|
|
166
|
+
});
|
|
167
|
+
router.put("/api/providers/:profile/credentials", async ({ req, params }) => {
|
|
168
|
+
const name = profileParam(params.profile);
|
|
169
|
+
const input = validate(await readJson(req), { apiKey: { check: secretValue }, bearerToken: { check: secretValue } });
|
|
170
|
+
const secrets = Object.fromEntries(Object.entries(input).filter(([, v]) => typeof v === "string"));
|
|
171
|
+
if (!Object.keys(secrets).length)
|
|
172
|
+
throw new HttpError("validation_failed", "Send apiKey or bearerToken", {
|
|
173
|
+
fields: ["apiKey", "bearerToken"],
|
|
174
|
+
});
|
|
175
|
+
if (!(await store().load()).profiles[name])
|
|
176
|
+
throw new HttpError("not_found", "Provider profile not found");
|
|
177
|
+
await store().setCredentials(name, secrets);
|
|
178
|
+
const last = Object.values(secrets).at(-1) ?? "";
|
|
179
|
+
const tail = maskSecret(last);
|
|
180
|
+
return { body: { configured: true, ...(tail ? { tail } : {}) } };
|
|
181
|
+
});
|
|
182
|
+
router.delete("/api/providers/:profile/credentials", async ({ params }) => {
|
|
183
|
+
const name = profileParam(params.profile);
|
|
184
|
+
if (!(await store().deleteCredentials(name)))
|
|
185
|
+
throw new HttpError("not_found", "No stored credentials for this profile");
|
|
186
|
+
return { body: { configured: false } };
|
|
187
|
+
});
|
|
188
|
+
router.post("/api/providers/:profile/activate", async ({ req, params }) => {
|
|
189
|
+
const name = profileParam(params.profile);
|
|
190
|
+
const input = validate(await readJson(req), {
|
|
191
|
+
workspace: { check: is.nonEmpty(4096), required: true },
|
|
192
|
+
model: { check: is.nonEmpty(300), required: true },
|
|
193
|
+
});
|
|
194
|
+
const opened = await workspaceApp(ctx.workspaces, input.workspace);
|
|
195
|
+
// P-01: the provider belongs to the workspace app; switching it under a run is refused.
|
|
196
|
+
if (ctx.scheduler.busyWorkspace(opened.id))
|
|
197
|
+
throw new HttpError("runs_active", "Runs are active in this workspace; switch the provider when they finish");
|
|
198
|
+
if (!(await store().load()).profiles[name])
|
|
199
|
+
throw new HttpError("not_found", "Provider profile not found");
|
|
200
|
+
let changed;
|
|
201
|
+
try {
|
|
202
|
+
changed = await opened.app.activateProviderProfile(name, input.model.trim());
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
throw new HttpError("provider_unavailable", error instanceof Error ? error.message : "The provider could not be activated");
|
|
206
|
+
}
|
|
207
|
+
recycler.announce(opened.id, ["models"]);
|
|
208
|
+
return { body: { changed } };
|
|
209
|
+
});
|
|
210
|
+
router.get("/api/models", async ({ url }) => {
|
|
211
|
+
const opened = await workspaceApp(ctx.workspaces, url.searchParams.get("workspace"));
|
|
212
|
+
let catalogs;
|
|
213
|
+
try {
|
|
214
|
+
catalogs = await opened.app.configuredProviderCatalogs(AbortSignal.timeout(15_000));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
throw new HttpError("provider_unavailable", "Configured providers could not be listed");
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
body: catalogs.map((c) => ({
|
|
221
|
+
profile: c.profile,
|
|
222
|
+
provider: c.provider,
|
|
223
|
+
title: c.title,
|
|
224
|
+
configuredModel: c.configuredModel,
|
|
225
|
+
models: c.models,
|
|
226
|
+
unavailable: c.unavailable,
|
|
227
|
+
})),
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SQLiteStore } from "@alisio/core";
|
|
2
|
+
import type { SessionService } from "../host/sessions.ts";
|
|
3
|
+
import type { Router } from "../http/router.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Per-session views the web composer and header need: the model catalog of the session's
|
|
6
|
+
* provider (model + effort selector, RF-09), the context estimate (context ring) and the
|
|
7
|
+
* session log download (RF-10).
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerSessionViewRoutes(router: Router, ctx: {
|
|
10
|
+
catalog: SQLiteStore;
|
|
11
|
+
sessions: SessionService;
|
|
12
|
+
}): void;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { storedToRunEvent } from "./sessions.js";
|
|
2
|
+
/**
|
|
3
|
+
* Per-session views the web composer and header need: the model catalog of the session's
|
|
4
|
+
* provider (model + effort selector, RF-09), the context estimate (context ring) and the
|
|
5
|
+
* session log download (RF-10).
|
|
6
|
+
*/
|
|
7
|
+
export function registerSessionViewRoutes(router, ctx) {
|
|
8
|
+
const { catalog, sessions } = ctx;
|
|
9
|
+
router.get("/api/sessions/:sid/models", async ({ params }) => {
|
|
10
|
+
const session = sessions.get(params.sid ?? "");
|
|
11
|
+
const { app } = await sessions.app(session);
|
|
12
|
+
let models = [];
|
|
13
|
+
let unavailable = false;
|
|
14
|
+
try {
|
|
15
|
+
models = await app.loadModels(AbortSignal.timeout(10_000));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
unavailable = true;
|
|
19
|
+
}
|
|
20
|
+
const effort = session.options?.effort;
|
|
21
|
+
return {
|
|
22
|
+
body: {
|
|
23
|
+
provider: session.provider,
|
|
24
|
+
model: session.model,
|
|
25
|
+
...(typeof effort === "string" ? { effort } : {}),
|
|
26
|
+
models,
|
|
27
|
+
unavailable,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
router.get("/api/sessions/:sid/context", async ({ params }) => {
|
|
32
|
+
const session = sessions.get(params.sid ?? "");
|
|
33
|
+
const { app } = await sessions.app(session);
|
|
34
|
+
const estimated = await app.runner.estimateContext(session.id);
|
|
35
|
+
const budget = app.contextBudget(session.model);
|
|
36
|
+
return {
|
|
37
|
+
body: {
|
|
38
|
+
estimated,
|
|
39
|
+
...(budget.total !== undefined ? { total: budget.total } : {}),
|
|
40
|
+
basis: budget.basis,
|
|
41
|
+
compactionAt: budget.compactionAt,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
// Session log: one `RunEvent` line per durable event (events.seq order, `alisio run --json`
|
|
46
|
+
// format with the global `eventId`), then one `{"type":"message"}` line per stored message.
|
|
47
|
+
router.get("/api/sessions/:sid/export", ({ params, res }) => {
|
|
48
|
+
const session = sessions.get(params.sid ?? "");
|
|
49
|
+
res.writeHead(200, {
|
|
50
|
+
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
51
|
+
"Content-Disposition": `attachment; filename="alisio-${session.id}.jsonl"`,
|
|
52
|
+
});
|
|
53
|
+
let after = 0;
|
|
54
|
+
for (;;) {
|
|
55
|
+
const page = catalog.eventsPage(session.id, { after, limit: 5000 });
|
|
56
|
+
for (const event of page.items)
|
|
57
|
+
res.write(`${JSON.stringify(storedToRunEvent(session.id, event))}\n`);
|
|
58
|
+
const last = page.items.at(-1);
|
|
59
|
+
if (!page.hasMore || !last)
|
|
60
|
+
break;
|
|
61
|
+
after = Number(last.eventId);
|
|
62
|
+
}
|
|
63
|
+
let seq = -1;
|
|
64
|
+
for (;;) {
|
|
65
|
+
const page = catalog.messagesPage(session.id, { after: seq, limit: 1000, compacted: true });
|
|
66
|
+
for (const item of page.items)
|
|
67
|
+
res.write(`${JSON.stringify({ type: "message", ...item })}\n`);
|
|
68
|
+
const last = page.items.at(-1);
|
|
69
|
+
if (!page.hasMore || !last)
|
|
70
|
+
break;
|
|
71
|
+
seq = last.seq;
|
|
72
|
+
}
|
|
73
|
+
res.end();
|
|
74
|
+
return undefined;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SQLiteStore, StoredEvent } from "@alisio/core";
|
|
2
|
+
import type { RunEvent } from "@alisio/sdk";
|
|
3
|
+
import type { RunScheduler } from "../host/run-scheduler.ts";
|
|
4
|
+
import type { SessionService } from "../host/sessions.ts";
|
|
5
|
+
import type { WorkspaceHost } from "../host/workspace-host.ts";
|
|
6
|
+
import type { Router } from "../http/router.ts";
|
|
7
|
+
export interface SessionRouteContext {
|
|
8
|
+
catalog: SQLiteStore;
|
|
9
|
+
workspaces: WorkspaceHost;
|
|
10
|
+
sessions: SessionService;
|
|
11
|
+
scheduler: RunScheduler;
|
|
12
|
+
}
|
|
13
|
+
/** A stored durable event as a `RunEvent` (in pages, `seq` is the global `events.seq`). */
|
|
14
|
+
export declare function storedToRunEvent(sessionId: string, event: StoredEvent): RunEvent;
|
|
15
|
+
/** Sessions: list/create/read/patch (no delete: archive instead), messages, events, runs. */
|
|
16
|
+
export declare function registerSessionRoutes(router: Router, ctx: SessionRouteContext): void;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { PRESET_IDS, presetInfo } from "../host/presets.js";
|
|
3
|
+
import { readJson } from "../http/body.js";
|
|
4
|
+
import { HttpError } from "../http/errors.js";
|
|
5
|
+
import { is, queryInt, validate } from "../schemas.js";
|
|
6
|
+
/** A stored durable event as a `RunEvent` (in pages, `seq` is the global `events.seq`). */
|
|
7
|
+
export function storedToRunEvent(sessionId, event) {
|
|
8
|
+
return {
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
runId: event.runId,
|
|
11
|
+
sessionId,
|
|
12
|
+
seq: Number(event.eventId),
|
|
13
|
+
type: event.type,
|
|
14
|
+
timestamp: new Date(event.createdAt ?? 0).toISOString(),
|
|
15
|
+
data: event.data,
|
|
16
|
+
eventId: event.eventId,
|
|
17
|
+
...(event.correlationId ? { correlationId: event.correlationId } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Sessions: list/create/read/patch (no delete: archive instead), messages, events, runs. */
|
|
21
|
+
export function registerSessionRoutes(router, ctx) {
|
|
22
|
+
const { catalog, sessions } = ctx;
|
|
23
|
+
router.get("/api/sessions", ({ url }) => {
|
|
24
|
+
const workspace = url.searchParams.get("workspace");
|
|
25
|
+
const q = url.searchParams.get("q")?.trim().toLowerCase();
|
|
26
|
+
const archived = url.searchParams.get("archived") ?? "false";
|
|
27
|
+
if (!["true", "false", "all"].includes(archived))
|
|
28
|
+
throw new HttpError("validation_failed", "Invalid query parameter", {
|
|
29
|
+
fields: ["archived"],
|
|
30
|
+
});
|
|
31
|
+
const limit = queryInt(url, "limit", 50, 200) ?? 50;
|
|
32
|
+
const offset = queryInt(url, "cursor", 0) ?? 0;
|
|
33
|
+
const rows = catalog
|
|
34
|
+
.list()
|
|
35
|
+
.map((session) => sessions.summary(session))
|
|
36
|
+
.filter((s) => (!workspace || s.workspaceId === workspace) &&
|
|
37
|
+
(!q || (s.title ?? "").toLowerCase().includes(q) || s.id.startsWith(q)) &&
|
|
38
|
+
(archived === "all" || s.archived === (archived === "true")))
|
|
39
|
+
.sort(bySidebarOrder);
|
|
40
|
+
const items = rows.slice(offset, offset + limit);
|
|
41
|
+
return {
|
|
42
|
+
body: {
|
|
43
|
+
items,
|
|
44
|
+
...(offset + limit < rows.length ? { next: String(offset + limit) } : {}),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
router.post("/api/sessions", async ({ req }) => {
|
|
49
|
+
const input = validate(await readJson(req), {
|
|
50
|
+
workspace: { check: is.nonEmpty(4096), required: true },
|
|
51
|
+
model: { check: is.nonEmpty(300) },
|
|
52
|
+
agent: { check: is.nonEmpty(200) },
|
|
53
|
+
preset: { check: is.oneOf(PRESET_IDS) },
|
|
54
|
+
title: { check: is.string(200) },
|
|
55
|
+
});
|
|
56
|
+
if (input.preset && !presetInfo(input.preset, sessions.ceiling).available)
|
|
57
|
+
throw new HttpError("capability_ceiling", "Preset not available under the server flags");
|
|
58
|
+
const path = isAbsolute(input.workspace)
|
|
59
|
+
? await ctx.workspaces.canonical(input.workspace)
|
|
60
|
+
: await ctx.workspaces.pathOf(input.workspace);
|
|
61
|
+
if (!path)
|
|
62
|
+
throw new HttpError("not_found", "Workspace not found");
|
|
63
|
+
const opened = await ctx.workspaces.openPath(path);
|
|
64
|
+
let created;
|
|
65
|
+
try {
|
|
66
|
+
created = await opened.app.createSession(input.model);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw new HttpError("validation_failed", error instanceof Error ? error.message : "Model is not available", { fields: ["model"] });
|
|
70
|
+
}
|
|
71
|
+
catalog.updateSessionMeta(created.id, {
|
|
72
|
+
...(input.title?.trim() ? { title: input.title.trim() } : {}),
|
|
73
|
+
options: {
|
|
74
|
+
...(input.preset ? { preset: input.preset } : {}),
|
|
75
|
+
...(input.agent ? { agent: input.agent } : {}),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
sessions.notify(created.id);
|
|
79
|
+
return { status: 201, body: sessions.detail(sessions.get(created.id)) };
|
|
80
|
+
});
|
|
81
|
+
router.get("/api/sessions/:sid", ({ params }) => ({
|
|
82
|
+
body: sessions.detail(sessions.get(params.sid ?? "")),
|
|
83
|
+
}));
|
|
84
|
+
router.patch("/api/sessions/:sid", async ({ req, params }) => {
|
|
85
|
+
const session = sessions.get(params.sid ?? "");
|
|
86
|
+
const patch = validate(await readJson(req), {
|
|
87
|
+
title: { check: is.nullableString(200) },
|
|
88
|
+
model: { check: is.nonEmpty(300) },
|
|
89
|
+
effort: { check: is.nullableString(50) },
|
|
90
|
+
preset: { check: is.oneOf(PRESET_IDS) },
|
|
91
|
+
agent: { check: is.nullableString(200) },
|
|
92
|
+
pinned: { check: is.boolean() },
|
|
93
|
+
archived: { check: is.boolean() },
|
|
94
|
+
});
|
|
95
|
+
if (patch.preset && !presetInfo(patch.preset, sessions.ceiling).available)
|
|
96
|
+
throw new HttpError("capability_ceiling", "Preset not available under the server flags");
|
|
97
|
+
if (patch.model !== undefined && patch.model !== session.model) {
|
|
98
|
+
if (ctx.scheduler.busy(session.id))
|
|
99
|
+
throw new HttpError("session_busy", "Cannot change the model while a run is active");
|
|
100
|
+
const { app } = await sessions.app(session);
|
|
101
|
+
app.runner.setModel(session.id, patch.model);
|
|
102
|
+
}
|
|
103
|
+
const options = {};
|
|
104
|
+
if (patch.effort !== undefined)
|
|
105
|
+
options.effort = patch.effort ?? undefined;
|
|
106
|
+
if (patch.agent !== undefined)
|
|
107
|
+
options.agent = patch.agent ?? undefined;
|
|
108
|
+
if (patch.preset !== undefined)
|
|
109
|
+
options.preset = patch.preset;
|
|
110
|
+
catalog.updateSessionMeta(session.id, {
|
|
111
|
+
...(patch.title !== undefined ? { title: patch.title?.trim() || null } : {}),
|
|
112
|
+
...(patch.pinned !== undefined ? { pinned: patch.pinned } : {}),
|
|
113
|
+
...(patch.archived !== undefined ? { archived: patch.archived } : {}),
|
|
114
|
+
...(Object.keys(options).length ? { options } : {}),
|
|
115
|
+
});
|
|
116
|
+
if (patch.preset !== undefined)
|
|
117
|
+
sessions.resetPolicy(session.id);
|
|
118
|
+
sessions.notify(session.id);
|
|
119
|
+
return { body: sessions.detail(sessions.get(session.id)) };
|
|
120
|
+
});
|
|
121
|
+
router.get("/api/sessions/:sid/messages", ({ params, url }) => {
|
|
122
|
+
const session = sessions.get(params.sid ?? "");
|
|
123
|
+
const before = queryInt(url, "before", undefined);
|
|
124
|
+
const after = queryInt(url, "after", undefined);
|
|
125
|
+
const page = catalog.messagesPage(session.id, {
|
|
126
|
+
...(before !== undefined ? { before } : {}),
|
|
127
|
+
...(after !== undefined ? { after } : {}),
|
|
128
|
+
limit: queryInt(url, "limit", 50, 1000) ?? 50,
|
|
129
|
+
compacted: url.searchParams.get("includeCompacted") === "true",
|
|
130
|
+
});
|
|
131
|
+
return { body: page };
|
|
132
|
+
});
|
|
133
|
+
router.get("/api/sessions/:sid/events", ({ params, url }) => {
|
|
134
|
+
const session = sessions.get(params.sid ?? "");
|
|
135
|
+
const types = url.searchParams.get("types")?.split(",").filter(Boolean);
|
|
136
|
+
const page = catalog.eventsPage(session.id, {
|
|
137
|
+
after: queryInt(url, "after", 0) ?? 0,
|
|
138
|
+
limit: queryInt(url, "limit", 200, 5000) ?? 200,
|
|
139
|
+
});
|
|
140
|
+
const last = page.items.at(-1);
|
|
141
|
+
return {
|
|
142
|
+
body: {
|
|
143
|
+
items: page.items
|
|
144
|
+
.filter((event) => !types?.length || types.includes(event.type))
|
|
145
|
+
.map((event) => storedToRunEvent(session.id, event)),
|
|
146
|
+
...(page.hasMore && last ? { next: last.eventId } : {}),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
router.get("/api/sessions/:sid/runs", ({ params, url }) => {
|
|
151
|
+
const session = sessions.get(params.sid ?? "");
|
|
152
|
+
return { body: catalog.runs(session.id, { limit: queryInt(url, "limit", 20, 500) ?? 20 }) };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** Pinned first, then most recently updated; sessions without `updatedAt` last. */
|
|
156
|
+
function bySidebarOrder(a, b) {
|
|
157
|
+
if (a.pinned !== b.pinned)
|
|
158
|
+
return a.pinned ? -1 : 1;
|
|
159
|
+
return (b.updatedAt ?? -1) - (a.updatedAt ?? -1);
|
|
160
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { WorkspaceHost } from "../host/workspace-host.ts";
|
|
2
|
+
import type { Router } from "../http/router.ts";
|
|
3
|
+
/** `GET/POST /api/workspaces`, `PATCH /api/workspaces/:wid`. */
|
|
4
|
+
export declare function registerWorkspaceRoutes(router: Router, ctx: {
|
|
5
|
+
workspaces: WorkspaceHost;
|
|
6
|
+
catalog: import("@alisio/core").SQLiteStore;
|
|
7
|
+
}): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { readJson } from "../http/body.js";
|
|
3
|
+
import { HttpError } from "../http/errors.js";
|
|
4
|
+
import { is, validate } from "../schemas.js";
|
|
5
|
+
/** `GET/POST /api/workspaces`, `PATCH /api/workspaces/:wid`. */
|
|
6
|
+
export function registerWorkspaceRoutes(router, ctx) {
|
|
7
|
+
router.get("/api/workspaces", async () => ({ body: await ctx.workspaces.list() }));
|
|
8
|
+
router.post("/api/workspaces", async ({ req }) => {
|
|
9
|
+
const { path } = validate(await readJson(req), {
|
|
10
|
+
path: { check: is.nonEmpty(4096), required: true },
|
|
11
|
+
});
|
|
12
|
+
if (!isAbsolute(path))
|
|
13
|
+
throw new HttpError("validation_failed", "path must be absolute", { fields: ["path"] });
|
|
14
|
+
const canonical = await ctx.workspaces.canonical(path);
|
|
15
|
+
const opened = await ctx.workspaces.openPath(canonical);
|
|
16
|
+
return { body: await ctx.workspaces.info(opened.path) };
|
|
17
|
+
});
|
|
18
|
+
router.patch("/api/workspaces/:wid", async ({ req, params }) => {
|
|
19
|
+
const patch = validate(await readJson(req), {
|
|
20
|
+
label: { check: is.nullableString(200) },
|
|
21
|
+
pinned: { check: is.boolean() },
|
|
22
|
+
});
|
|
23
|
+
const path = await ctx.workspaces.pathOf(params.wid ?? "");
|
|
24
|
+
if (!path)
|
|
25
|
+
throw new HttpError("not_found", "Workspace not found");
|
|
26
|
+
ctx.catalog.recordWorkspace(path, {
|
|
27
|
+
...(patch.label !== undefined ? { label: patch.label?.trim() || null } : {}),
|
|
28
|
+
...(patch.pinned !== undefined ? { pinned: patch.pinned } : {}),
|
|
29
|
+
});
|
|
30
|
+
return { body: await ctx.workspaces.info(path) };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type Check = (value: unknown) => boolean;
|
|
2
|
+
export interface FieldSpec {
|
|
3
|
+
check: Check;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare const is: {
|
|
7
|
+
string: (max?: number) => Check;
|
|
8
|
+
nonEmpty: (max?: number) => Check;
|
|
9
|
+
boolean: () => Check;
|
|
10
|
+
nullableString: (max?: number) => Check;
|
|
11
|
+
oneOf: (values: readonly string[]) => Check;
|
|
12
|
+
requestId: () => Check;
|
|
13
|
+
integer: (min: number, max: number) => Check;
|
|
14
|
+
array: (item: Check, max?: number) => Check;
|
|
15
|
+
object: () => Check;
|
|
16
|
+
any: () => Check;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Validates a JSON object against `fields`: unknown keys and wrong types are rejected. Returns
|
|
20
|
+
* the object typed as `T` (the caller's shape must match `fields`).
|
|
21
|
+
*/
|
|
22
|
+
export declare function validate<T>(body: unknown, fields: Record<string, FieldSpec>): T;
|
|
23
|
+
/** An optional positive integer query parameter, clamped to `max`. */
|
|
24
|
+
export declare function queryInt(url: URL, name: string, fallback: number | undefined, max?: number): number | undefined;
|
|
25
|
+
export {};
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request validation without a schema library (the server adds no runtime dependencies).
|
|
3
|
+
* Failures are 400 `validation_failed` with the offending field paths, never their values.
|
|
4
|
+
*/
|
|
5
|
+
import { HttpError } from "./http/errors.js";
|
|
6
|
+
export const is = {
|
|
7
|
+
string: (max = 100_000) => (v) => typeof v === "string" && v.length <= max,
|
|
8
|
+
nonEmpty: (max = 100_000) => (v) => typeof v === "string" && v.trim().length > 0 && v.length <= max,
|
|
9
|
+
boolean: () => (v) => typeof v === "boolean",
|
|
10
|
+
nullableString: (max = 1_000) => (v) => v === null || (typeof v === "string" && v.length <= max),
|
|
11
|
+
oneOf: (values) => (v) => typeof v === "string" && values.includes(v),
|
|
12
|
+
requestId: () => (v) => typeof v === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(v),
|
|
13
|
+
integer: (min, max) => (v) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max,
|
|
14
|
+
array: (item, max = 100) => (v) => Array.isArray(v) && v.length <= max && v.every(item),
|
|
15
|
+
object: () => (v) => !!v && typeof v === "object" && !Array.isArray(v),
|
|
16
|
+
any: () => () => true,
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Validates a JSON object against `fields`: unknown keys and wrong types are rejected. Returns
|
|
20
|
+
* the object typed as `T` (the caller's shape must match `fields`).
|
|
21
|
+
*/
|
|
22
|
+
export function validate(body, fields) {
|
|
23
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
24
|
+
throw new HttpError("validation_failed", "Request body must be a JSON object", {
|
|
25
|
+
fields: ["(body)"],
|
|
26
|
+
});
|
|
27
|
+
const record = body;
|
|
28
|
+
const invalid = [];
|
|
29
|
+
for (const key of Object.keys(record))
|
|
30
|
+
if (!(key in fields))
|
|
31
|
+
invalid.push(key);
|
|
32
|
+
for (const [key, spec] of Object.entries(fields)) {
|
|
33
|
+
const value = record[key];
|
|
34
|
+
if (value === undefined) {
|
|
35
|
+
if (spec.required)
|
|
36
|
+
invalid.push(key);
|
|
37
|
+
}
|
|
38
|
+
else if (!spec.check(value))
|
|
39
|
+
invalid.push(key);
|
|
40
|
+
}
|
|
41
|
+
if (invalid.length)
|
|
42
|
+
throw new HttpError("validation_failed", "Invalid request fields", { fields: invalid });
|
|
43
|
+
return record;
|
|
44
|
+
}
|
|
45
|
+
/** An optional positive integer query parameter, clamped to `max`. */
|
|
46
|
+
export function queryInt(url, name, fallback, max = Number.MAX_SAFE_INTEGER) {
|
|
47
|
+
const raw = url.searchParams.get(name);
|
|
48
|
+
if (raw === null || raw === "")
|
|
49
|
+
return fallback;
|
|
50
|
+
const value = Number(raw);
|
|
51
|
+
if (!Number.isInteger(value) || value < 0)
|
|
52
|
+
throw new HttpError("validation_failed", "Invalid query parameter", { fields: [name] });
|
|
53
|
+
return Math.min(value, max);
|
|
54
|
+
}
|