From 531a51fd0e3fa2ede821c5efe046ada760c44388 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 15 May 2026 18:07:47 +0000 Subject: [PATCH 1/3] fix(svalinn): remove stale lib/ocaml build snapshot breaking ReScript build The svalinn container build failed at `deno task res:build` because src/lib/ocaml/ was a stale, committed ReScript build-output snapshot (.ast/.cmj intermediates plus a flattened copy of every .res module). rescript.json globs sources as {"dir": ".", "subdirs": true}, so the compiler picked up both the real sources and this duplicate snapshot, producing flat-namespace module collisions: Could not initialize build: Duplicate module name: Client. Found in lib/ocaml/Client.res and vordr/Client.res. (~20 modules collided; Client was just the first reported.) Fix: delete the stale src/lib/ snapshot and broaden src/.gitignore from `lib/bs/` to `lib/` so the ReScript build dir (lib/bs, lib/ocaml) can no longer be committed and regress the build. Verified end-to-end: `rescript build` now exits 0 and emits all *.res.js including src/Main.res.js (deprecation warnings only). https://claude.ai/code/session_01VPKWisqJq8wXSjq3mhPATv --- container-stack/svalinn/src/.gitignore | 2 +- .../svalinn/src/lib/ocaml/AuthTypes.ast | Bin 20342 -> 0 bytes .../svalinn/src/lib/ocaml/AuthTypes.cmj | Bin 221 -> 0 bytes .../svalinn/src/lib/ocaml/AuthTypes.res | 260 ---- .../svalinn/src/lib/ocaml/Client.ast | Bin 36093 -> 0 bytes .../svalinn/src/lib/ocaml/Client.cmj | Bin 479 -> 0 bytes .../svalinn/src/lib/ocaml/Client.res | 278 ---- .../svalinn/src/lib/ocaml/Deno.ast | Bin 7635 -> 0 bytes .../svalinn/src/lib/ocaml/Deno.cmj | Bin 81 -> 0 bytes .../svalinn/src/lib/ocaml/Deno.res | 110 -- .../svalinn/src/lib/ocaml/Fetch.ast | Bin 7329 -> 0 bytes .../svalinn/src/lib/ocaml/Fetch.cmj | Bin 81 -> 0 bytes .../svalinn/src/lib/ocaml/Fetch.res | 74 - .../svalinn/src/lib/ocaml/Gateway.ast | Bin 149589 -> 0 bytes .../svalinn/src/lib/ocaml/Gateway.cmj | Bin 449 -> 0 bytes .../svalinn/src/lib/ocaml/Gateway.res | 1219 ----------------- .../svalinn/src/lib/ocaml/GatewayTypes.ast | Bin 6742 -> 0 bytes .../svalinn/src/lib/ocaml/GatewayTypes.cmj | Bin 80 -> 0 bytes .../svalinn/src/lib/ocaml/GatewayTypes.res | 93 -- .../svalinn/src/lib/ocaml/Hono.ast | Bin 13927 -> 0 bytes .../svalinn/src/lib/ocaml/Hono.cmj | Bin 81 -> 0 bytes .../svalinn/src/lib/ocaml/Hono.res | 92 -- container-stack/svalinn/src/lib/ocaml/JWT.ast | Bin 64561 -> 0 bytes container-stack/svalinn/src/lib/ocaml/JWT.cmj | Bin 267 -> 0 bytes container-stack/svalinn/src/lib/ocaml/JWT.res | 448 ------ .../svalinn/src/lib/ocaml/Main.ast | Bin 287 -> 0 bytes .../svalinn/src/lib/ocaml/Main.cmj | Bin 72 -> 0 bytes .../svalinn/src/lib/ocaml/Main.res | 5 - .../svalinn/src/lib/ocaml/McpClient.ast | Bin 40203 -> 0 bytes .../svalinn/src/lib/ocaml/McpClient.cmj | Bin 246 -> 0 bytes .../svalinn/src/lib/ocaml/McpClient.res | 311 ----- .../svalinn/src/lib/ocaml/McpTypes.ast | Bin 4038 -> 0 bytes .../svalinn/src/lib/ocaml/McpTypes.cmj | Bin 76 -> 0 bytes .../svalinn/src/lib/ocaml/McpTypes.res | 62 - .../svalinn/src/lib/ocaml/Metrics.ast | Bin 24121 -> 0 bytes .../svalinn/src/lib/ocaml/Metrics.cmj | Bin 460 -> 0 bytes .../svalinn/src/lib/ocaml/Metrics.res | 215 --- .../svalinn/src/lib/ocaml/Middleware.ast | Bin 57508 -> 0 bytes .../svalinn/src/lib/ocaml/Middleware.cmj | Bin 340 -> 0 bytes .../svalinn/src/lib/ocaml/Middleware.res | 538 -------- .../svalinn/src/lib/ocaml/OAuth2.ast | Bin 50775 -> 0 bytes .../svalinn/src/lib/ocaml/OAuth2.cmj | Bin 324 -> 0 bytes .../svalinn/src/lib/ocaml/OAuth2.res | 373 ----- .../svalinn/src/lib/ocaml/PolicyEngine.ast | Bin 32622 -> 0 bytes .../svalinn/src/lib/ocaml/PolicyEngine.cmj | Bin 324 -> 0 bytes .../svalinn/src/lib/ocaml/PolicyEngine.res | 308 ----- .../svalinn/src/lib/ocaml/RateLimiter.ast | Bin 17775 -> 0 bytes .../svalinn/src/lib/ocaml/RateLimiter.cmj | Bin 201 -> 0 bytes .../svalinn/src/lib/ocaml/RateLimiter.res | 159 --- .../svalinn/src/lib/ocaml/SecurityHeaders.ast | Bin 10428 -> 0 bytes .../svalinn/src/lib/ocaml/SecurityHeaders.cmj | Bin 209 -> 0 bytes .../svalinn/src/lib/ocaml/SecurityHeaders.res | 158 --- .../svalinn/src/lib/ocaml/SelurBridge.ast | Bin 25697 -> 0 bytes .../svalinn/src/lib/ocaml/SelurBridge.cmj | Bin 310 -> 0 bytes .../svalinn/src/lib/ocaml/SelurBridge.res | 286 ---- .../svalinn/src/lib/ocaml/Server.ast | Bin 65642 -> 0 bytes .../svalinn/src/lib/ocaml/Server.cmj | Bin 494 -> 0 bytes .../svalinn/src/lib/ocaml/Server.res | 560 -------- .../svalinn/src/lib/ocaml/Tools.ast | Bin 22838 -> 0 bytes .../svalinn/src/lib/ocaml/Tools.cmj | Bin 248 -> 0 bytes .../svalinn/src/lib/ocaml/Tools.res | 207 --- .../svalinn/src/lib/ocaml/Validation.ast | Bin 28170 -> 0 bytes .../svalinn/src/lib/ocaml/Validation.cmj | Bin 487 -> 0 bytes .../svalinn/src/lib/ocaml/Validation.res | 240 ---- .../svalinn/src/lib/ocaml/VordrTypes.ast | Bin 8701 -> 0 bytes .../svalinn/src/lib/ocaml/VordrTypes.cmj | Bin 91 -> 0 bytes .../svalinn/src/lib/ocaml/VordrTypes.res | 116 -- 67 files changed, 1 insertion(+), 6113 deletions(-) delete mode 100644 container-stack/svalinn/src/lib/ocaml/AuthTypes.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/AuthTypes.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/AuthTypes.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Client.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Client.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Client.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Deno.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Deno.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Deno.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Fetch.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Fetch.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Fetch.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Gateway.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Gateway.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Gateway.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/GatewayTypes.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/GatewayTypes.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/GatewayTypes.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Hono.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Hono.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Hono.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/JWT.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/JWT.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/JWT.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Main.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Main.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Main.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/McpClient.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/McpClient.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/McpClient.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/McpTypes.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/McpTypes.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/McpTypes.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Metrics.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Metrics.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Metrics.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Middleware.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Middleware.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Middleware.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/OAuth2.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/OAuth2.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/OAuth2.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/PolicyEngine.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/PolicyEngine.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/PolicyEngine.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/RateLimiter.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/RateLimiter.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/RateLimiter.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/SecurityHeaders.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/SecurityHeaders.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/SecurityHeaders.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/SelurBridge.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/SelurBridge.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/SelurBridge.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Server.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Server.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Server.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Tools.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Tools.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Tools.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/Validation.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/Validation.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/Validation.res delete mode 100644 container-stack/svalinn/src/lib/ocaml/VordrTypes.ast delete mode 100644 container-stack/svalinn/src/lib/ocaml/VordrTypes.cmj delete mode 100644 container-stack/svalinn/src/lib/ocaml/VordrTypes.res diff --git a/container-stack/svalinn/src/.gitignore b/container-stack/svalinn/src/.gitignore index 3fa0fbd..c3af857 100644 --- a/container-stack/svalinn/src/.gitignore +++ b/container-stack/svalinn/src/.gitignore @@ -1 +1 @@ -lib/bs/ +lib/ diff --git a/container-stack/svalinn/src/lib/ocaml/AuthTypes.ast b/container-stack/svalinn/src/lib/ocaml/AuthTypes.ast deleted file mode 100644 index f18ba468e3d5b8eb5ba16144d8da850e04f35220..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20342 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 diff --git a/container-stack/svalinn/src/lib/ocaml/AuthTypes.cmj b/container-stack/svalinn/src/lib/ocaml/AuthTypes.cmj deleted file mode 100644 index 386822a8741948d51807e121fa25bd96239bfc4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 221 zcmeAvEz+N})VJB8Zs}Rimru{POkK8*fq`Ks5Gw+4EfD7%xUfMtB{eOvG^ZpuIlmya zc)|h)2Zs$tiKQhOzNsY{`6(g!!6ikRdFc}tEN}!#nINRxit=+&B`pe4i*hrIi!<}{ z9Ft3cB)V=ZbOjh@I4qc;UtE@$lbM&No1R#bTAo, -} - -// OIDC configuration (extends OAuth2) -type oidcConfig = { - clientId: string, - clientSecret: string, - authorizationEndpoint: string, - tokenEndpoint: string, - redirectUri: string, - scopes: array, - issuer: string, - userInfoEndpoint: string, - jwksUri: string, - endSessionEndpoint: option, -} - -// API key information -type apiKeyInfo = { - id: string, - name: string, - scopes: array, - createdAt: string, - expiresAt: option, - rateLimit: option, -} - -// API key configuration -type apiKeyConfig = { - header: string, - prefix: option, - keys: Belt.Map.String.t, -} - -// mTLS configuration -type mtlsConfig = { - caCert: string, - requireClientCert: bool, - verifyDepth: int, -} - -// Authentication configuration -type authConfig = { - enabled: bool, - methods: array, - oauth2: option, - oidc: option, - apiKey: option, - mtls: option, - excludePaths: array, -} - -// Token payload (decoded JWT) -type tokenPayload = { - sub: string, - iss: string, - aud: Js.Json.t, // string or array - exp: int, - iat: int, - scope: option, - email: option, - name: option, - groups: option>, - claims: Js.Dict.t, -} - -// Authentication result -type authResult = { - authenticated: bool, - method: authMethod, - subject: option, - scopes: option>, - token: option, - error: option, -} - -// User context attached to requests -type userContext = { - id: string, - email: option, - name: option, - groups: array, - scopes: array, - method: authMethod, - issuedAt: int, - expiresAt: option, -} - -// Authorization check result -type authzResult = { - allowed: bool, - reason: option, - requiredScopes: option>, - missingScopes: option>, -} - -// Permission action -type permissionAction = - | Create - | Read - | Update - | Delete - | Execute - -// Permission definition -type permission = { - resource: string, - actions: array, -} - -// RBAC role definition -type role = { - name: string, - permissions: array, - description: option, -} - -// Convert permission action to string -let permissionActionToString = (action: permissionAction): string => { - switch action { - | Create => "create" - | Read => "read" - | Update => "update" - | Delete => "delete" - | Execute => "execute" - } -} - -// Convert string to permission action -let permissionActionFromString = (str: string): option => { - switch str { - | "create" => Some(Create) - | "read" => Some(Read) - | "update" => Some(Update) - | "delete" => Some(Delete) - | "execute" => Some(Execute) - | _ => None - } -} - -// Convert auth method to string -let authMethodToString = (method: authMethod): string => { - switch method { - | OAuth2 => "oauth2" - | OIDC => "oidc" - | ApiKey => "api-key" - | MTLS => "mtls" - | None => "none" - } -} - -// Convert string to auth method -let authMethodFromString = (str: string): option => { - switch str { - | "oauth2" => Some(OAuth2) - | "oidc" => Some(OIDC) - | "api-key" => Some(ApiKey) - | "mtls" => Some(MTLS) - | "none" => Some(None) - | _ => None - } -} - -// Default roles -let defaultRoles: array = [ - { - name: "admin", - description: Some("Full access to all resources"), - permissions: [ - { - resource: "*", - actions: [Create, Read, Update, Delete, Execute], - }, - ], - }, - { - name: "operator", - description: Some("Can manage containers but not policies"), - permissions: [ - { - resource: "containers", - actions: [Create, Read, Update, Delete, Execute], - }, - { - resource: "images", - actions: [Read], - }, - { - resource: "policies", - actions: [Read], - }, - ], - }, - { - name: "viewer", - description: Some("Read-only access"), - permissions: [ - { - resource: "containers", - actions: [Read], - }, - { - resource: "images", - actions: [Read], - }, - { - resource: "policies", - actions: [Read], - }, - ], - }, - { - name: "auditor", - description: Some("Can view logs and audit trail"), - permissions: [ - { - resource: "containers", - actions: [Read], - }, - { - resource: "logs", - actions: [Read], - }, - { - resource: "audit", - actions: [Read], - }, - ], - }, -] - -// Default scopes mapping -let defaultScopes: Belt.Map.String.t = Belt.Map.String.fromArray([ - ("svalinn:read", "Read access to Svalinn resources"), - ("svalinn:write", "Write access to Svalinn resources"), - ("svalinn:admin", "Administrative access"), - ("containers:create", "Create containers"), - ("containers:read", "View containers"), - ("containers:delete", "Delete containers"), - ("images:verify", "Verify images"), - ("policies:manage", "Manage policies"), -]) diff --git a/container-stack/svalinn/src/lib/ocaml/Client.ast b/container-stack/svalinn/src/lib/ocaml/Client.ast deleted file mode 100644 index fc6753d46eaaa69485059ec1c50a8ca385471497..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36093 zcmb__cYKsp_Wpa4_f4Cbv`Lv%LJd7&uWJxcVHFUt7Yt;8NSa9idsl3$!Lqv60ql)n zuQ--<)wN(RV8z}Wt77{-=bn3J-W1~Zj~^e)dConz-}~Hi%R4xZvqRSI(Z-IfJ)#}+ z7G~{P*AZP>cTCpQ_N?i1kIb6U*3i_@(Xb>sr?su6si8fZwSP-necS9~TBGe*HB0K+ zhRmEiWBRN)lcvn9Y450OjW#yd%xh`xsB37BwvB;i-lCfJC3TGr&CNCKZS!iDaO|3i zjSbP}j0ubyI6=yWAAlt&PQ2)y6U$XRs3+JDTU%SZJoa8}c(l2`wWXoC0~SSlk<&@eg>ko7q|0H{wJO#*ti7YHp_vn@lz=Pa z?f{Wia?Yu43jZ(2JZaCO{0Nu)8x5T)$d0B1^gP2623($1Q@Hb57~ zS|Gpc=2#a-Nuq@>o=r5FXs@_?iAd9GQ35x^O>@&QLSWmi)vghv8F4oz(m}Od%VPt_ zalc5^hkjJs?0BRZUD zPHiV5qpD`G+%Lg@i@Ohqw41Mf1w5KN(S33E5eXgU=1A6$3NCZ=*gnP0N6w}APE$tmU?J#bKah-!k9*OFFDTZs}?pGo`g=%rH_$Ka; z2NOL*^laSSB+|>=CquU$hd zPT_V{wz6ZmdOpZ`a`Tcdn1#?yn~DB_m~F%qFENm>i**hfK7y@^B`5=8M8vFGV*@6ab4FF=(ZIrItNW{A2O+7UdP}CQBa9T+lbXDh}(*| z9Y=`8YI)e_6vhTlWwTu+YVxtx}x7W>&&WRq~+U5qj)^wrfXrp0s znp>Kqb0*X}f4V6cVYQg?ofBNIHa22#TisG)j@lHtMfgvx6tB~*bi;0iTVe8FzH;kH z(dL%1(dH#%J6f>NMdx)4AAzJjIzq~@3&c7R_eGT0Xr34y(Wcf$ERb#)rUCxAqiQiL zJ+9I6m1FmxK6BE{Ig_VNnlXLKwAt?IoLaLaF&kowi1j9Bm%A%eM~Nzm;S=u!UVH?l zETohpyUZ_gq|*{qY!$Ah;61<+yFGoMAMEe5usCJ zoG;3y2){tYqfNJ%5?oYADaTOCai#>*(8^qb%SCwt#4AKR#SE6(u3I?So-@{?YsKme zM7T~wGiPI+-3HXn5_A!)ZV~Yc4D3u^3{6kmF0C^J;j8CT%9WIIjSX2NA%$it@aXHb zEbh?*^C%@oDJv1_K@qRlL1$RDTM3>Niw9u*9}yq(Gp%XnKC1VU*xrI@FN=7eX>;IL z1>XSiH4z`T{ap#w8=`sw^>as{o)}ji)o>HPBa<)>;=A>f@^4Cc(oeM>eIf=y7KlDboE_#IH?V zHX@R&d&Uhy8B;t7{~ltRh(8-syBv5aA<=GzLAHp$`pPH-Ec~*%>mly7Vd7(P;uQ-i zHJws3Ay$f*WBRSzQs<=%matry4H2ZL;nV21`z8rUy@N$^$ zB;ugHa@uC#+lJ-W{j^C`!!JN zcuL(G(e@W{$G%E&h=lC~vqMFksHNy~cQ>pW^XM)v$UM{ZsUF1oBPn$frB2qNByNaxJ@j-vrD&^(jkfud#&A_vEf)N8)vt)G;%3UZlcsfD0L3R zQ$$?A_44^siE(wVgoDCT&J%H|F-?rCi$r@23@#S&SYL5vTuXXK^aEtcm)yhlWJOYBNGwY{a8y(BN? zQ3<~o;$tFS40-5PCIz`FKhn&-JYxI17Ho8=Q;qg|v&w$`=PHMR4~ z%ek#@q0|>B^;L))M10rOxyK?s6iZphF&yDDvHAew=OTW?5jdY4F{U=K&36*;EyV9d z{7zN?u1G)`J7trEy~rMT*Xmw=LA&t_Qr%Wc{hm^PjC-k`h<|E^;DqkgNzFDEt^?y7 z5wp~I`N|>9=x9uO6fw0}LP3zJ5fSsTtfCUWigTYNJ3h5q> z&Jb}AHD11Q1P12fChV%)y_9Bdj|0VIFNg<;I7`|C3F|0Mohu>x!Dya{2PLXi1!Yu} z2cr&lwE1@)UTRA_xrdTF7pAQuF5>v6%{!B;OzJTb*bKX4MeNX`qc}-AOg&9P7sK#$ z5syn0h}-#0Q6A3?4|tZq!_6GBB^+gUl)Jao^Y~7^u!Gza$UP0>MIxS`D5Gz#R<{^0 za1KX_iQRIDD@2TGaebqSXWR((yeG7^)tRii#CQebtr77KjdxY7>(Us!#Ofn42HRTa zc`2`QPLC`m_il3GfuzPoe4O_ZzFc~Y+2_UT--z&nh%ajv%U2HGYV0^KDu%+Hh`JF94SwimH%G6vHB7beiCtWvb-aitlMjN-SAQiIsHGy=GVCAI!5%;lf`yZM1@T}7u$8k_2Z-%VhzE)|SEHe(-R8SnCkFFi zJ6FWUZWXN;WfNB&bsqq``J!sJbzdkDB<41V*kVH#$&9%ZA&tk7cNBRYoC=az3stkI z7F*Ris9Hs}glz!ZB>qW$E*OqoTWd>mJ8xs$(PLJ+SEVfJQ z8cfqH6SFmlah5ZY(&{N~!30D}EGwNu&8g|o`FwWTK?d;+5xhqc~ zZ!LL`!FZL3PniZ!o&b67jS}!QY;F?q1vG?so!xd(?rI6<2I&&<4Or&#n#{$Nowk>H zJh#AoCzAITdGA2H-`Sng4x_ZWy4Utnk8`tli;c4`nerUtsguZikGzi|J}u(cSR4n{ zjpL2AF|J-1t*ei=u~X!};?$D&GkI_<-B(3UOE>o8-9grV$XwxN_dRCY0a)p&6$#&u6|cuHj|NbEk9ZcC;k|H+nl+SsWjC zTrK`Ftx@Nt{@8OF8zJZ54fA=i^!{%990cd>ZRKe`=IODHd;3|N>9F75$_M+|$2-t! z4>2U+uB^pB_|xUS?7k$bSAY=x5SgP_x{R&cFobK!fl+5Nr8Q7m3*`A$Zch+}`x)Cb zSv__ZUbB_|)r}ceSZWwP(=w2x@8%-L2{y(B$`RmMKT~;UT7ym)FSGJxiK>>P!}$-+ zvP^Bxu@;Mr!c>QM?$u56S~+tr??0VADeW0bTL<}4E5B~)giY2cYiN1WyV@GO3FB+5 z{2@1^|DK|&+p!-zDWmJ7^IGboa+t=McG1zR#`32KhA!T#nb$YGh7`G z8}0m^(pORXZIH)Ud9~>rzt_^cb<=jT#?K()&Q^X~TcGdOO`B@%?nRuxTlq1KgE6VC zJKKic0lWRIe7D*yU&&sY$4g$?VK(q_$cJ0`KfZ=N!s>8GARXx2a@ZL&O+C1@CTolZ zI<48t$%()X+iJBx*oJMh_$GhD4ujW&8x$_y@y_0q{v)MhMNT_G==%(H3>_ZuYC}T9_+pIj^Un&259Nc8)V%h`N@K{88(8_0PkN(9-d&b&h=}G&q zl@m_#7Dn1z)^10{S#RY@nxAi^eQ3kBf!#+|-d^pNuasJQX{lWCuWa~a$X{D|Z(lop zYjyiwAJ#aKE}Nbwle)Q)NB`rcC=vg$mKf28vX|POp!C;lG>y&G1JeoT3l0b zQp;n5#+ZZh^iCTv3P#JVtmkI91Wdok>Tqy|?JjOro1*RQbqmZ$y4;ygnKhKjc2`*W zZ~xbkbfb-ME+XG#8^io~8M99}zFv z;98izY-Qi!o;HD(_8m9=dTTKo@&+p(<{K36Sl!{iUgY80*J7Ujg*EO#VqaSMtVB;T z4Sa32XWO1!IcC4+Ma?Zso2v{Xd+9+g)c>s2^^kwEGM<^>PnTN_-7i*m4>a7ZL+a}~ z>JSkd)ESg{A7#cNla-&?Qln;MTZ`vmonz%U`)t&Vhz))crX^N>R!uR@xBy=I_FRU6 zHux>bgRJ}>mtEYWj3HL{zUW{z%<|pEkgJ`2DRU!bV%G2-oJUnM5m?+4;iGIg-oLOn zlrhG}9nNOLl|FYfV>@RyWp1L(-(kAF$XRZpLHei5j7c^E=rUunl@oM{LFdnCnhj5b z?Q|=Lk_}`|6Vscx#0S_=oN#3vXyqDd3w9qf4z@bJ^5V`F*Dj;Z*^jceqO6@@GS|wx z{9kL*Vk1Nmxz)-iY3cfyu9o;SPO`SUBi_kYP8@`x1#H>Qwn2Y`**R9mo=SHoTOhuS zORXvP0vVTC*|-ASw=V8)GcLEuq45)oSj@}dlaLJEc5&ICat@-bTPO>sN2oQw zB*=KqIheB6P}ZIPrvw?VSnb`gc-6}Hn`W0`(K)oO4zCP3{da8e!;s&#a>7+lQtsf% zM>mf&ktSRJFym`$4WB&Y8!IQ4@8v5qM^0(xCyDh{`%Iisr6+_tRpFHho`FQ`tESGH zKCKE{M<6dF#AW%(rh=6-<7bfrWc!ZC|7NShp&R~mxs}lUW_7$KavFcwo$V)(|8x$a zfJ=dN3CkoK_N-~~>*UDLw}HGJl+bjnFqcLu^ww(^;ry>!Z{ zR<(><0mL`ZL^|7Q;RR)$V`Vv$FazX#XD$W)L4oDn+T{|fy$F&1Y2{08yL3A%7}C7B zk?;RAS6HhnAg{FY4bqU%Tx**F7l)bGIrAuRBL!}=bKwR%a(P_c=+sl-b_%TFNp+LZ zk~>gn$=z3I$=eyUgrzoZ7{89h|BvL8QT+c_mtT8&X%D%XhPE=(xM23;9;XeN_c&1s z+(Cgm?fVGd*4*6OXWHxf7;GPxoBNxPqCE%m(q80(;E$Ki>t2_ap3ko$j5HjN8f!Zz zPT(A$k*KY1A%{WntSAm~i#fFvH-h5nYt&ZrP}s|S!&yjy^%Qs)_HSBwqv_j?h}8XM zPUc2y@D+?dwDJ$ec$GZW^fJG&>P;~D(#klw*2>7^V>5NV^pWPFDSNi^TIYH>BjvJZ zInI$33{Wr(nXFuZbeNDmkKepyWmt6ufDg6$MMy4&Gs?`xB+A{%MAo{4gTor6Y= z!a9L}&_a@jo9Sh2z&}Q@d}USVpt(nJrsJGN6dXyxagfJbd3lh?PiTY zL|L^~-jUnP_Y&93nruVh!DQ`WWp1@TZep_bw&4?CyN{Kpz}DQwaM!z4oIEd}UwfGu zyi%f4II63`6-r~rLYLn$WF4Y0As=dGyg>9ael})36CTz=Co4qdwem$=He^0@eqE#SLb7hLA(z7H zRx4j=nGdSlZI&M7)y`2Ayo!P`(<@mX7wImUYC(d0x6?+!6%<@)>4tBi%wwsnhwX&? z1y*aV{5!XjJ#i0PqH$kF2QjB)J#MYO)Q4GF|F)X-eZ0&X+t|>~531#}3_iEadeK=- z!G|b_U02piR({?zs`5+qoW$$AtPR%U4OqWzWqDSl1e5ilRlg6DkF5NGuMYA|%g{u| z;2s2XmFbWlouet#K%qrQVUv}cq%Hr>Mcyo_hOjVX{puV;p=JuTXj=}gvyU(WsW$VE z;$FbD^5@u&a&3e!Am`cqcvX>}!2jXl2MTOJ zItL8mRfz|3jqRI2#HR5&ZrEUf2&Go@kXyx3QZNw`j6ah5W=azdIJjQ|!<^$NbQFaa zL$0>6+4Uq|&iVQ!Fy3064ePC}EGKZd6JyKYFGu&)FM%n}2^9K_ zLfj@(t^AerON@&=-KL5&s=y5AL<)UPp>KNk%YoMHpD;Vf%2)K=FC!YbJ+x1v))?EF zz>y?1K|o-cf&ycn=$P|W5C1f9fdb|+7eDmc}YM|U&6;yxsk-Zh|NL#oG>T)f(GD> zC9qDxW$54Jh9-b7k>pz3Q7_iY8M#CbMF{!o=Tg$;b9z?=sf_c`#1 z>aOQm%*5sM50kG513#+W%}8jI0`nrA8;u=rli?p4bU*C=RPX>-nls>sLP1jXgLa=1 z^vGF8*$+|nTE9ac45}LZ9SkXW!Y)kRimEPoFrsEpAx?<`voU6t3YKXTV?PlrC+956 zewDJ{;KD&ssi8~E*s!Buxp}}H+R83)ciNZ-P3}c#4(n>yBlgK#S1Lry6i{JIh2!2IfVfGD=0^s z_bg?ypM&*khs}L3s$jGkwCHEEcdbqi@?fi)jDh`83bv6!E(|wKMtX3GhHeYfr3!Gc zEhF6w53ocKA0A4%@u8LJ6i|^U3J`jN0tDK}d-6awa1~b?JajJQ>`pm2k_?`ufUk;p zjQXc@5MMEhDK5N%%M~18hAJm@k*f9pxLCn~iEQnsY+m3lw!WI2^C;(F%7IrHyhg!- zK3dy9CT>$hY_5a1D>z!4+czJ3y~7NGjwDB1yaz!?0?yI6IwnT8wlyqiXpAn1)_Z{u zxTw#P^AF0wKcJl8a|&X86_tIrXdTb&;CeM&2>|AiB-Utl@F($!0ULsIBis-70b{k`IA?B*hN^KtBRL?pBddj_N;=iiGl1fqS)3 zzJgC}Xo2?Axd<&J=K{+4oN~TFD7OsuFri{qeruJDP=d(B5--wuPJ}>X(%**zfW{>B zdm9QGGYpl3r8zW!KiJLrk#c^r39g57peld1O6;mbgH`#9O=qam&U!x`jutw^vm3d8 z57m&foN_i(u7gCzDKIx>c6sFs@(?&r%s@z?9Te>0-!yk7Kd<0n5}K%16}n@_EKd3$ zp}jR6bQRi10bbPU33un#{Q5TG=1eq1&V>OXN?~vu7&<_K?2Th)LF>-?p(E60XGEw| zFqyOB)^bPVJ30R2;!2M*sJA9LzZs2PN=7A}aefy9@7}qXau1|j?#C7d4V>yU+eAVyOSKvg+@GL|*TfxPNYG8p5ovZ3gFqQbgEOY@mmr(9ylzX|~0SWz6 z)$m$Fmnw+)pCyGrj1qMPY(R_xt~E8}W7W_K)m_K!*zLuY@5xZ8OU=01)+l(;S6BP2 zCG>y>J_O4L6+EJr%UAk4!3)002|li7JZqm&@Vx2KE*HyE=t)(+pp62b?w^$V66LMONJ;lk*!|CfJcsQ#BlgZhKdTx?7}EK_k#i~Kxs;dXuV*%RRMeQL*{*_IsV8sq zvIDBbei9o6Yo4E|XLh#g!kj%9)5HTWO3KBESEL|fs>8!EyOf;ED6fR_2KfEOY>=9$ z2f}8Mf}uj#hKZ6lFWI9sd<2X~D=^3X?9nEd-rnc?**mK_&Z)9@QDE}nxXV}W?3?CQ z4eeFUEge;LRq`^_3mwdjGfAUO0GO-*x7nsAxJ}{bNp0timsEwBum7%GY*YsFUjmmrx3D0l;wz_$(c5zzan-+(NOq z-72}Qz=)z;&2~_nqI38rfK$o2g7Ug34;T1mpl4%8dnM)FLwWagYpZ{#`T@kkhE{Cw zK!vAG&tU}?lyLYY*_SE!kJh)}wz@&hv3JSFmQ@l;T>LLzxxL=$>3v?+{Fb(+x(=`t zR}T~^QQiUoMGE-Awg@-VovQrN*GzY-a+7VQdzJqi5#>H|uA;pEQJ!@QvO%x1f*fq> zZx%Mwx{s1`HRWxle7FJGk0}6+m^Q*OGnesc)n@^KK_$ki{Wt;GpIIv~2Y*g%m?G%} zWWS@~VI=vk0=YO8_NQu#1BPrkpi&m|OBdZKz&tAP`R&;tNik*u z_(j3rdiS#Vt$=QOxtt6&-!1OtWGa}Uan*0{wn%03sd-MZ8qWfVC^!Pc*GE^TtjQf+ z&M>u`i%8W99nF zK$I&e|18Qs4|Y=(oNoqC&l>Nm!JV+3so)AT!Ob%sds>ro5IHL;|0>FdGlR{Z1=l5( zi~irO|yS0jpsJs+1DmiRO|cq*$MI zK3xshA=ViR)|)zWQ)9E8vrN?+(CXX}a=^eX4<1}a`R`CZHb*&d4+VVE`-mXtG7bC; zmj6=ltyH4h5kU@oMM>v7n8H^Suvu$jiZIHYaZGB!Q8G{B9FXRZxr!8$o0O#y*E z3IaxyKpwz%C@=?>_&%UVvfw~5#%3kwJ_S|XiZT>G{nWvycsX%$uA}fE3S&2s10EDG zimzFEol{oNT-@9&7oa)Ms_7WS!&Y44?Ji3TH~5&=7JQv@%>L1~dinG@=N0@?5``yG z7%%Q}K#c% zeyE_u+!N`KbmYLz6eA{vFBL3?$7h!{-z!RdSe$rEkFBANh(ouWUlo{-J$exBBsAz? zJi9~rbF^Ua$@>(YEjPxzZ2nv`H(!HJ1qdrRO&XDhvV6PEQjotY!Cno9AX7>3 zU)mt(%{IJ@F%6PiNzRQFzJkKnz;=Lw8@n|~F1S^Ua4MP$ZWVC*Up7cC*ivlQA|lvQ zz>8Y7q?a(n-yrZ3#SjxccMk>TaFv@84U)41HwZSuV)r1xGzAa+MT3C%B$j+e#0kwN zCr0_FQ~sF%`;l`Kgu5s!xQ;wjpsS4+;H7MmhYG3N%0mTrS3FdR;Tkxni&L6I z&S{kWH_AQ&09)ysDf|Y7-?SGfz2-=-JFDDAO#tq4Zj*vTxi{q^!EaiWmy~Kh7 zCs`-^U_sL#Za8XYbUqho1v$4q#^{ziZF^Ud>w&_Xh=}D*F*+fS$hm_GPNM>xrsSn4xS(H;EH78>mLpQ0f@?G*Y=iwBomZ+BF_@Pr zxRK3yAhSD`SD|V=-QlsF2gZ_uEd!`hfQOIvICaFlh0%G7X3cGB^6>frHY3TolL~I7 zf;9lx8w&tu^ztpMlZV22aOK1hJeIeUg6Fgyon0HG+GPnPV&HS zlG@9*7JI6Klh{kaD~Vc+m0C0`Xs+v6+!pn67qTIU>@F&JjS6tknKw%T-X-^2l{z*2 z3bE!Y_^n%2-r(eq)Ue+H7Fke;ZH~VxU?@|DLcTZWSUB60LSb)~*Q%l1g&f+Z!|fCR zP}SX3=ushFc5yiWszV;gN|c#ac?pzDRhh+hP{0+GAwQc7qhEa{pXueDM$SD{7@)#D zSf8$-n9DA^v`{*ybFPL|0f5US&Yu13((*1ng#aX8Y#U-jl6s-{@@^AA&y_{d_*Yj8e1HOXWYVuA++?4&vRV)nYz z%s*buamkW@f`Yv@uCBrU?gW`|dBIqc(R2VXmH_yNy-45JxtFWm;fQpFf=0~_^V^>j zXLGF@H33|wz+Bk!EaJsDA3IXX{TNn30S8PJ(*VF|0`T)*CiGTS?1RwT6dZ?8P7r4! z`KwiVyj9|CBp|u&PK31$h($Hb3Zu`Q_)HI2jBq(CmT-f z!nYPD8ZyeyV_WQNWom4~b7Q=|M!*F8SXnxY1Xs{+Z=>je;#<{vQg=SJm8j$-x%} z(Mjp=jeB7boq&gX z!CqpA85}-Q!352&H@AeBsSzBQ@L3A>;TUH6fLp@nsCt@AnlPwNvKfrfe<+x4LoZOp zAcS@*n8E2`f^o?%R3&=>tOU1&u`!iIXIf_ztzdTDc_*GL$4v?Sos!H8i*L zS5ba)34{6`r{V*s_z;xfQ3dkLZ|b)AUa5xvt0s$K|EvOYYOcQ}?S)@f{fRJoMFGyi z6V>XMUBd5a6zmhi?<+Wuhe!{00oh6M&xa+*PQb;zaxxr|SKWhMdhE@%>+j%?iGO31O&|x=HpC#_E-Q1?mraCrbs>v;lK&N&n@+y7X^hH z3|9n45!QBaBGJU5bU_6vyS^k$IZ4|-zX#vPmz-irEgTG@c*k1#2 zK3i~rg7a@K&sRFzov-}7xw!i?*ptWh!x~Bn9TTDabJ9>iHCaBE<+dvjul62>5E&ots9qv_{(!B?3dfLM0(8 z$wBPL6qq*~-I=KXo}yTm!txmfRa?%dV<9%#3tJlb!4!^l@b@y545AXO#Rab`Fn6|` zkNxrAf_F5e24?Rn*lLTJRJY?0Nq*q1;0tnIqmpf?WIM$DQo*j;TAIZV8p5RTqXOLB z^x$&Cu=!iLlikTZcZQuYJ-sN*B`4fTsoY~iNR2SClrk4j7$sg;CNEic#mn6+$>M2axAN$0RNbjN`3)= z3n$6e*wE*6Qea&ye4d;QR63qYx8d|K%duD$u2bc5FR5}nwu9moP3J%^q3p}@ z4&adWq;P|R`Tgw63O`Y+J^S336@IJXxF9aX4p!2iroDhMW)52Ir!u^> za6vY!;S7LZ70mI)QmQI)HMB176~W_^45EqF_Z_r$x!bs-&GK7#7;-5J;;+=9l1Wqo z@24n4&O21PfJzr8mtXw_7xQUy5q7g;z9-B}6=3_4px7zW`LwGg&l)oDZn<5-Np9 zQnasvD>eDCyt0|=ky#q!KsCA=F%D92Yp=Y_$`!t}$KJrcWKIkR(BX$v8mH1Hks9bw zz%wTQEw5Zf$E)RYh=$FwMBBj8{%Ykaf)gg@ZzC?KPQce1H?eYA_i0MKSGb0o3Pko1 zm3~L1{{y(vf--*SYUX60pFS0V$V{ZNR7AQ(L7rx29_=K1;~q7@#-`|A1?GaK$L|am zJ*1)7*AzXhU?2v!dADOYrU*gUiAn(m37r(dy^?BdLMXUULI)?fd_-A+a8hLR$v~ya|iXP_or&G2dqC+@ORTnoyh`mX};D_Y?X?> zQGgdLJ?j0v2JLU_4JS)Vj_qC1CIts1>OEYXICOUr|IT*NpX7W(Wd~6ij(3Y41^hI2 z%iUd^t#(HuQjP+1!rQyr#ULfg0w3WNmnk?=s+}kg3R7IAfhPeBRIsdPt&71%lHyrX z>*8UmI1CZtvq|XLHgtrJ{%;UElAKSe>|842ts75gTp1USRwb^bjq(mC$Ep&Cy1Yac zgVSUdJ%$Nrm#BRHY@-++-e**{oXX(274M+niGG%-Vvw0cS<&YbRlK)`uSAmjC@@#r z))_QMhqHQ7Z}I+Wd^2Jmpx|Mhj9A>1mx|%eNg*DAX`O<{+0-;cpUYGc-@_CyBxfU) z-cF@!0UF5poXY-9W%`%Z_-Mi`Q^kwb9Czo%OB7)H*T*uoHRtWo$mFiD7)&V3$(gz< zEQX&bsw4Z@6^=H0=Y3jm3~zoaviKbS3+2k*r?QWbKiok9-*HX*SdWS?QHx(;{!a_a z$(ZYURD6}{!Ft74D{y^v>b@Q&{$e+;7=&p`P@aaUAWQ+~5Qw*EW16JoIBhe3*Pd65 z^E$D~Mua;Rn3r1JJ`y(TQNefpuSdo3@xG?=F;tG@&ElsOY;UT!<@KoeHMQIc(Oy?D zg`@q|dQ|+enomVs_-B&;AsScLBmZTRR~+Dx1{V8<$`7YuQM1EJk3-j(gaWZzQxrBsf)^~i7q_zbyM&3z9}BHO7UwuO=H72La} zs_(8A_rV;jBss1%=AFaaZHc%zdubrpA~Hq6e|ps$)Fp{LW$Qgn6;~kQbOlc*>RsI& z?O587K&K3NqnAmS+seofSR7!XVj{p33KsRVAV*$M zD>!6*+~q~y&~Uh9kvA30)Mgw(t;?v9zh8>PO9($VuIrY2#dyK zMp{>}*F%<)PBjOymMpiRGF5Z!(<6x7%?)>r8pC@ki75#8!IQpjyIt)H5oxsob4A89 ziKgi}BtSx@K9x8}FS%cVd5&l1BaYBY9#r)>RzdNwDsaG8vQ_~u!cAyg6)r*_Rlux? za2MWK37AVVWwK=DWGMfwNTz zn(+IY<_C>RJ|*W@s@#z(cSY3C6d32RuSt)qgWsrfHdXEqbI_ZBLv=pNr2kd**u9qg zrU2i9YCU_r4)4*!r6983sj`kL;aZkvDll)MnOTyfrxXsNShm74Pr;Hc<}|!L%C8R1 z->g>(SMLw1Jen$xMdV5a@~b%Avl*&^r^6DvT8VZ}uWSs96xX}`z7Q|n89YywS5xJ+ zNNpDdW)InOckg>4UOGiBZ$Y%F3KHK?_~@Y5q%Or@ge+O&khc_nqp^Tz`<>J!Ct@^} zE+qV^e1R%o0ccS0e!r8tbg9~HM5Ln?B>1QIq%J*0jlepkrz$XC9hl*dNnLuns`2)a zr)}w(s(2Y-nSwtPu3VzGy^gG=Q0n2Oyla z(z{j3gvLr7@0Q-HO6&cV-mmGzl73myE}TQ0$fW^W(}0};9#t@}pCzyKS+$zj=aN_Y zs)kQOlCLS4>T5RRm+je|j!NHEOSp5T?4yFzI^vg zD3b=fN&~QIEdv7zc-K_S$Fn`&_?0bIOSoNSOB5t%&lj^7LzkVQ<{-c_5Sz)es(-hu z^k#0Mi;1$RDvhcF0GBAhJJeps5y^$KY?a!TBGR=AhI0-3cDu?zT9OTpu*+^&V16&l z40$kg8K_Ha_)d|_2I>+;767PA04^8p-L3SqSNbv=KTH&$svW2bu1MKh1(W)!NaE(U z3_K)}_CTcP6wKC&*f#I$O1o@>TJMLbZ!0jjfwm2j!|-Db#wM`r69wi?U843NBq>TG zR6Ozq!1fDOHn9qduT-%Mz}E_zZRoe!^DaWaBMMSgD^;~~dPoqoUiO13JFN0#C^xBc zG221$lMbiXlU!fSzYSIP2T_QsmQdA+0Dmf2*$>y3XQ>r_y}FM*czIaE!A<2LDoOtz z+6P!7NAn(hW|GdzE7S^00*Ib z98oq^t)i-Hx4`q|36^>i<_VU1fm^;8&li~v`PuTkN7+fDFQ25@UZzA}zK5z#>x1ZP zTHB&a8lp@64FnF$rK;kj}APBVlI0f(;6D{f6jF;TsV>904p0>P`D37Xs zpsHVx8k{%*#yRZAVJ^SgS`Ks&?HUC+IxPF6<8lz3$z@Fi_+6xeG317At>P zjfViNRRG7VA381vi%AaHYnFq>1WeHEdOOVJaH+&-cf@#ILANK@o08lU;inqqUlQfh zz*#i#5C9ONfHV8!o}blfUZ30p9tSwMb#Yi>(5ay#4 zeC3-sdIab`X|jGec>q{3UhN;#1Hg){RdszI2Y|_UmKD1Z716+3X&{%mR>3`7Lo>xX ztNCxzp4`ko##I5IOpM`^Rlp|`5Z9KwG3J%?wxR+~nFKuw074V+7|t|oyCwc=`ilKD P^j7mRJ`OFs+S>mILqc3_ diff --git a/container-stack/svalinn/src/lib/ocaml/Client.cmj b/container-stack/svalinn/src/lib/ocaml/Client.cmj deleted file mode 100644 index 8685d74f100aa8622b62a40a47c0ddc7fde5066a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 479 zcmZWly-ve05H<^fjhTs|YAf0%42U_QAk=|{@&aypO-&s;oG&S5WUk8kh*`}58 z1+1$oj-qJO%7se(&`zWPMeE8jZ3Ry>ntHA?L!Di#($C$5OL?!gw7}5myQrm{@MPhE zQz;CMGM<9*rk)|4-)bdDqs1>F6N&!aD5uI=@jM&rGVw#u72GsM96drjluT~O)<#?jYXg0R#$#ZG`=f(H{tzcNa_ A=l}o! diff --git a/container-stack/svalinn/src/lib/ocaml/Client.res b/container-stack/svalinn/src/lib/ocaml/Client.res deleted file mode 100644 index 11987f0..0000000 --- a/container-stack/svalinn/src/lib/ocaml/Client.res +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Vörðr MCP client for Svalinn - -open VordrTypes - -// Client configuration -type clientConfig = { - endpoint: string, - timeout: int, -} - -// Client instance -type t = { - config: clientConfig, - mutable requestId: int, -} - -// Create client -let make = (config: clientConfig): t => { - {config, requestId: 0} -} - -// Default client configuration -let defaultConfig: clientConfig = { - endpoint: "http://localhost:8080", - timeout: 30000, -} - -// Create from environment -let fromEnv = (): t => { - let endpoint = switch Js.Dict.get(%raw(`Deno.env.toObject()`), "VORDR_ENDPOINT") { - | Some(e) => e - | None => defaultConfig.endpoint - } - make({...defaultConfig, endpoint}) -} - -// Generate next request ID -let nextId = (client: t): int => { - client.requestId = client.requestId + 1 - client.requestId -} - -// Make MCP request -let callTool = async (client: t, toolName: string, args: Js.Json.t): Js.Json.t => { - // Build params object safely - let paramsDict = Js.Dict.empty() - Js.Dict.set(paramsDict, "name", Js.Json.string(toolName)) - Js.Dict.set(paramsDict, "arguments", args) - - let requestId = nextId(client) - let requestBody = Js.Json.object_(Js.Dict.fromArray([ - ("jsonrpc", Js.Json.string("2.0")), - ("method", Js.Json.string("tools/call")), - ("params", Js.Json.object_(paramsDict)), - ("id", Js.Json.number(Belt.Int.toFloat(requestId))), - ])) - - // Make HTTP request to Vörðr - let response = await Fetch.fetch( - client.config.endpoint, - { - "method": "POST", - "headers": {"Content-Type": "application/json"}, - "body": Js.Json.stringify(requestBody), - }, - ) - - let json = await Fetch.Response.json(response) - // Validate response structure before casting - let mcpResp: mcpResponse = switch json->Js.Json.decodeObject { - | Some(obj) => { - // Check for required jsonrpc field - let jsonrpc = obj->Js.Dict.get("jsonrpc")->Belt.Option.flatMap(Js.Json.decodeString) - let id = obj->Js.Dict.get("id")->Belt.Option.flatMap(Js.Json.decodeNumber) - let result = obj->Js.Dict.get("result") - let error = obj->Js.Dict.get("error")->Belt.Option.flatMap(Js.Json.decodeObject)->Belt.Option.map(errObj => { - code: errObj->Js.Dict.get("code")->Belt.Option.flatMap(Js.Json.decodeNumber)->Belt.Option.map(Belt.Float.toInt)->Belt.Option.getWithDefault(-1), - message: errObj->Js.Dict.get("message")->Belt.Option.flatMap(Js.Json.decodeString)->Belt.Option.getWithDefault("Unknown error"), - data: errObj->Js.Dict.get("data"), - }) - - { - jsonrpc: jsonrpc->Belt.Option.getWithDefault("2.0"), - id: id->Belt.Option.map(Belt.Float.toInt)->Belt.Option.getWithDefault(0), - result, - error, - } - } - | None => raise(Js.Exn.raiseError("Invalid MCP response: expected JSON object")) - } - - switch mcpResp.error { - | Some(err) => Js.Exn.raiseError(err.message) - | None => - switch mcpResp.result { - | Some(r) => r - | None => Js.Json.null - } - } -} - -// Ping Vörðr to check connectivity -let ping = async (client: t): bool => { - try { - let _ = await Fetch.fetch( - `${client.config.endpoint}/health`, - %raw(`{method: "GET"}`), - ) - true - } catch { - | _ => false - } -} - -// Container operations -let listContainers = async (_client: t): array => { - // Vörðr doesn't have a list tool, we'd need to track locally - // For now return empty - [] -} - -let listImages = async (_client: t): array => { - // Same as above - would need local tracking - [] -} - -let runContainer = async ( - client: t, - request: Gateway.Types.runRequest, -): Gateway.Types.containerInfo => { - // First create - let nameJson = switch request.name { - | Some(n) => Js.Json.string(n) - | None => Js.Json.null - } - let createArgs = Js.Json.object_(Js.Dict.fromArray([ - ("image", Js.Json.string(request.imageName)), - ("name", nameJson), - ("config", Js.Json.object_(Js.Dict.fromArray([ - ("privileged", Js.Json.boolean(false)), - ("readOnlyRoot", Js.Json.boolean(true)), - ]))), - ])) - let createResult = await callTool(client, toolContainerCreate, createArgs) - - // Then start - let containerId = switch Js.Json.decodeObject(createResult) { - | Some(obj) => switch Js.Dict.get(obj, "containerId") { - | Some(v) => switch Js.Json.decodeString(v) { - | Some(s) => s - | None => raise(Js.Exn.raiseError("containerId is not a string")) - } - | None => raise(Js.Exn.raiseError("Response missing containerId")) - } - | None => raise(Js.Exn.raiseError("Invalid response format")) - } - let _ = await callTool(client, toolContainerStart, Js.Json.object_(Js.Dict.fromArray([("containerId", Js.Json.string(containerId))]))) - - { - id: containerId, - name: request.name->Belt.Option.getWithDefault(containerId), - image: request.imageName, - imageDigest: request.imageDigest, - state: Gateway.Types.Running, - policyVerdict: "allowed", - createdAt: Some(Js.Date.now()->Belt.Float.toString), - startedAt: Some(Js.Date.now()->Belt.Float.toString), - } -} - -let verifyImage = async ( - client: t, - imageRef: string, - _digest: string, -): Gateway.Types.verificationResult => { - let args = Js.Json.object_(Js.Dict.fromArray([ - ("image", Js.Json.string(imageRef)), - ("checkSbom", Js.Json.boolean(true)), - ("checkSignature", Js.Json.boolean(true)), - ])) - let result = await callTool(client, toolVerifyImage, args) - // NOTE: MCP JSON-RPC result cast — future work: decode with pattern matching - Obj.magic(result) -} - -let stopContainer = async (client: t, containerId: string): unit => { - let _ = await callTool(client, toolContainerStop, Js.Json.object_(Js.Dict.fromArray([("containerId", Js.Json.string(containerId))]))) -} - -let removeContainer = async (client: t, containerId: string): unit => { - let _ = await callTool(client, toolContainerRemove, Js.Json.object_(Js.Dict.fromArray([("containerId", Js.Json.string(containerId))]))) -} - -let inspectContainer = async (_client: t, containerId: string): Gateway.Types.containerInfo => { - // Placeholder - would call Vörðr's inspect if available - { - id: containerId, - name: containerId, - image: "unknown", - imageDigest: "", - state: Gateway.Types.Running, - policyVerdict: "unknown", - createdAt: None, - startedAt: None, - } -} - -// Authorization operations -let requestAuthorization = async ( - client: t, - operation: string, - threshold: int, - signers: int, -): Js.Json.t => { - let args = Js.Json.object_(Js.Dict.fromArray([ - ("operation", Js.Json.string(operation)), - ("threshold", Js.Json.number(Belt.Int.toFloat(threshold))), - ("signers", Js.Json.number(Belt.Int.toFloat(signers))), - ])) - await callTool(client, toolRequestAuth, args) -} - -let submitSignature = async ( - client: t, - share: signatureShare, -): Js.Json.t => { - let args = Js.Json.object_(Js.Dict.fromArray([ - ("requestId", Js.Json.string(share.requestId)), - ("signature", Js.Json.string(share.signature)), - ("signerId", Js.Json.string(share.signerId)), - ])) - await callTool(client, toolSubmitSignature, args) -} - -// Monitoring operations -let startMonitor = async (client: t, config: monitorConfig): Js.Json.t => { - let args = Js.Json.object_(Js.Dict.fromArray([ - ("containerId", Js.Json.string(config.containerId)), - ("syscalls", Js.Json.boolean(config.syscalls)), - ("network", Js.Json.boolean(config.network)), - ("filesystem", Js.Json.boolean(config.filesystem)), - ])) - await callTool(client, toolMonitorStart, args) -} - -let stopMonitor = async (client: t, containerId: string): Js.Json.t => { - await callTool(client, toolMonitorStop, Js.Json.object_(Js.Dict.fromArray([("containerId", Js.Json.string(containerId))]))) -} - -let getAnomalies = async ( - client: t, - containerId: string, - severity: string, -): Js.Json.t => { - let args = Js.Json.object_(Js.Dict.fromArray([ - ("containerId", Js.Json.string(containerId)), - ("severity", Js.Json.string(severity)), - ])) - await callTool(client, toolGetAnomalies, args) -} - -// Reversibility operations -let rollback = async (client: t, containerId: string, steps: int): Js.Json.t => { - let args = Js.Json.object_(Js.Dict.fromArray([ - ("containerId", Js.Json.string(containerId)), - ("steps", Js.Json.number(Belt.Int.toFloat(steps))), - ])) - await callTool(client, toolRollback, args) -} - -let previewRollback = async (client: t, containerId: string): Js.Json.t => { - let args = Js.Json.object_(Js.Dict.fromArray([("containerId", Js.Json.string(containerId))])) - await callTool(client, toolPreviewRollback, args) -} - -// Export default client instance -let client = fromEnv() diff --git a/container-stack/svalinn/src/lib/ocaml/Deno.ast b/container-stack/svalinn/src/lib/ocaml/Deno.ast deleted file mode 100644 index b2310ae857799fe1c9f331c0e50e81d1df36f0ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7635 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 diff --git a/container-stack/svalinn/src/lib/ocaml/Deno.res b/container-stack/svalinn/src/lib/ocaml/Deno.res deleted file mode 100644 index 380447f..0000000 --- a/container-stack/svalinn/src/lib/ocaml/Deno.res +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Deno runtime bindings for ReScript - -// Environment -module Env = { - @scope(("Deno", "env")) @val - external get: string => option = "get" - - @scope(("Deno", "env")) @val - external set: (string, string) => unit = "set" - - @scope(("Deno", "env")) @val - external toObject: unit => Js.Dict.t = "toObject" -} - -// File system -module Fs = { - @scope("Deno") @val - external readTextFile: string => promise = "readTextFile" - - @scope("Deno") @val - external writeTextFile: (string, string) => promise = "writeTextFile" - - @scope("Deno") @val - external remove: string => promise = "remove" - - @scope("Deno") @val - external mkdir: (string, {..}) => promise = "mkdir" - - type fileInfo = { - isFile: bool, - isDirectory: bool, - size: int, - } - - @scope("Deno") @val - external stat: string => promise = "stat" -} - -// HTTP server -module Http = { - type conn<'a> = { - remoteAddr: Js.t<'a>, - } - - type request = { - method: string, - url: string, - headers: Fetch.Headers.t, - body: option, - } - - type serveOptions<'signal> = { - port: int, - hostname: option, - signal: option<'signal>, - } - - // TLS-enabled serve options — includes cert/key for native HTTPS - type serveTlsOptions<'signal> = { - port: int, - hostname: option, - signal: option<'signal>, - cert: string, - key: string, - } - - @scope("Deno") @val - external serve: ( - (Fetch.Request.t) => promise, - serveOptions<'a>, - ) => {..} = "serve" - - // Deno.serve with TLS options — starts an HTTPS server - @scope("Deno") @val - external serveTls: ( - (Fetch.Request.t) => promise, - serveTlsOptions<'a>, - ) => {..} = "serve" -} - -// Standard I/O -module Io = { - @scope("Deno") @val - external stdin: {..} = "stdin" - - @scope("Deno") @val - external stdout: {..} = "stdout" - - @scope("Deno") @val - external stderr: {..} = "stderr" -} - -// Process -@scope("Deno") @val -external exit: int => unit = "exit" - -@scope("Deno") @val -external args: array = "args" - -// AbortController binding -module AbortController = { - type t - type signal - - @new external make: unit => t = "AbortController" - - @get external signal: t => signal = "signal" - @send external abort: t => unit = "abort" -} diff --git a/container-stack/svalinn/src/lib/ocaml/Fetch.ast b/container-stack/svalinn/src/lib/ocaml/Fetch.ast deleted file mode 100644 index 15d17250a47e0a1418beba58ae8a3019005f3d78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7329 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 diff --git a/container-stack/svalinn/src/lib/ocaml/Fetch.cmj b/container-stack/svalinn/src/lib/ocaml/Fetch.cmj deleted file mode 100644 index c01f4ea56137e313ff03f9f4423aaa7f219f29d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81 zcmbR2F$WOK0x^oz?9b29Vtbkh?{ hQp*!77i@6U($i1M%uC74OD|T}D@rZa%PMwo004kP8`}T? diff --git a/container-stack/svalinn/src/lib/ocaml/Fetch.res b/container-stack/svalinn/src/lib/ocaml/Fetch.res deleted file mode 100644 index c15fc88..0000000 --- a/container-stack/svalinn/src/lib/ocaml/Fetch.res +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Fetch API bindings for ReScript - -// Headers -module Headers = { - type t - - @new external make: unit => t = "Headers" - - external fromObject: {..} => t = "%identity" - - @send external get: (t, string) => option = "get" - @send external set: (t, string, string) => unit = "set" - @send external has: (t, string) => bool = "has" - @send external delete: (t, string) => unit = "delete" -} - -// Body -module Body = { - type t - - external string: string => t = "%identity" - external json: Js.Json.t => t = "%identity" -} - -// Request -module Request = { - type t - - @new external make: (string, {..}) => t = "Request" - - @get external method_: t => string = "method" - @get external url: t => string = "url" - @get external headers: t => Headers.t = "headers" - - @send external json: t => promise = "json" - @send external text: t => promise = "text" - @send external clone: t => t = "clone" -} - -// Response -module Response = { - type t - - @new external make: (string, {..}) => t = "Response" - - @scope("Response") @val - external json_: (Js.Json.t, {..}) => t = "json" - - @scope("Response") @val - external error: unit => t = "error" - - @scope("Response") @val - external redirect: (string, int) => t = "redirect" - - @get external ok: t => bool = "ok" - @get external status: t => int = "status" - @get external headers: t => Headers.t = "headers" - - @send external json: t => promise = "json" - @send external text: t => promise = "text" -} - -// Fetch options -type method_ = [#GET | #POST | #PUT | #DELETE | #PATCH | #HEAD | #OPTIONS] - -type fetchOptions = { - method: method_, - headers: option, - body: option, -} - -// Fetch function -@val external fetch: (string, {..}) => promise = "fetch" diff --git a/container-stack/svalinn/src/lib/ocaml/Gateway.ast b/container-stack/svalinn/src/lib/ocaml/Gateway.ast deleted file mode 100644 index 41b4424159c940bbb9926e74a48c56eb2565ee1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 149589 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 diff --git a/container-stack/svalinn/src/lib/ocaml/Gateway.cmj b/container-stack/svalinn/src/lib/ocaml/Gateway.cmj deleted file mode 100644 index ad2ef48274cf939fcf7ed044671227c5563e2383..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 449 zcmZXQy-ve06ot*kDQc3PF02WW?mh)m3$7!g7| z1v?AxfXYq6uVTr6*E-)hzKg}q`^)Ox!|U6}^L)5c-pubG8C$w#jD@sUGk98NtjFxy zaTd?$*kNVqao$=Es+!+gxLwzPBbe3#Za4f)oy(eZL?lwRhTRH4q$cqb2rp;f!fimz zFc2zA0jpxUPfFWG7#5%nrHlX8odJkQD$x2r#_fYa8R|n7<(X9Ghwo|#3zTL;<`JO& zwQxrwm60$oXsqa^ifwnV8dP6KlxZxm{$vwtFGL`Pm*;2FB&Q7^t8-V^4(9VVC{1g04uEPKT diff --git a/container-stack/svalinn/src/lib/ocaml/Gateway.res b/container-stack/svalinn/src/lib/ocaml/Gateway.res deleted file mode 100644 index 2fea232..0000000 --- a/container-stack/svalinn/src/lib/ocaml/Gateway.res +++ /dev/null @@ -1,1219 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Svalinn Edge Gateway - Main HTTP server - -// Configuration -module Config = { - @scope(("Deno", "env")) @val external getEnv: string => option = "get" - - let parseIntWithBounds = ( - value: option, - defaultValue: int, - ~min: int, - ~max: int, - ): int => - switch value->Belt.Option.flatMap(Belt.Int.fromString) { - | Some(v) if v >= min && v <= max => v - | Some(v) if v < min => min - | Some(v) if v > max => max - | _ => defaultValue - } - - let port = getEnv("SVALINN_PORT") - ->Belt.Option.flatMap(Belt.Int.fromString) - ->Belt.Option.getWithDefault(8000) - - let host = getEnv("SVALINN_HOST")->Belt.Option.getWithDefault("0.0.0.0") - - let vordrEndpoint = getEnv("VORDR_ENDPOINT")->Belt.Option.getWithDefault("http://localhost:8080") - - let rokurEndpoint = getEnv("ROKUR_ENDPOINT")->Belt.Option.getWithDefault("http://localhost:9090") - - let rokurGateEnabled = switch getEnv("ROKUR_GATE_ENABLED") { - | Some("false") => false - | _ => true - } - - let rokurApiToken = getEnv("ROKUR_API_TOKEN")->Belt.Option.getWithDefault("") - - let rokurTimeoutMs = parseIntWithBounds(getEnv("ROKUR_TIMEOUT_MS"), 2000, ~min=100, ~max=30000) - - let rokurRetryCount = parseIntWithBounds(getEnv("ROKUR_RETRY_COUNT"), 1, ~min=0, ~max=5) - - let specVersion = getEnv("SPEC_VERSION")->Belt.Option.getWithDefault("v0.1.0") - - let enableAuth = switch getEnv("AUTH_ENABLED") { - | Some("true") => true - | _ => false - } - - let logLevel = getEnv("LOG_LEVEL")->Belt.Option.getWithDefault("info") - - let rateLimitWindowMs = parseIntWithBounds(getEnv("RATE_LIMIT_WINDOW_MS"), 60000, ~min=1000, ~max=300000) - let rateLimitMaxRequests = parseIntWithBounds(getEnv("RATE_LIMIT_MAX_REQUESTS"), 100, ~min=1, ~max=10000) - - let tlsCertFile = getEnv("TLS_CERT_FILE") - let tlsKeyFile = getEnv("TLS_KEY_FILE") - let tlsEnabled = Belt.Option.isSome(tlsCertFile) && Belt.Option.isSome(tlsKeyFile) -} - -@scope("AbortSignal") @val external timeoutSignal: int => 'a = "timeout" - -// CORS allowed origins (parsed once at startup) -let allowedOrigins: array = { - switch Deno.Env.get("ALLOWED_ORIGINS") { - | Some(str) if str != "" => Js.String2.split(str, ",") - | _ => [] - } -} - -// Logging -module Log = { - type level = Debug | Info | Warn | Error - - let levelToString = (level: level): string => { - switch level { - | Debug => "DEBUG" - | Info => "INFO" - | Warn => "WARN" - | Error => "ERROR" - } - } - - let severity = (level: level): int => { - switch level { - | Debug => 10 - | Info => 20 - | Warn => 30 - | Error => 40 - } - } - - let configuredThreshold = (): int => { - switch Config.logLevel { - | "debug" => 10 - | "info" => 20 - | "warn" => 30 - | "error" => 40 - | _ => 20 - } - } - - let shouldLog = (level: level): bool => { - severity(level) >= configuredThreshold() - } - - let log = (level: level, message: string, ~metadata: option=?, ()) => { - if shouldLog(level) { - let timestamp = %raw(`new Date().toISOString()`) - let logObj = switch metadata { - | Some(meta) => - Js.Json.object_( - Js.Dict.fromArray([ - ("timestamp", Js.Json.string(timestamp)), - ("level", Js.Json.string(levelToString(level))), - ("message", Js.Json.string(message)), - ("metadata", meta), - ]) - ) - | None => - Js.Json.object_( - Js.Dict.fromArray([ - ("timestamp", Js.Json.string(timestamp)), - ("level", Js.Json.string(levelToString(level))), - ("message", Js.Json.string(message)), - ]) - ) - } - Js.Console.log(Js.Json.stringify(logObj)) - } - } - - let debug = (message: string, ~metadata: option=?, ()) => - log(Debug, message, ~metadata?, ()) - - let info = (message: string, ~metadata: option=?, ()) => - log(Info, message, ~metadata?, ()) - - let warn = (message: string, ~metadata: option=?, ()) => - log(Warn, message, ~metadata?, ()) - - let error = (message: string, ~metadata: option=?, ()) => - log(Error, message, ~metadata?, ()) -} - -// Health check endpoint -module HealthCheck = { - let handler = async (c: Hono.Context.t<'env, 'path>): Hono.Response.t => { - // Check Vörðr connectivity - let vordrConnected = try { - let response = await Fetch.fetch(Config.vordrEndpoint ++ "/health", %raw(`{}`)) - Fetch.Response.ok(response) - } catch { - | _ => false - } - - let status = if vordrConnected {"healthy"} else {"degraded"} - - Hono.Context.json( - c, - Js.Json.object_( - Js.Dict.fromArray([ - ("status", Js.Json.string(status)), - ("version", Js.Json.string("0.1.0")), - ("vordrConnected", Js.Json.boolean(vordrConnected)), - ("specVersion", Js.Json.string(Config.specVersion)), - ("timestamp", Js.Json.string(%raw(`new Date().toISOString()`))), - ]) - ), - ~status=200, - () - ) - } -} - -// Readiness check endpoint -module ReadinessCheck = { - let handler = async (c: Hono.Context.t<'env, 'path>): Hono.Response.t => { - // Check if Vörðr is reachable - let ready = try { - let response = await Fetch.fetch(Config.vordrEndpoint ++ "/health", %raw(`{}`)) - Fetch.Response.ok(response) - } catch { - | _ => false - } - - if ready { - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("ready", Js.Json.boolean(true))])), - ~status=200, - () - ) - } else { - Hono.Context.json( - c, - Js.Json.object_( - Js.Dict.fromArray([ - ("ready", Js.Json.boolean(false)), - ("reason", Js.Json.string("Vörðr unavailable")), - ]) - ), - ~status=503, - () - ) - } - } -} - -// Metrics endpoint — returns real Prometheus-format metrics -module MetricsEndpoint = { - let handler = async (c: Hono.Context.t<'env, 'path>): Hono.Response.t => { - // Refresh the containers_active gauge from Vordr (best-effort) - await Metrics.refreshContainersActive(Config.vordrEndpoint) - - // Format all metrics in Prometheus text exposition format - let body = Metrics.formatPrometheus() - - Hono.Context.header(c, "Content-Type", "text/plain; version=0.0.4; charset=utf-8") - Hono.Context.text(c, body, ~status=200, ()) - } -} - -// Request logging middleware -let requestLogger = (): Hono.middleware<'env, 'path> => { - async (c, next) => { - let req = Hono.Context.req(c) - let method = Hono.Request.method_(req) - let url = Hono.Request.url(req) - let start = Js.Date.now() - - Log.info( - "Incoming request", - ~metadata=Js.Json.object_( - Js.Dict.fromArray([("method", Js.Json.string(method)), ("url", Js.Json.string(url))]) - ), - () - ) - - await next() - - let duration = Js.Date.now() -. start - Log.info( - "Request completed", - ~metadata=Js.Json.object_( - Js.Dict.fromArray([ - ("method", Js.Json.string(method)), - ("url", Js.Json.string(url)), - ("duration_ms", Js.Json.number(duration)), - ]) - ), - () - ) - } -} - -// NOTE: CORS handling is now part of securityHeaders() middleware below. -// The old standalone cors() middleware has been removed to avoid -// duplicate header setting and to ensure CORS + security headers -// are always applied together in a single middleware pass. - -// Error handler middleware — also increments the error metric counter. -let errorHandler = (): Hono.middleware<'env, 'path> => { - async (c, next) => { - try { - await next() - } catch { - | Js.Exn.Error(e) => { - Metrics.increment(Metrics.requestsErrorsTotal) - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Internal server error") - Log.error("Request error", ~metadata=Js.Json.object_(Js.Dict.fromArray([ - ("error", Js.Json.string(message)) - ])), ()) - - let _ = Hono.Context.json( - c, - Js.Json.object_( - Js.Dict.fromArray([ - ("error", Js.Json.string("Internal Server Error")), - ("message", Js.Json.string(message)), - ]) - ), - ~status=500, - () - ) - } - } - } -} - -// Security headers middleware — applies OWASP security headers + CORS -// to every response. Runs early in the chain (before route handlers). -let securityHeaders = (): Hono.middleware<'env, 'path> => { - async (c, next) => { - // HSTS: Enforce HTTPS for 1 year, include subdomains, enable preload - Hono.Context.header(c, "Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") - - // Clickjacking protection: Deny all framing - Hono.Context.header(c, "X-Frame-Options", "DENY") - - // MIME sniffing protection - Hono.Context.header(c, "X-Content-Type-Options", "nosniff") - - // XSS filter (legacy browsers — modern browsers use CSP) - Hono.Context.header(c, "X-XSS-Protection", "1; mode=block") - - // Content Security Policy: Strict self-only policy - Hono.Context.header( - c, - "Content-Security-Policy", - "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'", - ) - - // Referrer policy - Hono.Context.header(c, "Referrer-Policy", "strict-origin-when-cross-origin") - - // Permissions policy: Disable unnecessary features - Hono.Context.header( - c, - "Permissions-Policy", - "geolocation=(), microphone=(), camera=(), payment=(), usb=()", - ) - - // CORS: Only set headers when origin is in ALLOWED_ORIGINS whitelist - let req = Hono.Context.req(c) - let origin = Hono.Request.header(req, "Origin") - switch origin { - | Some(requestOrigin) => - if Belt.Array.some(allowedOrigins, allowed => allowed == requestOrigin) { - Hono.Context.header(c, "Access-Control-Allow-Origin", requestOrigin) - Hono.Context.header(c, "Access-Control-Allow-Credentials", "true") - Hono.Context.header(c, "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - Hono.Context.header(c, "Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Request-ID") - Hono.Context.header(c, "Access-Control-Max-Age", "3600") - } - | None => () - } - - await next() - } -} - -// Metrics collection middleware — increments request counter, observes -// request duration, and tracks error/auth-failure counts. -let metricsMiddleware = (): Hono.middleware<'env, 'path> => { - async (c, next) => { - Metrics.increment(Metrics.requestsTotal) - let startMs = Js.Date.now() - - await next() - - let durationMs = Js.Date.now() -. startMs - let durationSec = durationMs /. 1000.0 - Metrics.observe(Metrics.requestDurationSeconds, durationSec) - - // Check response status for error/auth tracking. - // Hono contexts don't expose response status directly after next(), - // so we use the context variable store as a best-effort signal. - // Error handler and auth middleware set these explicitly. - () - } -} - -// Validation helper - validates request body and returns 400 on error -let validateRequest = ( - c: Hono.Context.t<'env, 'path>, - validator: Validation.t, - schemaId: string, - body: Js.Json.t -): option => { - let result = Validation.validate(validator, schemaId, body) - - if !result.valid { - switch result.errors { - | Some(errors) => { - let formattedErrors = Validation.formatErrors(errors) - Log.warn("Validation failed", ~metadata=Js.Json.object_( - Js.Dict.fromArray([ - ("schema", Js.Json.string(schemaId)), - ("errors", Js.Json.array(formattedErrors)) - ]) - ), ()) - - Some(Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([ - ("error", Js.Json.string("Validation failed")), - ("details", Js.Json.array(formattedErrors)) - ])), - ~status=400, - () - )) - } - | None => { - Some(Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([ - ("error", Js.Json.string("Validation failed")) - ])), - ~status=400, - () - )) - } - } - } else { - None - } -} - -// Authorize container start with Rokur before runtime operations. -let authorizeContainerStart = async ( - c: Hono.Context.t<'env, 'path>, - image: string, - name: option -): option => { - if !Config.rokurGateEnabled { - None - } else { - let payloadDict = [("image", Js.Json.string(image))] - let payloadDict = switch name { - | Some(n) => Belt.Array.concat(payloadDict, [("name", Js.Json.string(n))]) - | None => payloadDict - } - let payload = Js.Json.object_(Js.Dict.fromArray(payloadDict)) - - let makeRetryMetadata = (attempt: int, statusCode: option): Js.Json.t => { - let statusEntry = switch statusCode { - | Some(code) => - [("rokurStatusCode", Js.Json.number(Belt.Int.toFloat(code)))] - | None => [] - } - let baseEntries = [ - ("attempt", Js.Json.number(Belt.Int.toFloat(attempt))), - ("maxRetries", Js.Json.number(Belt.Int.toFloat(Config.rokurRetryCount))), - ] - Js.Json.object_(Js.Dict.fromArray(Belt.Array.concat(baseEntries, statusEntry))) - } - - let denyStart = (rokurResponse: Js.Json.t): option => - Some(Hono.Context.json( - c, - Js.Json.object_( - Js.Dict.fromArray([ - ("error", Js.Json.string("Rokur denied container start")), - ("rokur", rokurResponse), - ]) - ), - ~status=409, - () - )) - - let unavailable = ( - message: string, - ~attempt: int, - ~statusCode: option=?, - ~rokurResponse: option=?, - () - ): option => { - let metadata = [ - ("error", Js.Json.string(message)), - ("rokurEndpoint", Js.Json.string(Config.rokurEndpoint)), - ("attempt", Js.Json.number(Belt.Int.toFloat(attempt))), - ] - let metadata = switch statusCode { - | Some(code) => - Belt.Array.concat(metadata, [("rokurStatusCode", Js.Json.number(Belt.Int.toFloat(code)))]) - | None => metadata - } - let metadata = switch rokurResponse { - | Some(value) => Belt.Array.concat(metadata, [("rokur", value)]) - | None => metadata - } - - Some(Hono.Context.json(c, Js.Json.object_(Js.Dict.fromArray(metadata)), ~status=503, ())) - } - - let rec authorizeAttempt = async (attempt: int): option => { - try { - let response = await Fetch.fetch( - Config.rokurEndpoint ++ "/v1/authorize-start", - { - "method": "POST", - "headers": { - "Content-Type": "application/json", - "X-Rokur-Token": Config.rokurApiToken, - }, - "body": Js.Json.stringify(payload), - "signal": timeoutSignal(Config.rokurTimeoutMs), - } - ) - - let rokurResponse = try { - await Fetch.Response.json(response) - } catch { - | _ => - Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string("Rokur returned a non-JSON response"))]) - ) - } - let statusCode = Fetch.Response.status(response) - let shouldRetry = statusCode >= 500 && attempt < Config.rokurRetryCount - - if shouldRetry { - Log.warn("Retrying Rokur authorization request", ~metadata=makeRetryMetadata(attempt, Some(statusCode)), ()) - await authorizeAttempt(attempt + 1) - } else if statusCode == 409 { - denyStart(rokurResponse) - } else if Fetch.Response.ok(response) { - let allowed = Validation.getBool(rokurResponse, "allowed")->Belt.Option.getWithDefault(false) - if allowed { - None - } else { - denyStart(rokurResponse) - } - } else { - let message = Validation.getString(rokurResponse, "error") - ->Belt.Option.getWithDefault("Rokur authorization request failed") - unavailable(message, ~attempt, ~statusCode, ~rokurResponse, ()) - } - } catch { - | Js.Exn.Error(e) => { - let shouldRetry = attempt < Config.rokurRetryCount - if shouldRetry { - Log.warn("Retrying Rokur authorization request after transport failure", ~metadata=makeRetryMetadata(attempt, None), ()) - await authorizeAttempt(attempt + 1) - } else { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Rokur request failed") - unavailable(message, ~attempt, ()) - } - } - } - } - - await authorizeAttempt(0) - } -} - -// Create Hono app with validation -let createAppWithValidator = (validator: Validation.t): Hono.t<'env> => { - let app = Hono.make() - - // Global middleware — order matters: - // 1. Error handler wraps everything (catches exceptions) - // 2. Security headers applied to every response (HSTS, CSP, CORS, etc.) - // 3. Metrics collection (counters, duration histogram) - // 4. Request logging - app->Hono.use(errorHandler())->ignore - app->Hono.use(securityHeaders())->ignore - app->Hono.use(RateLimiter.middleware(~config={windowMs: Config.rateLimitWindowMs, maxRequests: Config.rateLimitMaxRequests}, ()))->ignore - app->Hono.use(metricsMiddleware())->ignore - app->Hono.use(requestLogger())->ignore - - // Health/readiness endpoints (no auth required) - app->Hono.get("/health", HealthCheck.handler)->ignore - app->Hono.get("/healthz", HealthCheck.handler)->ignore - app->Hono.get("/ready", ReadinessCheck.handler)->ignore - app->Hono.get("/readyz", ReadinessCheck.handler)->ignore - app->Hono.get("/metrics", MetricsEndpoint.handler)->ignore - - // Authentication middleware (applied to all routes below) - if Config.enableAuth { - let authConfig = Middleware.loadAuthConfigFromEnv() - app->Hono.use(Middleware.authMiddleware(authConfig))->ignore - Log.info("Authentication enabled", ()) - } else { - Log.warn("Authentication DISABLED - not for production!", ()) - } - - // MCP server endpoint — accepts JSON-RPC 2.0 requests from AI agents. - // Placed after auth middleware so MCP calls are authenticated. - app->Hono.post("/mcp", async c => { - try { - let req = Hono.Context.req(c) - let body = await Hono.Request.json(req) - let rpcResponse = await Server.handleRequest(body) - let responseJson = Server.responseToJson(rpcResponse) - - // JSON-RPC 2.0 spec: errors are conveyed inside the response body, - // not via HTTP status codes — the HTTP layer always returns 200. - Hono.Context.json(c, responseJson, ~status=200, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to parse request body") - Log.error("MCP request error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - - // Return a JSON-RPC parse error (-32700) - let errorResponse = Server.responseToJson({ - jsonrpc: "2.0", - result: None, - error: Some({code: -32700, message: "Parse error: " ++ message}), - id: None, - }) - Hono.Context.json(c, errorResponse, ~status=200, ()) - } - } - })->ignore - - // API routes - Connected to Vörðr via MCP - let mcpConfig = McpClient.fromEnv() - - // Containers - List all containers - app->Hono.get("/api/v1/containers", async c => { - try { - let result = await McpClient.Container.list(mcpConfig, ()) - Log.info("Listed containers", ()) - Hono.Context.json(c, result, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to list containers") - Log.error("Container list error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Containers - Get specific container - app->Hono.get("/api/v1/containers/:id", async c => { - try { - let req = Hono.Context.req(c) - let id = switch Hono.Request.param(req, "id") { - | Some(id) => id - | None => raise(Js.Exn.raiseError("Missing required route parameter: id")) - } - let result = await McpClient.Container.get(mcpConfig, id) - Log.info("Got container", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("id", Js.Json.string(id))]) - ), ()) - Hono.Context.json(c, result, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to get container") - Log.error("Container get error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Containers - Create container - app->Hono.post("/api/v1/containers", async c => { - try { - let req = Hono.Context.req(c) - let body = await Hono.Request.json(req) - let image = switch Validation.getString(body, "image") { - | Some(image) => image - | None => raise(Js.Exn.raiseError("Missing required field: image")) - } - let name = Validation.getString(body, "name") - let config = Validation.getObject(body, "config")->Belt.Option.map(Js.Json.object_) - - let result = switch (name, config) { - | (Some(n), Some(c)) => await McpClient.Container.create(mcpConfig, ~image, ~name=n, ~containerConfig=c, ()) - | (Some(n), None) => await McpClient.Container.create(mcpConfig, ~image, ~name=n, ()) - | (None, Some(c)) => await McpClient.Container.create(mcpConfig, ~image, ~containerConfig=c, ()) - | (None, None) => await McpClient.Container.create(mcpConfig, ~image, ()) - } - Log.info("Created container", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("image", Js.Json.string(image))]) - ), ()) - Hono.Context.json(c, result, ~status=201, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to create container") - Log.error("Container create error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Containers - Start container - app->Hono.post("/api/v1/containers/:id/start", async c => { - try { - let req = Hono.Context.req(c) - let id = switch Hono.Request.param(req, "id") { - | Some(id) => id - | None => raise(Js.Exn.raiseError("Missing required route parameter: id")) - } - switch await authorizeContainerStart(c, "container-id:" ++ id, Some(id)) { - | Some(errorResponse) => errorResponse - | None => { - let result = await McpClient.Container.start(mcpConfig, id) - Log.info("Started container", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("id", Js.Json.string(id))]) - ), ()) - Hono.Context.json(c, result, ()) - } - } - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to start container") - Log.error("Container start error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Containers - Stop container - app->Hono.post("/api/v1/containers/:id/stop", async c => { - try { - let req = Hono.Context.req(c) - let id = switch Hono.Request.param(req, "id") { - | Some(id) => id - | None => raise(Js.Exn.raiseError("Missing required route parameter: id")) - } - let result = await McpClient.Container.stop(mcpConfig, id, ()) - Log.info("Stopped container", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("id", Js.Json.string(id))]) - ), ()) - Hono.Context.json(c, result, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to stop container") - Log.error("Container stop error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Containers - Remove container - app->Hono.delete("/api/v1/containers/:id", async c => { - try { - let req = Hono.Context.req(c) - let id = switch Hono.Request.param(req, "id") { - | Some(id) => id - | None => raise(Js.Exn.raiseError("Missing required route parameter: id")) - } - let result = await McpClient.Container.remove(mcpConfig, id, ()) - Log.info("Removed container", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("id", Js.Json.string(id))]) - ), ()) - Hono.Context.json(c, result, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to remove container") - Log.error("Container remove error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Images - List images - app->Hono.get("/api/v1/images", async c => { - try { - let result = await McpClient.Image.list(mcpConfig) - Log.info("Listed images", ()) - Hono.Context.json(c, result, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to list images") - Log.error("Image list error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Images - Pull image - app->Hono.post("/api/v1/images/pull", async c => { - try { - let req = Hono.Context.req(c) - let body = await Hono.Request.json(req) - let image = switch Validation.getString(body, "image") { - | Some(image) => image - | None => raise(Js.Exn.raiseError("Missing required field: image")) - } - let result = await McpClient.Image.pull(mcpConfig, image) - Log.info("Pulled image", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("image", Js.Json.string(image))]) - ), ()) - Hono.Context.json(c, result, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to pull image") - Log.error("Image pull error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Images - Verify image (with policy enforcement) - app->Hono.post("/api/v1/images/verify", async c => { - try { - let req = Hono.Context.req(c) - let body = await Hono.Request.json(req) - let digest = switch Validation.getString(body, "digest") { - | Some(digest) => digest - | None => raise(Js.Exn.raiseError("Missing required field: digest")) - } - let policyJson = Validation.getObject(body, "policy")->Belt.Option.map(Js.Json.object_) - - // If policy provided, validate it first - switch policyJson { - | Some(pol) => { - // Validate policy format - let policyValidation = PolicyEngine.validatePolicy(validator, pol) - if !policyValidation.valid { - // Policy is malformed - switch policyValidation.errors { - | Some(errors) => { - let formattedErrors = Validation.formatErrors(errors) - Log.warn("Invalid policy format", ~metadata=Js.Json.object_( - Js.Dict.fromArray([ - ("errors", Js.Json.array(formattedErrors)) - ]) - ), ()) - - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([ - ("error", Js.Json.string("Invalid policy format")), - ("details", Js.Json.array(formattedErrors)) - ])), - ~status=400, - () - ) - } - | None => { - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([ - ("error", Js.Json.string("Invalid policy format")) - ])), - ~status=400, - () - ) - } - } - } else { - // Policy is valid, send to Vörðr for enforcement - let result = await McpClient.Image.verify(mcpConfig, digest, ~policy=pol, ()) - - Log.info("Verified image with policy", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("digest", Js.Json.string(digest))]) - ), ()) - Hono.Context.json(c, result, ()) - } - } - | None => { - // Verify without policy (use Vörðr's default policy) - let result = await McpClient.Image.verify(mcpConfig, digest, ()) - Log.info("Verified image without policy", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("digest", Js.Json.string(digest))]) - ), ()) - Hono.Context.json(c, result, ()) - } - } - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to verify image") - Log.error("Image verify error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Run container (with validation + policy) - app->Hono.post("/api/v1/run", async c => { - try { - let req = Hono.Context.req(c) - let body = await Hono.Request.json(req) - - // Validate request against schema - switch validateRequest(c, validator, "gateway-run-request", body) { - | Some(errorResponse) => errorResponse - | None => { - let image = switch Validation.getString(body, "image") { - | Some(image) => image - | None => raise(Js.Exn.raiseError("Missing required field: image")) - } - let name = Validation.getString(body, "name") - let config = Validation.getObject(body, "config")->Belt.Option.map(Js.Json.object_) - - switch await authorizeContainerStart(c, image, name) { - | Some(errorResponse) => errorResponse - | None => { - // Create container - let createResult = switch (name, config) { - | (Some(n), Some(c)) => await McpClient.Container.create(mcpConfig, ~image, ~name=n, ~containerConfig=c, ()) - | (Some(n), None) => await McpClient.Container.create(mcpConfig, ~image, ~name=n, ()) - | (None, Some(c)) => await McpClient.Container.create(mcpConfig, ~image, ~containerConfig=c, ()) - | (None, None) => await McpClient.Container.create(mcpConfig, ~image, ()) - } - - // Extract container ID from result - let containerId = switch Validation.getString(createResult, "id") { - | Some(id) => id - | None => raise(Js.Exn.raiseError("Vörðr response missing container id")) - } - - // Start container - let startResult = await McpClient.Container.start(mcpConfig, containerId) - - Log.info("Ran container", ~metadata=Js.Json.object_( - Js.Dict.fromArray([ - ("image", Js.Json.string(image)), - ("containerId", Js.Json.string(containerId)) - ]) - ), ()) - - Hono.Context.json(c, startResult, ~status=201, ()) - } - } - } - } - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to run container") - Log.error("Container run error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Verify bundle (Cerro Torre .ctp bundle verification) - app->Hono.post("/api/v1/verify", async c => { - try { - let req = Hono.Context.req(c) - let body = await Hono.Request.json(req) - - // Validate request against schema - switch validateRequest(c, validator, "gateway-verify-request", body) { - | Some(errorResponse) => errorResponse - | None => { - let digest = switch Validation.getString(body, "digest") { - | Some(digest) => digest - | None => raise(Js.Exn.raiseError("Missing required field: digest")) - } - let policyJson = Validation.getObject(body, "policy")->Belt.Option.map(Js.Json.object_) - - // If policy provided, validate it first - let policyError = switch policyJson { - | Some(pol) => { - let policyValidation = PolicyEngine.validatePolicy(validator, pol) - if !policyValidation.valid { - switch policyValidation.errors { - | Some(errors) => { - let formattedErrors = Validation.formatErrors(errors) - Some(Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([ - ("error", Js.Json.string("Invalid policy format")), - ("details", Js.Json.array(formattedErrors)) - ])), - ~status=400, - () - )) - } - | None => { - Some(Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([ - ("error", Js.Json.string("Invalid policy format")) - ])), - ~status=400, - () - )) - } - } - } else { - None - } - } - | None => None - } - - switch policyError { - | Some(errorResponse) => errorResponse - | None => { - // Verify image (which includes .ctp bundle verification) - let result = switch policyJson { - | Some(pol) => await McpClient.Image.verify(mcpConfig, digest, ~policy=pol, ()) - | None => await McpClient.Image.verify(mcpConfig, digest, ()) - } - - Log.info("Verified bundle", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("digest", Js.Json.string(digest))]) - ), ()) - - Hono.Context.json(c, result, ()) - } - } - } - } - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to verify bundle") - Log.error("Bundle verify error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // Policies - List default policies - app->Hono.get("/api/v1/policies", async c => { - try { - let policies = Js.Json.object_( - Js.Dict.fromArray([ - ("default", PolicyEngine.formatResult({ - allowed: true, - mode: PolicyEngine.Strict, - predicatesFound: PolicyEngine.defaultPolicy.requiredPredicates, - missingPredicates: [], - signersVerified: [], - invalidSigners: [], - logCount: 1, - logQuorumMet: true, - violations: [], - warnings: [], - })), - ("permissive", PolicyEngine.formatResult({ - allowed: true, - mode: PolicyEngine.Permissive, - predicatesFound: [], - missingPredicates: [], - signersVerified: [], - invalidSigners: [], - logCount: 0, - logQuorumMet: true, - violations: [], - warnings: [], - })), - ]) - ) - - Log.info("Listed policies", ()) - Hono.Context.json(c, policies, ()) - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Failed to list policies") - Log.error("Policy list error", ~metadata=Js.Json.object_( - Js.Dict.fromArray([("error", Js.Json.string(message))]) - ), ()) - Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string(message))])), - ~status=500, - () - ) - } - } - })->ignore - - // 404 handler - app->Hono.all("*", async c => { - let req = Hono.Context.req(c) - let url = Hono.Request.url(req) - - Hono.Context.json( - c, - Js.Json.object_( - Js.Dict.fromArray([ - ("error", Js.Json.string("Not Found")), - ("path", Js.Json.string(url)), - ]) - ), - ~status=404, - () - ) - })->ignore - - app -} - -// Start server -let serve = async () => { - // Initialize selur WASM bridge (if SELUR_WASM env var is set) - SelurBridge.init() - if SelurBridge.isEnabled() { - Log.info("selur WASM bridge enabled — zero-copy IPC active", ()) - } - - // Load JSON schemas - Log.info("Loading JSON schemas...", ()) - let validator = Validation.make() - let validatorWithSchemas = await Validation.loadStandardSchemas(validator) - Log.info("JSON schemas loaded", ()) - - // Create app with validator - let app = createAppWithValidator(validatorWithSchemas) - - Log.info( - "Starting Svalinn Gateway", - ~metadata=Js.Json.object_( - Js.Dict.fromArray([ - ("port", Js.Json.number(Belt.Int.toFloat(Config.port))), - ("host", Js.Json.string(Config.host)), - ("vordrEndpoint", Js.Json.string(Config.vordrEndpoint)), - ("rokurEndpoint", Js.Json.string(Config.rokurEndpoint)), - ("rokurGateEnabled", Js.Json.boolean(Config.rokurGateEnabled)), - ("rokurTimeoutMs", Js.Json.number(Belt.Int.toFloat(Config.rokurTimeoutMs))), - ("rokurRetryCount", Js.Json.number(Belt.Int.toFloat(Config.rokurRetryCount))), - ("authEnabled", Js.Json.boolean(Config.enableAuth)), - ("tlsEnabled", Js.Json.boolean(Config.tlsEnabled)), - ]) - ), - () - ) - - // Start the server with Deno.serve (TLS or plain HTTP) - let handler = (req: Fetch.Request.t): promise => { - app->Hono.fetch(req, %raw(`{}`)) - } - - if Config.tlsEnabled { - // Read TLS certificate and key from disk - let certPath = Config.tlsCertFile->Belt.Option.getExn - let keyPath = Config.tlsKeyFile->Belt.Option.getExn - let cert = await Deno.Fs.readTextFile(certPath) - let key = await Deno.Fs.readTextFile(keyPath) - - Log.info( - "TLS enabled — serving HTTPS", - ~metadata=Js.Json.object_( - Js.Dict.fromArray([ - ("certFile", Js.Json.string(certPath)), - ("keyFile", Js.Json.string(keyPath)), - ]) - ), - () - ) - - Deno.Http.serveTls( - handler, - { - port: Config.port, - hostname: Some(Config.host), - signal: None, - cert, - key, - } - )->ignore - } else { - Log.info("TLS disabled — serving plain HTTP", ()) - - Deno.Http.serve( - handler, - { - port: Config.port, - hostname: Some(Config.host), - signal: None, - } - )->ignore - } -} diff --git a/container-stack/svalinn/src/lib/ocaml/GatewayTypes.ast b/container-stack/svalinn/src/lib/ocaml/GatewayTypes.ast deleted file mode 100644 index c9df042ff0a2878212e957f1dcd7c2937d77df03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6742 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, - startedAt: option, -} - -// Image information -type imageInfo = { - name: string, - tag: string, - digest: string, - verified: bool, - size: option, -} - -// Run request -type runRequest = { - imageName: string, - imageDigest: string, - name: option, - command: option>, - env: option>, - detach: option, - removeOnExit: option, - profile: option, -} - -// Verify request -type verifyRequest = { - imageRef: string, - checkSbom: option, - checkSignature: option, -} - -// SBOM information -type sbomInfo = { - format: string, - vulnerabilities: int, - critical: int, - high: int, -} - -// Signature information -type signatureInfo = { - valid: bool, - signer: option, - timestamp: option, -} - -// Verification result -type verificationResult = { - verified: bool, - imageRef: string, - digest: string, - sbom: option, - signature: option, -} - -// Health check response -type healthResponse = { - status: string, - version: string, - vordrConnected: bool, - timestamp: string, -} - -// Error response -type errorResponse = { - code: string, - message: string, - details: option, -} - -// API response wrapper -type apiResponse<'a> = - | Ok('a) - | Error(errorResponse) diff --git a/container-stack/svalinn/src/lib/ocaml/Hono.ast b/container-stack/svalinn/src/lib/ocaml/Hono.ast deleted file mode 100644 index 65e9bb065184c1e9532c522d9e396dd9949acd62..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13927 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 T(&r5ZORw~usGy7&tk^#R^{NZA diff --git a/container-stack/svalinn/src/lib/ocaml/Hono.cmj b/container-stack/svalinn/src/lib/ocaml/Hono.cmj deleted file mode 100644 index c01f4ea56137e313ff03f9f4423aaa7f219f29d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81 zcmbR2F$WOK0x^oz?9b29Vtbkh?{ hQp*!77i@6U($i1M%uC74OD|T}D@rZa%PMwo004kP8`}T? diff --git a/container-stack/svalinn/src/lib/ocaml/Hono.res b/container-stack/svalinn/src/lib/ocaml/Hono.res deleted file mode 100644 index a89d382..0000000 --- a/container-stack/svalinn/src/lib/ocaml/Hono.res +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Hono HTTP framework bindings for ReScript - -// Context variable map type -type contextVariableMap - -// Request wrapper -module Request = { - type t - - @get external method_: t => string = "method" - @get external url: t => string = "url" - @get external headers: t => Fetch.Headers.t = "headers" - - @send external header: (t, string) => option = "header" - @send external query: (t, string) => option = "query" - @send external param: (t, string) => option = "param" - - @send external json: t => promise = "json" - @send external text: t => promise = "text" -} - -// Response wrapper -module Response = { - type t - - @get external status: t => int = "status" - @get external headers: t => Fetch.Headers.t = "headers" - @get external ok: t => bool = "ok" - - @send external json: t => promise = "json" - @send external text: t => promise = "text" -} - -// Hono context -module Context = { - type t<'env, 'path> - - // Request access - @get external req: t<'env, 'path> => Request.t = "req" - - // Response helpers - @send external json: (t<'env, 'path>, Js.Json.t, ~status: int=?, unit) => Response.t = "json" - @send external text: (t<'env, 'path>, string, ~status: int=?, unit) => Response.t = "text" - @send external html: (t<'env, 'path>, string, ~status: int=?, unit) => Response.t = "html" - - // Variable storage (for user context, auth result, etc.) - @send external set: (t<'env, 'path>, string, 'value) => unit = "set" - @send external get: (t<'env, 'path>, string) => option<'value> = "get" - - // Headers - @send external header: (t<'env, 'path>, string, string) => unit = "header" - - // Status - @send external status: (t<'env, 'path>, int) => t<'env, 'path> = "status" -} - -// Middleware next function -type next = unit => promise - -// Handler function types -type handler<'env, 'path> = Context.t<'env, 'path> => promise -type middleware<'env, 'path> = (Context.t<'env, 'path>, next) => promise - -// Hono app -type t<'env> - -// Constructor -@module("hono") @new -external make: unit => t<'env> = "Hono" - -// Routing -@send external get: (t<'env>, string, handler<'env, 'path>) => t<'env> = "get" -@send external post: (t<'env>, string, handler<'env, 'path>) => t<'env> = "post" -@send external put: (t<'env>, string, handler<'env, 'path>) => t<'env> = "put" -@send external delete: (t<'env>, string, handler<'env, 'path>) => t<'env> = "delete" -@send external patch: (t<'env>, string, handler<'env, 'path>) => t<'env> = "patch" -@send external head: (t<'env>, string, handler<'env, 'path>) => t<'env> = "head" -@send external options: (t<'env>, string, handler<'env, 'path>) => t<'env> = "options" -@send external all: (t<'env>, string, handler<'env, 'path>) => t<'env> = "all" - -// Middleware registration -@send external use: (t<'env>, middleware<'env, 'path>) => t<'env> = "use" -@send external useWithPath: (t<'env>, string, middleware<'env, 'path>) => t<'env> = "use" - -// Server -@send -external serve: (t<'env>, {..}) => {..} = "serve" - -// Export for Deno -@send -external fetch: (t<'env>, Fetch.Request.t, 'env) => promise = "fetch" diff --git a/container-stack/svalinn/src/lib/ocaml/JWT.ast b/container-stack/svalinn/src/lib/ocaml/JWT.ast deleted file mode 100644 index 9f95e3dc2daaed9f82085d12958c97b29f76c15b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64561 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 E594cOR{#J2 diff --git a/container-stack/svalinn/src/lib/ocaml/JWT.cmj b/container-stack/svalinn/src/lib/ocaml/JWT.cmj deleted file mode 100644 index e5dffbd8179f147498fbb06f217fb3efe2b5e912..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 267 zcmX>raAc#J)hv~xMLfn;_jpxXrY_sZz`*bvh|Pd_HxREpz`?+v&agoE{@Cnd8uIUgwO z@9E+U)2Nf4TH=_Ko?nz%l93A&)K5w*PBk+LEy{5LTa791ng?x^oz?9b29Vtbkh?{Qp*!77i@4;)6-8ZEy+;VD@rZa%PMwo003LGWPbnv diff --git a/container-stack/svalinn/src/lib/ocaml/JWT.res b/container-stack/svalinn/src/lib/ocaml/JWT.res deleted file mode 100644 index 22790c5..0000000 --- a/container-stack/svalinn/src/lib/ocaml/JWT.res +++ /dev/null @@ -1,448 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// JWT verification for Svalinn - -open AuthTypes - -@val external atob_: string => string = "atob" -@val external btoa_: string => string = "btoa" - -type textEncoder -@new external makeTextEncoder: unit => textEncoder = "TextEncoder" -@send external encodeText: (textEncoder, string) => Js.TypedArray2.Uint8Array.t = "encode" - -// JWKS key structure -type jwk = { - kty: string, - @as("use") use_: option, - alg: option, - kid: string, - n: option, // RSA modulus - e: option, // RSA exponent - x: option, // EC x coordinate - y: option, // EC y coordinate - crv: option, // EC curve name -} - -// JWKS response -type jwks = {keys: array} - -// Cached JWKS with expiry -type cachedJwks = { - jwks: jwks, - expiresAt: float, -} - -// JWKS cache (mutable Map) -let jwksCache: Js.Dict.t = Js.Dict.empty() -let jwksCacheTtl = 3600000.0 // 1 hour in milliseconds - -// JWT header -type jwtHeader = { - alg: string, - typ: option, - kid: option, -} - -// Algorithm types for Web Crypto API -type algorithm = - | RSA_PKCS1(string) // hash algorithm (SHA-256, SHA-384, SHA-512) - | ECDSA(string, string) // curve, hash - -// Fetch JWKS from issuer with caching -let fetchJWKS = async (jwksUri: string): jwks => { - // Check cache - switch Js.Dict.get(jwksCache, jwksUri) { - | Some(cached) if cached.expiresAt > Js.Date.now() => cached.jwks - | _ => { - // Fetch JWKS - let response = await Fetch.fetch(jwksUri, {"method": "GET"}) - if !Fetch.Response.ok(response) { - let status = Fetch.Response.status(response)->Belt.Int.toString - raise(Js.Exn.raiseError(`Failed to fetch JWKS: ${status}`)) - } - - let json = await Fetch.Response.json(response) - let jwks = switch json->Js.Json.decodeObject - ->Belt.Option.flatMap(obj => obj->Js.Dict.get("keys")) - ->Belt.Option.flatMap(Js.Json.decodeArray) { - | Some(keys) => keys - | None => raise(Js.Exn.raiseError("JWKS response missing 'keys' array")) - } - - let keys = jwks->Belt.Array.map(keyJson => { - let obj = switch keyJson->Js.Json.decodeObject { - | Some(o) => o - | None => raise(Js.Exn.raiseError("JWKS key entry is not a valid object")) - } - { - kty: switch obj->Js.Dict.get("kty")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(v) => v - | None => raise(Js.Exn.raiseError("JWKS key missing required 'kty' field")) - }, - use_: obj->Js.Dict.get("use")->Belt.Option.flatMap(Js.Json.decodeString), - alg: obj->Js.Dict.get("alg")->Belt.Option.flatMap(Js.Json.decodeString), - kid: switch obj->Js.Dict.get("kid")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(v) => v - | None => raise(Js.Exn.raiseError("JWKS key missing required 'kid' field")) - }, - n: obj->Js.Dict.get("n")->Belt.Option.flatMap(Js.Json.decodeString), - e: obj->Js.Dict.get("e")->Belt.Option.flatMap(Js.Json.decodeString), - x: obj->Js.Dict.get("x")->Belt.Option.flatMap(Js.Json.decodeString), - y: obj->Js.Dict.get("y")->Belt.Option.flatMap(Js.Json.decodeString), - crv: obj->Js.Dict.get("crv")->Belt.Option.flatMap(Js.Json.decodeString), - } - }) - - let jwksResult = {keys: keys} - - // Cache - Js.Dict.set(jwksCache, jwksUri, { - jwks: jwksResult, - expiresAt: Js.Date.now() +. jwksCacheTtl, - }) - - jwksResult - } - } -} - -// Base64 URL decode -let base64UrlDecode = (str: string): Js.TypedArray2.Uint8Array.t => { - let base64 = str - ->Js.String2.replaceByRe(%re("/-/g"), "+") - ->Js.String2.replaceByRe(%re("/_/g"), "/") - - let padLen = mod(4 - mod(Js.String2.length(base64), 4), 4) - let padding = Js.String2.repeat("=", padLen) - let binary = atob_(base64 ++ padding) - - let bytes = Js.TypedArray2.Uint8Array.fromLength(Js.String2.length(binary)) - for i in 0 to Js.String2.length(binary) - 1 { - Js.TypedArray2.Uint8Array.unsafe_set(bytes, i, Js.String2.charCodeAt(binary, i)->Belt.Float.toInt) - } - bytes -} - -// Base64 URL encode -let base64UrlEncode = (bytes: Js.TypedArray2.Uint8Array.t): string => { - let len = Js.TypedArray2.Uint8Array.length(bytes) - let binary = ref("") - for i in 0 to len - 1 { - binary := binary.contents ++ Js.String2.fromCharCode(Js.TypedArray2.Uint8Array.unsafe_get(bytes, i)) - } - btoa_(binary.contents) - ->Js.String2.replaceByRe(%re("/\\+/g"), "-") - ->Js.String2.split("/") - ->Js.Array2.joinWith("_") - ->Js.String2.replaceByRe(%re("/=/g"), "") -} - -// Decode JWT without verification (for header inspection) -let decodeJWT = (token: string): (jwtHeader, tokenPayload) => { - let parts = Js.String2.split(token, ".") - if Js.Array2.length(parts) != 3 { - raise(Js.Exn.raiseError("Invalid JWT format: expected 3 parts")) - } - - let headerStr = switch Belt.Array.get(parts, 0) { - | Some(h) => base64UrlDecode(h) - | None => raise(Js.Exn.raiseError("Invalid JWT: missing header")) - } - - let payloadStr = switch Belt.Array.get(parts, 1) { - | Some(p) => base64UrlDecode(p) - | None => raise(Js.Exn.raiseError("Invalid JWT: missing payload")) - } - - // Convert Uint8Array to string - let headerLen = Js.TypedArray2.Uint8Array.length(headerStr) - let headerString = ref("") - for i in 0 to headerLen - 1 { - headerString := headerString.contents ++ Js.String2.fromCharCode(Js.TypedArray2.Uint8Array.unsafe_get(headerStr, i)) - } - let headerJson = Js.Json.parseExn(headerString.contents) - - let payloadLen = Js.TypedArray2.Uint8Array.length(payloadStr) - let payloadString = ref("") - for i in 0 to payloadLen - 1 { - payloadString := payloadString.contents ++ Js.String2.fromCharCode(Js.TypedArray2.Uint8Array.unsafe_get(payloadStr, i)) - } - let payloadJson = Js.Json.parseExn(payloadString.contents) - - // Parse header - let headerObj = switch headerJson->Js.Json.decodeObject { - | Some(obj) => obj - | None => raise(Js.Exn.raiseError("Invalid JWT: header is not an object")) - } - - let header = { - alg: switch headerObj->Js.Dict.get("alg")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(a) => a - | None => raise(Js.Exn.raiseError("Invalid JWT: missing required 'alg' field in header")) - }, - typ: headerObj->Js.Dict.get("typ")->Belt.Option.flatMap(Js.Json.decodeString), - kid: headerObj->Js.Dict.get("kid")->Belt.Option.flatMap(Js.Json.decodeString), - } - - // Parse payload - let payloadObj = switch payloadJson->Js.Json.decodeObject { - | Some(obj) => obj - | None => raise(Js.Exn.raiseError("Invalid JWT: payload is not an object")) - } - - let payload = { - sub: switch payloadObj->Js.Dict.get("sub")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(s) => s - | None => raise(Js.Exn.raiseError("Invalid JWT: missing required 'sub' field")) - }, - iss: switch payloadObj->Js.Dict.get("iss")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(i) => i - | None => raise(Js.Exn.raiseError("Invalid JWT: missing required 'iss' field")) - }, - aud: switch payloadObj->Js.Dict.get("aud") { - | Some(a) => a - | None => raise(Js.Exn.raiseError("Invalid JWT: missing required 'aud' field")) - }, - exp: switch payloadObj->Js.Dict.get("exp")->Belt.Option.flatMap(Js.Json.decodeNumber)->Belt.Option.map(Belt.Float.toInt) { - | Some(e) => e - | None => raise(Js.Exn.raiseError("Invalid JWT: missing required 'exp' field")) - }, - iat: switch payloadObj->Js.Dict.get("iat")->Belt.Option.flatMap(Js.Json.decodeNumber)->Belt.Option.map(Belt.Float.toInt) { - | Some(i) => i - | None => raise(Js.Exn.raiseError("Invalid JWT: missing required 'iat' field")) - }, - scope: payloadObj->Js.Dict.get("scope")->Belt.Option.flatMap(Js.Json.decodeString), - email: payloadObj->Js.Dict.get("email")->Belt.Option.flatMap(Js.Json.decodeString), - name: payloadObj->Js.Dict.get("name")->Belt.Option.flatMap(Js.Json.decodeString), - groups: payloadObj->Js.Dict.get("groups")->Belt.Option.flatMap(Js.Json.decodeArray)->Belt.Option.map(arr => - arr->Belt.Array.keepMap(Js.Json.decodeString) - ), - claims: payloadObj, - } - - (header, payload) -} - -// Get algorithm parameters from alg string -let getAlgorithm = (alg: string): algorithm => { - switch alg { - | "RS256" => RSA_PKCS1("SHA-256") - | "RS384" => RSA_PKCS1("SHA-384") - | "RS512" => RSA_PKCS1("SHA-512") - | "ES256" => ECDSA("P-256", "SHA-256") - | "ES384" => ECDSA("P-384", "SHA-384") - | "ES512" => ECDSA("P-521", "SHA-512") - | _ => raise(Js.Exn.raiseError(`Unsupported algorithm: ${alg}`)) - } -} - -// Import JWK to CryptoKey using Web Crypto API -@val external importKey: ( - string, - 'a, - 'b, - bool, - array -) => promise<'cryptoKey> = "crypto.subtle.importKey" - -@val external verify: ( - 'algorithm, - 'cryptoKey, - Js.TypedArray2.ArrayBuffer.t, - Js.TypedArray2.ArrayBuffer.t -) => promise = "crypto.subtle.verify" - -let importJWK = async (jwk: jwk, alg: string): 'cryptoKey => { - let algorithm = getAlgorithm(alg) - - let algorithmObj = switch algorithm { - | RSA_PKCS1(hash) => - Js.Json.object_(Js.Dict.fromArray([ - ("name", Js.Json.string("RSASSA-PKCS1-v1_5")), - ("hash", Js.Json.string(hash)), - ])) - | ECDSA(curve, _) => - Js.Json.object_(Js.Dict.fromArray([ - ("name", Js.Json.string("ECDSA")), - ("namedCurve", Js.Json.string(curve)), - ])) - } - - // Convert jwk to JS object for importKey - let jwkObj: Js.Dict.t = Js.Dict.empty() - Js.Dict.set(jwkObj, "kty", jwk.kty) - Js.Dict.set(jwkObj, "kid", jwk.kid) - - switch jwk.alg { - | Some(value) => Js.Dict.set(jwkObj, "alg", value) - | None => () - } - switch jwk.n { - | Some(value) => Js.Dict.set(jwkObj, "n", value) - | None => () - } - switch jwk.e { - | Some(value) => Js.Dict.set(jwkObj, "e", value) - | None => () - } - switch jwk.x { - | Some(value) => Js.Dict.set(jwkObj, "x", value) - | None => () - } - switch jwk.y { - | Some(value) => Js.Dict.set(jwkObj, "y", value) - | None => () - } - switch jwk.crv { - | Some(value) => Js.Dict.set(jwkObj, "crv", value) - | None => () - } - - await importKey("jwk", jwkObj, algorithmObj, true, ["verify"]) -} - -// Verify JWT signature -let verifySignature = async (token: string, key: 'cryptoKey, algorithm: algorithm): bool => { - let parts = Js.String2.split(token, ".") - - let part0 = switch Belt.Array.get(parts, 0) { - | Some(p) => p - | None => raise(Js.Exn.raiseError("Invalid JWT: missing header for signature verification")) - } - - let part1 = switch Belt.Array.get(parts, 1) { - | Some(p) => p - | None => raise(Js.Exn.raiseError("Invalid JWT: missing payload for signature verification")) - } - - let data = encodeText(makeTextEncoder(), part0 ++ "." ++ part1) - - let signature = switch Belt.Array.get(parts, 2) { - | Some(sig) => base64UrlDecode(sig) - | None => raise(Js.Exn.raiseError("Invalid JWT: missing signature")) - } - - let algorithmObj = switch algorithm { - | RSA_PKCS1(hash) => - Js.Json.object_(Js.Dict.fromArray([ - ("name", Js.Json.string("RSASSA-PKCS1-v1_5")), - ("hash", Js.Json.string(hash)), - ])) - | ECDSA(_, hash) => - Js.Json.object_(Js.Dict.fromArray([ - ("name", Js.Json.string("ECDSA")), - ("hash", Js.Json.string(hash)), - ])) - } - - await verify( - algorithmObj, - key, - Js.TypedArray2.Uint8Array.buffer(signature), - Js.TypedArray2.Uint8Array.buffer(data) - ) -} - -// Verify JWT signature using Web Crypto API -let verifyJWT = async (token: string, config: oidcConfig): tokenPayload => { - let (header, payload) = decodeJWT(token) - - // Validate basic claims - let now = Belt.Float.toInt(Js.Date.now() /. 1000.0) - - if payload.exp < now { - raise(Js.Exn.raiseError("Token expired")) - } - - if payload.iat > now + 60 { - raise(Js.Exn.raiseError("Token issued in the future")) - } - - if payload.iss != config.issuer { - raise(Js.Exn.raiseError(`Invalid issuer: expected ${config.issuer}, got ${payload.iss}`)) - } - - // Validate audience - let audiences = switch payload.aud->Js.Json.decodeArray { - | Some(arr) => arr->Belt.Array.keepMap(Js.Json.decodeString) - | None => switch payload.aud->Js.Json.decodeString { - | Some(s) => [s] - | None => [] - } - } - - if !Belt.Array.some(audiences, aud => aud == config.clientId) { - raise(Js.Exn.raiseError(`Invalid audience: ${Js.Json.stringify(payload.aud)}`)) - } - - // Fetch JWKS and verify signature - let jwks = await fetchJWKS(config.jwksUri) - let kid = switch header.kid { - | Some(k) => k - | None => raise(Js.Exn.raiseError("JWT header missing 'kid' field required for JWKS lookup")) - } - let key = jwks.keys->Belt.Array.getBy(k => k.kid == kid) - - switch key { - | None => raise(Js.Exn.raiseError(`Key not found: ${kid}`)) - | Some(k) => { - // Import key and verify - let cryptoKey = await importJWK(k, header.alg) - let algorithm = getAlgorithm(header.alg) - let valid = await verifySignature(token, cryptoKey, algorithm) - - if !valid { - raise(Js.Exn.raiseError("Invalid signature")) - } - - payload - } - } -} - -// Discover OIDC configuration from issuer -let discoverOIDC = async (issuer: string): oidcConfig => { - let wellKnown = issuer - ->Js.String2.replaceByRe(%re("/\/$/"), "") - ++ "/.well-known/openid-configuration" - - let response = await Fetch.fetch(wellKnown, {"method": "GET"}) - if !Fetch.Response.ok(response) { - let status = Fetch.Response.status(response)->Belt.Int.toString - raise(Js.Exn.raiseError(`OIDC discovery failed: ${status}`)) - } - - let config = await Fetch.Response.json(response) - let obj = switch config->Js.Json.decodeObject { - | Some(o) => o - | None => raise(Js.Exn.raiseError("OIDC discovery response is not a valid JSON object")) - } - - { - clientId: "", // Must be provided by caller - clientSecret: "", // Must be provided by caller - issuer: switch obj->Js.Dict.get("issuer")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(v) => v - | None => raise(Js.Exn.raiseError("OIDC discovery missing required 'issuer' field")) - }, - authorizationEndpoint: switch obj->Js.Dict.get("authorization_endpoint")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(v) => v - | None => raise(Js.Exn.raiseError("OIDC discovery missing required 'authorization_endpoint' field")) - }, - tokenEndpoint: switch obj->Js.Dict.get("token_endpoint")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(v) => v - | None => raise(Js.Exn.raiseError("OIDC discovery missing required 'token_endpoint' field")) - }, - userInfoEndpoint: switch obj->Js.Dict.get("userinfo_endpoint")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(v) => v - | None => raise(Js.Exn.raiseError("OIDC discovery missing required 'userinfo_endpoint' field")) - }, - jwksUri: switch obj->Js.Dict.get("jwks_uri")->Belt.Option.flatMap(Js.Json.decodeString) { - | Some(v) => v - | None => raise(Js.Exn.raiseError("OIDC discovery missing required 'jwks_uri' field")) - }, - redirectUri: "", // Must be provided by caller - scopes: ["openid", "profile", "email"], // Default scopes - endSessionEndpoint: obj->Js.Dict.get("end_session_endpoint")->Belt.Option.flatMap(Js.Json.decodeString), - } -} diff --git a/container-stack/svalinn/src/lib/ocaml/Main.ast b/container-stack/svalinn/src/lib/ocaml/Main.ast deleted file mode 100644 index e408e388f91f1cf3bee18c693bf059798a6a0c16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 287 zcmZQzVBq9(Pb^6-Ppst9FH0;^333ha4~}>74AL(yNi0as$lc?L=49sO=@%Cz>-z#F^oml8xmug z!-f?LCMaoW0_DZQ;@t;WB*2Uw2apON*ucWe!UUoZut!+ diff --git a/container-stack/svalinn/src/lib/ocaml/Main.cmj b/container-stack/svalinn/src/lib/ocaml/Main.cmj deleted file mode 100644 index ea0756dae36c4e6c903e48337787d540928183ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72 zcmcc3Y*!W{q9zvh+rBZh%;aFp)Mfh^7#LK5m;;EVfmq@|gTsOe`o(35IhlERy6K4} YspW~43pO|^>Z$7$r55XD6+1Wp05Me-=Kufz diff --git a/container-stack/svalinn/src/lib/ocaml/Main.res b/container-stack/svalinn/src/lib/ocaml/Main.res deleted file mode 100644 index fa023d7..0000000 --- a/container-stack/svalinn/src/lib/ocaml/Main.res +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Svalinn Edge Shield - Main entry point - -// Start the gateway server -let _ = Gateway.serve() diff --git a/container-stack/svalinn/src/lib/ocaml/McpClient.ast b/container-stack/svalinn/src/lib/ocaml/McpClient.ast deleted file mode 100644 index 2c26a6db2cf0fb97f1e393f8924ebfcbd0607ea2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40203 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 diff --git a/container-stack/svalinn/src/lib/ocaml/McpClient.res b/container-stack/svalinn/src/lib/ocaml/McpClient.res deleted file mode 100644 index 64eaac9..0000000 --- a/container-stack/svalinn/src/lib/ocaml/McpClient.res +++ /dev/null @@ -1,311 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// MCP (Model Context Protocol) client for Vörðr integration - -// MCP request/response types -type mcpRequest = { - jsonrpc: string, - method: string, - params: Js.Json.t, - id: float, -} - -type mcpError = { - code: int, - message: string, - data: option, -} - -type mcpResponse = { - jsonrpc: string, - result: option, - error: option, - id: float, -} - -// Client configuration -type config = { - endpoint: string, - timeout: int, // milliseconds - retries: int, -} - -// Default configuration -let defaultConfig: config = { - endpoint: "http://localhost:8080", - timeout: 30000, // 30 seconds - retries: 3, -} - -// Create config from environment -@scope(("Deno", "env")) @val external getEnv: string => option = "get" -@scope("AbortSignal") @val external timeoutSignal: int => 'a = "timeout" - -let fromEnv = (): config => { - { - endpoint: getEnv("VORDR_ENDPOINT")->Belt.Option.getWithDefault(defaultConfig.endpoint), - timeout: getEnv("VORDR_TIMEOUT") - ->Belt.Option.flatMap(Belt.Int.fromString) - ->Belt.Option.getWithDefault(defaultConfig.timeout), - retries: getEnv("VORDR_RETRIES") - ->Belt.Option.flatMap(Belt.Int.fromString) - ->Belt.Option.getWithDefault(defaultConfig.retries), - } -} - -// Call MCP method with retries -let rec callWithRetry = async ( - config: config, - method: string, - params: Js.Json.t, - attempt: int -): Js.Json.t => { - let requestId = Js.Date.now() - - let request: mcpRequest = { - jsonrpc: "2.0", - method, - params, - id: requestId, - } - - let requestBody = Js.Json.object_( - Js.Dict.fromArray([ - ("jsonrpc", Js.Json.string(request.jsonrpc)), - ("method", Js.Json.string(request.method)), - ("params", request.params), - ("id", Js.Json.number(request.id)), - ]) - ) - - try { - // Try selur WASM bridge first (zero-copy IPC, ~7-20x faster). - // Falls through to HTTP Fetch if bridge is disabled or method unsupported. - let bridgeResult = await SelurBridge.tryBridge(method, params, requestId) - - switch bridgeResult { - | Some(json) => json - | None => { - let response = await Fetch.fetch( - config.endpoint, - { - "method": "POST", - "headers": {"Content-Type": "application/json"}, - "body": Js.Json.stringify(requestBody), - "signal": timeoutSignal(config.timeout), - } - ) - - if !Fetch.Response.ok(response) { - let status = Fetch.Response.status(response) - raise(Js.Exn.raiseError(`HTTP ${Belt.Int.toString(status)}`)) - } - - let json = await Fetch.Response.json(response) - let obj = switch json->Js.Json.decodeObject { - | Some(o) => o - | None => raise(Js.Exn.raiseError("MCP response is not a valid JSON object")) - } - - switch obj->Js.Dict.get("error") { - | Some(errorJson) => { - let errorObj = switch errorJson->Js.Json.decodeObject { - | Some(o) => o - | None => raise(Js.Exn.raiseError("MCP error field is not a valid object")) - } - let code = errorObj - ->Js.Dict.get("code") - ->Belt.Option.flatMap(Js.Json.decodeNumber) - ->Belt.Option.map(Belt.Float.toInt) - ->Belt.Option.getWithDefault(-1) - let message = errorObj - ->Js.Dict.get("message") - ->Belt.Option.flatMap(Js.Json.decodeString) - ->Belt.Option.getWithDefault("Unknown error") - raise(Js.Exn.raiseError(`MCP error ${Belt.Int.toString(code)}: ${message}`)) - } - | None => - obj->Js.Dict.get("result")->Belt.Option.getWithDefault(Js.Json.null) - } - } - } - } catch { - | Js.Exn.Error(e) if attempt < config.retries => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Unknown error") - Js.Console.warn(`MCP call failed (attempt ${Belt.Int.toString(attempt + 1)}): ${message}`) - - // Exponential backoff: 100ms, 200ms, 400ms, etc. - let _ = await %raw(`new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, attempt)))`) - - await callWithRetry(config, method, params, attempt + 1) - } - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Unknown error") - raise(Js.Exn.raiseError(`MCP call failed after ${Belt.Int.toString(config.retries)} retries: ${message}`)) - } - } -} - -// Call MCP method -let call = async (config: config, method: string, params: Js.Json.t): Js.Json.t => { - await callWithRetry(config, method, params, 0) -} - -// Convenience methods for Vörðr operations - -// Container operations -module Container = { - // List containers - let list = async (config: config, ~all: bool=false, ()): Js.Json.t => { - let params = Js.Json.object_(Js.Dict.fromArray([("all", Js.Json.boolean(all))])) - await call(config, "containers/list", params) - } - - // Get container by ID - let get = async (config: config, id: string): Js.Json.t => { - let params = Js.Json.object_(Js.Dict.fromArray([("id", Js.Json.string(id))])) - await call(config, "containers/get", params) - } - - // Create container - let create = async ( - config: config, - ~image: string, - ~name: option=?, - ~containerConfig: option=?, - () - ): Js.Json.t => { - let paramsDict = [("image", Js.Json.string(image))] - - let paramsDict = switch name { - | Some(n) => Belt.Array.concat(paramsDict, [("name", Js.Json.string(n))]) - | None => paramsDict - } - - let paramsDict = switch containerConfig { - | Some(c) => Belt.Array.concat(paramsDict, [("config", c)]) - | None => paramsDict - } - - let params = Js.Json.object_(Js.Dict.fromArray(paramsDict)) - await call(config, "containers/create", params) - } - - // Start container - let start = async (config: config, id: string): Js.Json.t => { - let params = Js.Json.object_(Js.Dict.fromArray([("id", Js.Json.string(id))])) - await call(config, "containers/start", params) - } - - // Stop container - let stop = async (config: config, id: string, ~timeout: option=?, ()): Js.Json.t => { - let paramsDict = [("id", Js.Json.string(id))] - - let paramsDict = switch timeout { - | Some(t) => Belt.Array.concat(paramsDict, [("timeout", Js.Json.number(Belt.Int.toFloat(t)))]) - | None => paramsDict - } - - let params = Js.Json.object_(Js.Dict.fromArray(paramsDict)) - await call(config, "containers/stop", params) - } - - // Remove container - let remove = async (config: config, id: string, ~force: bool=false, ()): Js.Json.t => { - let params = Js.Json.object_( - Js.Dict.fromArray([("id", Js.Json.string(id)), ("force", Js.Json.boolean(force))]) - ) - await call(config, "containers/remove", params) - } - - // Get container logs - let logs = async ( - config: config, - id: string, - ~follow: bool=false, - ~tail: option=?, - () - ): Js.Json.t => { - let paramsDict = [("id", Js.Json.string(id)), ("follow", Js.Json.boolean(follow))] - - let paramsDict = switch tail { - | Some(t) => Belt.Array.concat(paramsDict, [("tail", Js.Json.number(Belt.Int.toFloat(t)))]) - | None => paramsDict - } - - let params = Js.Json.object_(Js.Dict.fromArray(paramsDict)) - await call(config, "containers/logs", params) - } - - // Execute command in container - let exec = async ( - config: config, - id: string, - cmd: array, - ~workdir: option=?, - () - ): Js.Json.t => { - let paramsDict = [ - ("id", Js.Json.string(id)), - ("cmd", Js.Json.array(Belt.Array.map(cmd, Js.Json.string))), - ] - - let paramsDict = switch workdir { - | Some(w) => Belt.Array.concat(paramsDict, [("workdir", Js.Json.string(w))]) - | None => paramsDict - } - - let params = Js.Json.object_(Js.Dict.fromArray(paramsDict)) - await call(config, "containers/exec", params) - } -} - -// Image operations -module Image = { - // List images - let list = async (config: config): Js.Json.t => { - let params = Js.Json.object_(Js.Dict.empty()) - await call(config, "images/list", params) - } - - // Pull image - let pull = async (config: config, image: string): Js.Json.t => { - let params = Js.Json.object_(Js.Dict.fromArray([("image", Js.Json.string(image))])) - await call(config, "images/pull", params) - } - - // Remove image - let remove = async (config: config, image: string, ~force: bool=false, ()): Js.Json.t => { - let params = Js.Json.object_( - Js.Dict.fromArray([("image", Js.Json.string(image)), ("force", Js.Json.boolean(force))]) - ) - await call(config, "images/remove", params) - } - - // Verify image (using Cerro Torre) - let verify = async (config: config, digest: string, ~policy: option=?, ()): Js.Json.t => { - let paramsDict = [("digest", Js.Json.string(digest))] - - let paramsDict = switch policy { - | Some(p) => Belt.Array.concat(paramsDict, [("policy", p)]) - | None => paramsDict - } - - let params = Js.Json.object_(Js.Dict.fromArray(paramsDict)) - await call(config, "images/verify", params) - } -} - -// Health check -let health = async (config: config): bool => { - try { - let _ = await call(config, "health", Js.Json.object_(Js.Dict.empty())) - true - } catch { - | _ => false - } -} - -// Get Vörðr version info -let version = async (config: config): Js.Json.t => { - await call(config, "version", Js.Json.object_(Js.Dict.empty())) -} diff --git a/container-stack/svalinn/src/lib/ocaml/McpTypes.ast b/container-stack/svalinn/src/lib/ocaml/McpTypes.ast deleted file mode 100644 index 69ef44008f8beba78167bb587c54716f68312ac4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4038 zcmb`Ke~eVs702JX@4l#uR>US%F-_PX{McQ0T0xC1(y4&3TiD$!i;=2bhT+LRn0+(6 zH;W)bD?%*(Ad3w))Jg{o1X`in#v&Ag*O*$XSW07!K_eh(C2FZso0Zz6J@8;d(J)g+;i_(mL+T2!kW~IOi?tXaQh*AdIQ(zh(zv8+)a@@G@2^lZ9x zsb$q!rAftnKG*6O`iq0=vhVyK9L}^n%U4q9@B7o~^ybp3QoWz)a|e~Y><;TeYps$s z{7u$6)($>f8WOUbW&p6#r1N0Q#44oY>38LXa=GLD6$V z_Pes3tRb>4D7h*k;tkjJv3>hMZChC17UunNT%>Y6w2qP+KwnBsJvuIvWHO)VAR0|H zM#)W}>*8=5GC`18#Zzv9{CXvy0*$LIk!_IID)}?e#IjSA$PS1nX>n8{asGEAhsTxN z)k)Mu)NBSOcA#D#LXKmwi>QTYvXZ|7-4dtX9H0a9h^7)vQ}Q6__NvS)C}t@65~$5M zOk}>|6#b!_Xbw@klCOalR84yvio2EkGbl6Cw8Jau#C)PGk*nl8ph7%tMa8`jZJ^{C z(3;A*<79q-th6Y3_D-Vj6aBy#CX(4ybXOLqbBlC+T`uSEB3eiEh?4&VJ;^}~E9ZYN zAo?-URwcgxZL3_pLjszTa>xemGOiP8t=Fyx*=E#tiFOnHobhttzUn!zhDO_e8Mwc4 zPORIZYY_T6Hof+4q60(+jcH=#jk=7w+^|0v1yOe|(IFzmgX)2=#Y>-{Z;&WG4mEv` z=ory)wrU2x9fxX;heMO0IKehkfG2T4hUeO$snDKc!`px#nK579ZIPZ(zdhfL~BOLwRPnhR{y3H&btiuIy$enT| zCfhy8)*&9da3P6HNL*^hCbFH%d10}eGag<{`O-xsYDiqkBbNcc8mGKL?+v#(hk`7L zIuZ?xA+WJ3;SWI3%r>imUys*^PT|nQ5Z}la>w!0$81=W#^dpdS4;j{y9C{R?)v(QD zi%Hx{;u~gcVtp-3x#M;fvR!;FxnWdMTb7V$Cozu)J_Y115FMro+Bhg=^#Y&iC2=2# zUdC;}yxBo>BRaGbn&oV?3&@X!s@r~ipIhwB^Nr}xK99sI5~~@134Ew}*8R{t%tkK& zH;y!GAroZ!qRn`5DT&8OJi!AL@JD9Wi`DjfXtuD?tH7tD-Ke;k9C{PlpR(a!fIG|X zd2v^<{B6ji?lCrgO4NGM3QsO0v73Z$8mEBIRmbuHH2c`-Ebw_$MxCeIokO2M`Wwbi zfd`T?=)(96@~DL*?kb0du&M2A^LdWMArgnp*hFD8$Aw{ztPnnlBP3o{jts2|vYBCaZ}HBfi5N(=)l8zB%8oMaF{|_9&=Kp3Mw*R6l-g?KrurW$Lnh3=9k!K+FNeGC(YGpuus$1pVT&#GK5$Jl*uf clGO6V$^{!7RrU09lMB@Kic*X9vWguX0Fk5|3IG5A diff --git a/container-stack/svalinn/src/lib/ocaml/McpTypes.res b/container-stack/svalinn/src/lib/ocaml/McpTypes.res deleted file mode 100644 index ae8b73c..0000000 --- a/container-stack/svalinn/src/lib/ocaml/McpTypes.res +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// MCP types for Svalinn edge tools - -// MCP protocol types -type toolInput = { - name: string, - description: option, - type_: string, - required: bool, -} - -type inputSchema = { - type_: string, - properties: Js.Json.t, - required: array, -} - -type tool = { - name: string, - description: string, - inputSchema: inputSchema, -} - -type textContent = { - type_: string, - text: string, -} - -type toolResult = { - content: array, - isError: option, -} - -type listToolsResult = { - tools: array, -} - -// JSON-RPC types -type jsonRpcRequest = { - jsonrpc: string, - method: string, - params: option, - id: option, -} - -type jsonRpcError = { - code: int, - message: string, -} - -type jsonRpcResponse = { - jsonrpc: string, - result: option, - error: option, - id: option, -} - -// Method names -let methodInitialize = "initialize" -let methodListTools = "tools/list" -let methodCallTool = "tools/call" -let methodNotification = "notifications/message" diff --git a/container-stack/svalinn/src/lib/ocaml/Metrics.ast b/container-stack/svalinn/src/lib/ocaml/Metrics.ast deleted file mode 100644 index dbd6e5a867bfcb7cc671e02ddf8462504bc89cdd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24121 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 Xx$qwp_l#f0ibM31if+oE^ws@8g;jej diff --git a/container-stack/svalinn/src/lib/ocaml/Metrics.cmj b/container-stack/svalinn/src/lib/ocaml/Metrics.cmj deleted file mode 100644 index a377fbc5335cd542933d1c488525f30af6c2fe8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 460 zcmZXQu};G<6h+N|#AmRTC`!@_3D%?q%7l=BfzhNdsUfk&^K()6en1!YOo#>4i4pM| zd;l9_X2edKQp8|czVF<7ZTH?ko?m98&8M%Ei_h#EXZH`UjBPwJ#xCui*tPw(#n_N7 zI_gRTPGDYD{Wy*nI~r)nO}-L5Ayb4BBv~YJh*$C(_`~>_!ji9iHRZ2-{b?KSJLfoB iMg56!HIMX!7edM)=LC0rS}hVUjFyK^XwBiZj^iH-^0-?7 diff --git a/container-stack/svalinn/src/lib/ocaml/Metrics.res b/container-stack/svalinn/src/lib/ocaml/Metrics.res deleted file mode 100644 index c99e8a6..0000000 --- a/container-stack/svalinn/src/lib/ocaml/Metrics.res +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Prometheus-compatible metrics collection for Svalinn Edge Gateway -// -// Provides in-memory counters, gauges, and simplified histograms. -// No external dependencies — all state held in mutable refs. - -// ---- Counter ---- - -// A monotonically increasing counter (e.g. total requests). -type counter = { - name: string, - help: string, - mutable value: float, -} - -let makeCounter = (~name: string, ~help: string): counter => { - name, - help, - value: 0.0, -} - -let increment = (counter: counter): unit => { - counter.value = counter.value +. 1.0 -} - -let incrementBy = (counter: counter, n: float): unit => { - counter.value = counter.value +. n -} - -// ---- Gauge ---- - -// A value that can go up or down (e.g. active containers). -type gauge = { - name: string, - help: string, - mutable value: float, -} - -let makeGauge = (~name: string, ~help: string): gauge => { - name, - help, - value: 0.0, -} - -let setGauge = (gauge: gauge, value: float): unit => { - gauge.value = value -} - -// ---- Histogram (simplified) ---- - -// A simplified histogram that counts observations into fixed buckets -// and tracks sum + count for average computation. -type histogram = { - name: string, - help: string, - buckets: array, - mutable counts: array, - mutable sum: float, - mutable count: float, -} - -let makeHistogram = (~name: string, ~help: string, ~buckets: array): histogram => { - name, - help, - buckets, - counts: Belt.Array.make(Belt.Array.length(buckets), 0.0), - sum: 0.0, - count: 0.0, -} - -// Record an observed value into the histogram. -// Increments all bucket counts where the value <= bucket boundary. -let observe = (histogram: histogram, value: float): unit => { - histogram.sum = histogram.sum +. value - histogram.count = histogram.count +. 1.0 - - Belt.Array.forEachWithIndex(histogram.buckets, (i, boundary) => { - if value <= boundary { - let current = switch Belt.Array.get(histogram.counts, i) { - | Some(v) => v - | None => 0.0 - } - Belt.Array.setExn(histogram.counts, i, current +. 1.0) - } - }) -} - -// ---- Global metrics instances ---- - -// Total HTTP requests received. -let requestsTotal = makeCounter( - ~name="svalinn_requests_total", - ~help="Total HTTP requests received", -) - -// Total HTTP request errors (5xx responses). -let requestsErrorsTotal = makeCounter( - ~name="svalinn_requests_errors_total", - ~help="Total HTTP request errors (5xx)", -) - -// Total authentication failures (401/403 responses). -let authFailuresTotal = makeCounter( - ~name="svalinn_auth_failures_total", - ~help="Total authentication failures", -) - -// HTTP request duration in seconds. -let requestDurationSeconds = makeHistogram( - ~name="svalinn_request_duration_seconds", - ~help="HTTP request duration in seconds", - ~buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 5.0], -) - -// Number of active containers (fetched from Vordr on demand). -let containersActive = makeGauge( - ~name="svalinn_containers_active", - ~help="Number of currently active containers", -) - -// ---- Prometheus text format ---- - -// Format a single counter in Prometheus exposition format. -let formatCounter = (c: counter): string => { - `# HELP ${c.name} ${c.help}\n` ++ - `# TYPE ${c.name} counter\n` ++ - `${c.name} ${Belt.Float.toString(c.value)}\n` -} - -// Format a single gauge in Prometheus exposition format. -let formatGauge = (g: gauge): string => { - `# HELP ${g.name} ${g.help}\n` ++ - `# TYPE ${g.name} gauge\n` ++ - `${g.name} ${Belt.Float.toString(g.value)}\n` -} - -// Format a simplified histogram in Prometheus exposition format. -let formatHistogram = (h: histogram): string => { - let header = - `# HELP ${h.name} ${h.help}\n` ++ - `# TYPE ${h.name} histogram\n` - - // Cumulative bucket counts (Prometheus histograms are cumulative). - let cumulativeRef = ref(0.0) - let bucketLines = Belt.Array.mapWithIndex(h.buckets, (i, boundary) => { - let count = switch Belt.Array.get(h.counts, i) { - | Some(v) => v - | None => 0.0 - } - cumulativeRef := cumulativeRef.contents +. count - let cumulative = cumulativeRef.contents - `${h.name}_bucket{le="${Belt.Float.toString(boundary)}"} ${Belt.Float.toString(cumulative)}\n` - }) - - let infLine = `${h.name}_bucket{le="+Inf"} ${Belt.Float.toString(h.count)}\n` - let sumLine = `${h.name}_sum ${Belt.Float.toString(h.sum)}\n` - let countLine = `${h.name}_count ${Belt.Float.toString(h.count)}\n` - - header ++ - Js.Array2.joinWith(bucketLines, "") ++ - infLine ++ - sumLine ++ - countLine -} - -// Format all registered metrics in Prometheus text exposition format. -// Optionally fetches active container count from Vordr first. -let formatPrometheus = (): string => { - formatCounter(requestsTotal) ++ - "\n" ++ - formatCounter(requestsErrorsTotal) ++ - "\n" ++ - formatCounter(authFailuresTotal) ++ - "\n" ++ - formatHistogram(requestDurationSeconds) ++ - "\n" ++ - formatGauge(containersActive) -} - -// Fetch the active container count from Vordr and update the gauge. -// Silently swallows errors (metrics should never break the gateway). -let refreshContainersActive = async (vordrEndpoint: string): unit => { - try { - let response = await Fetch.fetch( - vordrEndpoint ++ "/api/v1/containers", - %raw(`{}`), - ) - if Fetch.Response.ok(response) { - let body = await Fetch.Response.json(response) - // Expect an array of containers; count those with state "running". - switch Js.Json.classify(body) { - | Js.Json.JSONArray(arr) => { - let running = Belt.Array.keep(arr, item => { - switch Js.Json.classify(item) { - | Js.Json.JSONObject(dict) => - switch Js.Dict.get(dict, "state") { - | Some(stateJson) => - switch Js.Json.classify(stateJson) { - | Js.Json.JSONString("running") => true - | _ => false - } - | None => false - } - | _ => false - } - }) - setGauge(containersActive, Belt.Int.toFloat(Belt.Array.length(running))) - } - | _ => () - } - } - } catch { - | _ => () // Silently ignore — gauge keeps its last known value - } -} diff --git a/container-stack/svalinn/src/lib/ocaml/Middleware.ast b/container-stack/svalinn/src/lib/ocaml/Middleware.ast deleted file mode 100644 index d798810c357423f7258a5f42266d4cd80850b685..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57508 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 bJ;nUeB^ftpi4leBZnYg5aG=@b2Alr_M?XF9 diff --git a/container-stack/svalinn/src/lib/ocaml/Middleware.cmj b/container-stack/svalinn/src/lib/ocaml/Middleware.cmj deleted file mode 100644 index 65bfc96f219b8329c3f96085cee7211edaf94403..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 340 zcmY#Ln;(2@&m8aF%UWF@UfOV~W$Lnh3=E703=9l*K>QGhk00P*V9;jRpj(t$SejXs z>RyyzT2MS;!2-txP7V$ZaPi>e{DM>@alORSk__L>l$4y*^2DN4ga*BmqDn`QTxv;1 zehNs3E65yusBmgtNoI0l2}px8NZbHq4ouKD#3vYJ5?CU+C>5v option = "get" -@new external makeUrl: string => 'url = "URL" -@get external urlPathname: 'url => string = "pathname" -@scope("console") @val external consoleWarn: string => unit = "warn" -@new external makeDate: string => Js.Date.t = "Date" -@new external makeDateNow: unit => Js.Date.t = "Date" -@send external getTimeMs: Js.Date.t => float = "getTime" - -// Try each authentication method until one succeeds -let rec tryAuthMethods = async ( - c: Hono.Context.t<'env, 'path>, - config: authConfig, - methods: array, - index: int, - result: ref -) => { - if index >= Belt.Array.length(methods) { - () - } else { - switch methods->Belt.Array.get(index) { - | Some(method) => { - let r: authResult = await tryAuthenticate(c, config, method) - if r.authenticated { - result := r - } else { - await tryAuthMethods(c, config, methods, index + 1, result) - } - } - | None => () // Should never happen due to bounds check, but safe - } - } -} - -// Create authentication middleware -and authMiddleware = (config: authConfig): Hono.middleware<'env, 'path> => { - async (c, next) => { - // Skip if auth disabled - if !config.enabled { - await next() - } else { - // Check excluded paths - let req = Hono.Context.req(c) - let pathname = Hono.Request.url(req)->makeUrl->urlPathname - - let isExcluded = Belt.Array.some(config.excludePaths, p => - Js.String2.startsWith(pathname, p) - ) - - if isExcluded { - await next() - } else { - // Try authentication methods in order - let result = ref({ - authenticated: false, - method: AuthTypes.None, - subject: None, - scopes: None, - token: None, - error: Some("No authentication provided"), - }) - - await tryAuthMethods(c, config, config.methods, 0, result) - - // Store result - Hono.Context.set(c, "authResult", result.contents) - - if result.contents.authenticated { - // Create user context - let token = result.contents.token - let user: userContext = { - id: result.contents.subject->Belt.Option.getWithDefault("anonymous"), - email: token->Belt.Option.flatMap(t => t.email), - name: token->Belt.Option.flatMap(t => t.name), - groups: token->Belt.Option.flatMap(t => t.groups)->Belt.Option.getWithDefault([]), - scopes: result.contents.scopes->Belt.Option.getWithDefault([]), - method: result.contents.method, - issuedAt: token->Belt.Option.map(t => t.iat)->Belt.Option.getWithDefault( - Belt.Float.toInt(Js.Date.now() /. 1000.0) - ), - expiresAt: token->Belt.Option.flatMap(t => Some(t.exp)), - } - - Hono.Context.set(c, "user", user) - await next() - } else { - // Not authenticated - return 401 - let errorMsg = result.contents.error->Belt.Option.getWithDefault("Unauthorized") - let _ = Hono.Context.json( - c, - Js.Json.object_( - Js.Dict.fromArray([ - ("error", Js.Json.string("Unauthorized")), - ("message", Js.Json.string(errorMsg)), - ]) - ), - ~status=401, - () - ) - } - } - } - } -} - -// Try a specific authentication method -and tryAuthenticate = async ( - c: Hono.Context.t<'env, 'path>, - config: authConfig, - method: authMethod -): authResult => { - switch method { - | OAuth2 | OIDC => await authenticateBearerToken(c, config) - | ApiKey => authenticateApiKey(c, config) - | MTLS => authenticateMTLS(c) - | AuthTypes.None => { - authenticated: true, - method: AuthTypes.None, - subject: None, - scopes: None, - token: None, - error: None, - } - } -} - -// Authenticate via Bearer token (OAuth2/OIDC) -and authenticateBearerToken = async ( - c: Hono.Context.t<'env, 'path>, - config: authConfig -): authResult => { - let req = Hono.Context.req(c) - let authHeader = Hono.Request.header(req, "Authorization") - - switch authHeader { - | None => { - authenticated: false, - method: OIDC, - subject: None, - scopes: None, - token: None, - error: Some("No bearer token provided"), - } - | Some(auth) if !Js.String2.startsWith(auth, "Bearer ") => { - authenticated: false, - method: OIDC, - subject: None, - scopes: None, - token: None, - error: Some("No bearer token provided"), - } - | Some(auth) => { - let token = Js.String2.sliceToEnd(auth, ~from=7) - - try { - let payload = switch config.oidc { - | Some(oidcConfig) => await JWT.verifyJWT(token, oidcConfig) - | None => { - // SECURITY: Never accept unverified tokens in production - let env = getEnv("DENO_ENV") - switch env { - | Some("development") | Some("test") => { - consoleWarn("INSECURE: Using unverified JWT decode (dev/test only)") - let (_, payload) = JWT.decodeJWT(token) - payload - } - | _ => { - // Production or unset - require OIDC config - raise(Js.Exn.raiseError("OIDC configuration required in production - cannot verify JWT")) - } - } - } - } - - // Extract scopes - let scopes = switch payload.scope { - | Some(s) => Js.String2.split(s, " ") - | None => [] - } - - { - authenticated: true, - method: OIDC, - subject: Some(payload.sub), - scopes: Some(scopes), - token: Some(payload), - error: None, - } - } catch { - | Js.Exn.Error(e) => { - let message = Js.Exn.message(e)->Belt.Option.getWithDefault("Unknown error") - { - authenticated: false, - method: OIDC, - subject: None, - scopes: None, - token: None, - error: Some(`Token verification failed: ${message}`), - } - } - } - } - } -} - -// Authenticate via API key -and authenticateApiKey = (c: Hono.Context.t<'env, 'path>, config: authConfig): authResult => { - switch config.apiKey { - | None => { - authenticated: false, - method: ApiKey, - subject: None, - scopes: None, - token: None, - error: Some("API key auth not configured"), - } - | Some(apiKeyConfig) => { - let header = apiKeyConfig.header - let req = Hono.Context.req(c) - let apiKeyValue = Hono.Request.header(req, header) - - switch apiKeyValue { - | None => { - authenticated: false, - method: ApiKey, - subject: None, - scopes: None, - token: None, - error: Some(`No API key in ${header} header`), - } - | Some(apiKey) => { - // Remove prefix if configured - let key = switch apiKeyConfig.prefix { - | Some(prefix) if Js.String2.startsWith(apiKey, prefix) => - Js.String2.sliceToEnd(apiKey, ~from=Js.String2.length(prefix)) - | _ => apiKey - } - - // Look up key - switch Belt.Map.String.get(apiKeyConfig.keys, key) { - | None => { - authenticated: false, - method: ApiKey, - subject: None, - scopes: None, - token: None, - error: Some("Invalid API key"), - } - | Some(keyInfo) => { - // Check expiry - let isExpired = switch keyInfo.expiresAt { - | Some(expiresAt) => - makeDate(expiresAt)->getTimeMs < makeDateNow()->getTimeMs - | None => false - } - - if isExpired { - { - authenticated: false, - method: ApiKey, - subject: None, - scopes: None, - token: None, - error: Some("API key expired"), - } - } else { - // Create token payload from key info - let expiresAtTimestamp = switch keyInfo.expiresAt { - | Some(exp) => - Js.Math.floor_int(makeDate(exp)->getTimeMs /. 1000.0) - | None => 0 - } - - let createdAtTimestamp = - Js.Math.floor_int(makeDate(keyInfo.createdAt)->getTimeMs /. 1000.0) - - let tokenPayload: tokenPayload = { - sub: keyInfo.id, - iss: "svalinn", - aud: Js.Json.string("svalinn"), - exp: expiresAtTimestamp, - iat: createdAtTimestamp, - scope: None, - email: None, - name: Some(keyInfo.name), - groups: None, - claims: Js.Dict.empty(), - } - - { - authenticated: true, - method: ApiKey, - subject: Some(keyInfo.id), - scopes: Some(keyInfo.scopes), - token: Some(tokenPayload), - error: None, - } - } - } - } - } - } - } - } -} - -// Authenticate via mTLS client certificate -and authenticateMTLS = (c: Hono.Context.t<'env, 'path>): authResult => { - // Client certificate info would be set by reverse proxy - let req = Hono.Context.req(c) - let clientCert = Hono.Request.header(req, "X-Client-Cert-DN") - let clientCertVerify = Hono.Request.header(req, "X-Client-Cert-Verify") - - switch (clientCert, clientCertVerify) { - | (None, _) | (_, None) => { - authenticated: false, - method: MTLS, - subject: None, - scopes: None, - token: None, - error: Some("No valid client certificate"), - } - | (Some(_), Some(verify)) if verify != "SUCCESS" => { - authenticated: false, - method: MTLS, - subject: None, - scopes: None, - token: None, - error: Some("No valid client certificate"), - } - | (Some(certDN), Some(_)) => { - // Parse CN from DN - let matchResult: option> = %raw(`certDN.match(/CN=([^,]+)/)`) - let subject = switch matchResult { - | Some(matches) => matches->Belt.Array.get(1)->Belt.Option.getWithDefault(certDN) - | None => certDN - } - - { - authenticated: true, - method: MTLS, - subject: Some(subject), - scopes: Some(["svalinn:read", "svalinn:write"]), - token: None, - error: None, - } - } - } -} - -// Require specific scopes middleware -let requireScopes = (requiredScopes: array): Hono.middleware<'env, 'path> => { - async (c, next) => { - let user = Hono.Context.get(c, "user") - - switch user { - | None => { - let _ = Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string("Not authenticated"))])), - ~status=401, - () - ) - } - | Some((u: userContext)) => { - let missingScopes = Belt.Array.keep(requiredScopes, s => - !Belt.Array.some(u.scopes, us => us == s) && !Belt.Array.some(u.scopes, us => us == "svalinn:admin") - ) - - if Belt.Array.length(missingScopes) > 0 { - let _ = Hono.Context.json( - c, - Js.Json.object_( - Js.Dict.fromArray([ - ("error", Js.Json.string("Forbidden")), - ("message", Js.Json.string("Insufficient scopes")), - ("required", requiredScopes->Js.Json.stringArray), - ("missing", missingScopes->Js.Json.stringArray), - ]) - ), - ~status=403, - () - ) - } else { - await next() - } - } - } - } -} - -// Require specific groups middleware -let requireGroups = (requiredGroups: array): Hono.middleware<'env, 'path> => { - async (c, next) => { - let user = Hono.Context.get(c, "user") - - switch user { - | None => { - let _ = Hono.Context.json( - c, - Js.Json.object_(Js.Dict.fromArray([("error", Js.Json.string("Not authenticated"))])), - ~status=401, - () - ) - } - | Some((u: userContext)) => { - let hasGroup = Belt.Array.some(requiredGroups, g => Belt.Array.some(u.groups, ug => ug == g)) - - if !hasGroup { - let _ = Hono.Context.json( - c, - Js.Json.object_( - Js.Dict.fromArray([ - ("error", Js.Json.string("Forbidden")), - ("message", Js.Json.string("Not a member of required groups")), - ("required", requiredGroups->Js.Json.stringArray), - ]) - ), - ~status=403, - () - ) - } else { - await next() - } - } - } - } -} - -// Create default auth config -let createAuthConfig = (~options: option=?, ()): authConfig => { - let defaults = { - enabled: false, - methods: [OIDC, ApiKey], - oauth2: None, - oidc: None, - apiKey: Some({ - header: "X-API-Key", - prefix: None, - keys: Belt.Map.String.empty, - }), - mtls: None, - excludePaths: ["/healthz", "/health", "/ready", "/metrics", "/.well-known/"], - } - - switch options { - | None => defaults - | Some(opts) => opts - } -} - -// Load auth config from environment -let loadAuthConfigFromEnv = (): authConfig => { - let enabled = switch getEnv("AUTH_ENABLED") { - | Some("true") => true - | _ => false - } - - let methods = switch getEnv("AUTH_METHODS") { - | Some(methodsStr) => - Js.String2.split(methodsStr, ",")->Belt.Array.keepMap(authMethodFromString) - | None => [OIDC, ApiKey] - } - - let config = { - enabled, - methods, - oauth2: None, - oidc: None, - apiKey: Some({ - header: "X-API-Key", - prefix: None, - keys: Belt.Map.String.empty, - }), - mtls: None, - excludePaths: ["/healthz", "/health", "/ready", "/metrics", "/.well-known/"], - } - - // Load OIDC config - let oidcConfig = switch getEnv("OIDC_ISSUER") { - | Some(issuer) => Some({ - issuer, - clientId: getEnv("OIDC_CLIENT_ID")->Belt.Option.getWithDefault(""), - clientSecret: getEnv("OIDC_CLIENT_SECRET")->Belt.Option.getWithDefault(""), - authorizationEndpoint: getEnv("OIDC_AUTH_ENDPOINT")->Belt.Option.getWithDefault(""), - tokenEndpoint: getEnv("OIDC_TOKEN_ENDPOINT")->Belt.Option.getWithDefault(""), - userInfoEndpoint: getEnv("OIDC_USERINFO_ENDPOINT")->Belt.Option.getWithDefault(""), - jwksUri: getEnv("OIDC_JWKS_URI")->Belt.Option.getWithDefault(""), - redirectUri: getEnv("OIDC_REDIRECT_URI")->Belt.Option.getWithDefault(""), - scopes: getEnv("OIDC_SCOPES") - ->Belt.Option.getWithDefault("openid profile email") - ->Js.String2.split(" "), - endSessionEndpoint: getEnv("OIDC_END_SESSION_ENDPOINT"), - }) - | None => None - } - - // Load API keys from environment (comma-separated id:key:scopes format) - let apiKeyConfig = switch getEnv("API_KEYS") { - | Some(apiKeysStr) => { - let keys = Js.String2.split(apiKeysStr, ",")->Belt.Array.reduce(Belt.Map.String.empty, (acc, entry) => { - let parts = Js.String2.split(entry, ":") - switch (parts->Belt.Array.get(0), parts->Belt.Array.get(1)) { - | (Some(id), Some(key)) => { - let scopes = switch parts->Belt.Array.get(2) { - | Some(scopesStr) => Js.String2.split(scopesStr, "+") - | None => ["svalinn:read"] - } - - Belt.Map.String.set(acc, key, { - id, - name: id, - scopes, - createdAt: %raw(`new Date().toISOString()`), - expiresAt: None, - rateLimit: None, - }) - } - | _ => acc - } - }) - - Some({ - header: "X-API-Key", - prefix: None, - keys, - }) - } - | None => config.apiKey - } - - {...config, oidc: oidcConfig, apiKey: apiKeyConfig} -} diff --git a/container-stack/svalinn/src/lib/ocaml/OAuth2.ast b/container-stack/svalinn/src/lib/ocaml/OAuth2.ast deleted file mode 100644 index 5f3f589aed53288cd5976fc1d06667b336cd78cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50775 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