@suveren/gateway 0.3.17 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/bin/suveren-gateway.js +419 -28
  2. package/content/integrations/calendar.json +20 -8
  3. package/content/integrations/crm.json +97 -23
  4. package/content/integrations/gmail.json +166 -29
  5. package/content/integrations/linkedin.json +15 -3
  6. package/content/integrations/mollie.json +2 -2
  7. package/dist/control-plane/index.mjs +333 -41
  8. package/dist/mcp-server/http.mjs +880 -210
  9. package/dist/ui/assets/index-BDQY7Z3H.js +105 -0
  10. package/dist/ui/assets/{index-DOt3SNnl.css → index-JHaCddDE.css} +1 -1
  11. package/dist/ui/index.html +2 -2
  12. package/node_modules/@hap/core/dist/index.d.mts +27 -0
  13. package/node_modules/@hap/core/dist/index.d.ts +27 -0
  14. package/node_modules/@hap/core/dist/index.js +13 -1
  15. package/node_modules/@hap/core/dist/index.mjs +13 -1
  16. package/node_modules/@hap/core/package.json +1 -1
  17. package/node_modules/@hap/core/src/gatekeeper.ts +47 -1
  18. package/node_modules/@hap/core/src/types.ts +27 -0
  19. package/node_modules/@hono/node-server/README.md +58 -25
  20. package/node_modules/@hono/node-server/dist/conninfo.cjs +22 -0
  21. package/node_modules/@hono/node-server/dist/{conninfo.d.ts → conninfo.d.cts} +4 -3
  22. package/node_modules/@hono/node-server/dist/conninfo.d.mts +4 -3
  23. package/node_modules/@hono/node-server/dist/conninfo.mjs +19 -16
  24. package/node_modules/@hono/node-server/dist/constants-BLSFu_RU.mjs +5 -0
  25. package/node_modules/@hono/node-server/dist/constants-BXAKTxRC.cjs +11 -0
  26. package/node_modules/@hono/node-server/dist/index.cjs +1173 -0
  27. package/node_modules/@hono/node-server/dist/index.d.cts +101 -0
  28. package/node_modules/@hono/node-server/dist/index.d.mts +101 -8
  29. package/node_modules/@hono/node-server/dist/index.mjs +1143 -637
  30. package/node_modules/@hono/node-server/dist/serve-static.cjs +151 -0
  31. package/node_modules/@hono/node-server/dist/serve-static.d.cts +18 -0
  32. package/node_modules/@hono/node-server/dist/serve-static.d.mts +14 -13
  33. package/node_modules/@hono/node-server/dist/serve-static.mjs +143 -145
  34. package/node_modules/@hono/node-server/dist/utils/response.cjs +8 -0
  35. package/node_modules/@hono/node-server/dist/utils/response.d.cts +4 -0
  36. package/node_modules/@hono/node-server/dist/utils/response.d.mts +3 -2
  37. package/node_modules/@hono/node-server/dist/utils/response.mjs +6 -9
  38. package/node_modules/@hono/node-server/dist/utils/stream.cjs +65 -0
  39. package/node_modules/@hono/node-server/dist/utils/stream.d.cts +6 -0
  40. package/node_modules/@hono/node-server/dist/utils/stream.d.mts +6 -0
  41. package/node_modules/@hono/node-server/dist/utils/stream.mjs +64 -0
  42. package/node_modules/@hono/node-server/package.json +65 -54
  43. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/index.d.ts.map +1 -1
  44. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/index.js +1 -12
  45. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/index.js.map +1 -1
  46. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/stdio.d.ts +7 -0
  47. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/stdio.d.ts.map +1 -1
  48. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/stdio.js +9 -3
  49. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/stdio.js.map +1 -1
  50. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.d.ts.map +1 -1
  51. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js +5 -3
  52. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/client/streamableHttp.js.map +1 -1
  53. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/index.d.ts.map +1 -1
  54. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/index.js +1 -12
  55. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/index.js.map +1 -1
  56. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/sseKeepAlive.d.ts +5 -0
  57. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/sseKeepAlive.d.ts.map +1 -0
  58. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/sseKeepAlive.js +17 -0
  59. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/sseKeepAlive.js.map +1 -0
  60. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/stdio.d.ts +9 -1
  61. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/stdio.d.ts.map +1 -1
  62. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/stdio.js +10 -4
  63. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/stdio.js.map +1 -1
  64. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.d.ts +21 -0
  65. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.d.ts.map +1 -1
  66. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.js +239 -41
  67. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/webStandardStreamableHttp.js.map +1 -1
  68. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.d.ts.map +1 -1
  69. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.js +25 -6
  70. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/server/zod-compat.js.map +1 -1
  71. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/mediaType.d.ts +32 -0
  72. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/mediaType.d.ts.map +1 -0
  73. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/mediaType.js +64 -0
  74. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/mediaType.js.map +1 -0
  75. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/stdio.d.ts +5 -0
  76. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/stdio.d.ts.map +1 -1
  77. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/stdio.js +10 -1
  78. package/node_modules/@modelcontextprotocol/sdk/dist/cjs/shared/stdio.js.map +1 -1
  79. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/index.d.ts.map +1 -1
  80. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/index.js +2 -13
  81. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/index.js.map +1 -1
  82. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.d.ts +7 -0
  83. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.d.ts.map +1 -1
  84. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js +9 -3
  85. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/stdio.js.map +1 -1
  86. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.d.ts.map +1 -1
  87. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js +5 -3
  88. package/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js.map +1 -1
  89. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.d.ts.map +1 -1
  90. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js +2 -13
  91. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js.map +1 -1
  92. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.d.ts +5 -0
  93. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.d.ts.map +1 -0
  94. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.js +13 -0
  95. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/sseKeepAlive.js.map +1 -0
  96. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.d.ts +9 -1
  97. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.d.ts.map +1 -1
  98. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js +10 -4
  99. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js.map +1 -1
  100. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.d.ts +21 -0
  101. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.d.ts.map +1 -1
  102. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js +239 -41
  103. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/webStandardStreamableHttp.js.map +1 -1
  104. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.d.ts.map +1 -1
  105. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js +25 -6
  106. package/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js.map +1 -1
  107. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.d.ts +32 -0
  108. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.d.ts.map +1 -0
  109. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.js +57 -0
  110. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/mediaType.js.map +1 -0
  111. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.d.ts +5 -0
  112. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.d.ts.map +1 -1
  113. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js +9 -0
  114. package/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js.map +1 -1
  115. package/node_modules/@modelcontextprotocol/sdk/node_modules/media-typer/README.md +15 -14
  116. package/node_modules/@modelcontextprotocol/sdk/node_modules/media-typer/index.js +0 -8
  117. package/node_modules/@modelcontextprotocol/sdk/node_modules/media-typer/package.json +16 -12
  118. package/node_modules/@modelcontextprotocol/sdk/package.json +4 -3
  119. package/node_modules/@types/node/README.md +1 -1
  120. package/node_modules/@types/node/crypto.d.ts +1 -1
  121. package/node_modules/@types/node/package.json +2 -2
  122. package/node_modules/body-parser/HISTORY.md +7 -1
  123. package/node_modules/body-parser/lib/types/json.js +7 -3
  124. package/node_modules/body-parser/lib/types/raw.js +7 -3
  125. package/node_modules/body-parser/lib/types/text.js +7 -3
  126. package/node_modules/body-parser/lib/types/urlencoded.js +7 -3
  127. package/node_modules/body-parser/package.json +1 -1
  128. package/node_modules/express-rate-limit/dist/index.cjs +69 -5
  129. package/node_modules/express-rate-limit/dist/index.d.cts +16 -0
  130. package/node_modules/express-rate-limit/dist/index.d.mts +16 -0
  131. package/node_modules/express-rate-limit/dist/index.d.ts +16 -0
  132. package/node_modules/express-rate-limit/dist/index.mjs +55 -5
  133. package/node_modules/express-rate-limit/node_modules/debug/LICENSE +20 -0
  134. package/node_modules/express-rate-limit/node_modules/debug/README.md +481 -0
  135. package/node_modules/express-rate-limit/node_modules/debug/package.json +64 -0
  136. package/node_modules/express-rate-limit/node_modules/debug/src/browser.js +272 -0
  137. package/node_modules/express-rate-limit/node_modules/debug/src/common.js +292 -0
  138. package/node_modules/express-rate-limit/node_modules/debug/src/index.js +10 -0
  139. package/node_modules/express-rate-limit/node_modules/debug/src/node.js +263 -0
  140. package/node_modules/express-rate-limit/node_modules/ms/index.js +162 -0
  141. package/node_modules/express-rate-limit/node_modules/ms/license.md +21 -0
  142. package/node_modules/express-rate-limit/node_modules/ms/package.json +38 -0
  143. package/node_modules/express-rate-limit/node_modules/ms/readme.md +59 -0
  144. package/node_modules/express-rate-limit/package.json +12 -9
  145. package/node_modules/fast-uri/index.js +17 -0
  146. package/node_modules/fast-uri/package.json +1 -1
  147. package/node_modules/fast-uri/test/security.test.js +62 -0
  148. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -4
  149. package/node_modules/hono/dist/adapter/lambda-edge/handler.js +12 -2
  150. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -4
  151. package/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +12 -2
  152. package/node_modules/hono/dist/cjs/client/client.js +10 -1
  153. package/node_modules/hono/dist/cjs/client/utils.js +1 -1
  154. package/node_modules/hono/dist/cjs/helper/streaming/sse.js +5 -4
  155. package/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
  156. package/node_modules/hono/dist/cjs/middleware/compress/index.js +2 -1
  157. package/node_modules/hono/dist/cjs/middleware/etag/index.js +2 -1
  158. package/node_modules/hono/dist/cjs/middleware/method-override/index.js +5 -3
  159. package/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +10 -4
  160. package/node_modules/hono/dist/cjs/request.js +8 -3
  161. package/node_modules/hono/dist/cjs/router/trie-router/node.js +9 -0
  162. package/node_modules/hono/dist/cjs/utils/accept.js +1 -1
  163. package/node_modules/hono/dist/cjs/utils/body.js +6 -0
  164. package/node_modules/hono/dist/cjs/utils/url.js +1 -1
  165. package/node_modules/hono/dist/client/client.js +10 -1
  166. package/node_modules/hono/dist/client/utils.js +1 -1
  167. package/node_modules/hono/dist/helper/streaming/sse.js +5 -4
  168. package/node_modules/hono/dist/middleware/cache/index.js +1 -1
  169. package/node_modules/hono/dist/middleware/compress/index.js +2 -1
  170. package/node_modules/hono/dist/middleware/etag/index.js +2 -1
  171. package/node_modules/hono/dist/middleware/method-override/index.js +5 -3
  172. package/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +10 -4
  173. package/node_modules/hono/dist/request.js +8 -3
  174. package/node_modules/hono/dist/router/trie-router/node.js +9 -0
  175. package/node_modules/hono/dist/types/adapter/aws-lambda/types.d.ts +9 -0
  176. package/node_modules/hono/dist/types/adapter/lambda-edge/handler.d.ts +1 -1
  177. package/node_modules/hono/dist/types/helper/websocket/index.d.ts +1 -1
  178. package/node_modules/hono/dist/types/middleware/combine/index.d.ts +1 -1
  179. package/node_modules/hono/dist/types/middleware/language/language.d.ts +18 -0
  180. package/node_modules/hono/dist/types/utils/types.d.ts +1 -1
  181. package/node_modules/hono/dist/utils/accept.js +1 -1
  182. package/node_modules/hono/dist/utils/body.js +6 -0
  183. package/node_modules/hono/dist/utils/url.js +1 -1
  184. package/node_modules/hono/package.json +4 -4
  185. package/node_modules/ip-address/README.md +102 -96
  186. package/node_modules/ip-address/dist/common.d.ts +23 -0
  187. package/node_modules/ip-address/dist/common.js +27 -4
  188. package/node_modules/ip-address/dist/common.js.map +1 -1
  189. package/node_modules/ip-address/dist/ipv4.d.ts +8 -1
  190. package/node_modules/ip-address/dist/ipv4.js +21 -8
  191. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  192. package/node_modules/ip-address/dist/ipv6.d.ts +59 -1
  193. package/node_modules/ip-address/dist/ipv6.js +149 -41
  194. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  195. package/node_modules/ip-address/dist/v4/constants.js +5 -1
  196. package/node_modules/ip-address/dist/v4/constants.js.map +1 -1
  197. package/node_modules/ip-address/dist/v6/constants.js +3 -2
  198. package/node_modules/ip-address/dist/v6/constants.js.map +1 -1
  199. package/node_modules/ip-address/package.json +2 -2
  200. package/node_modules/jose/README.md +1 -4
  201. package/node_modules/jose/dist/types/jwks/remote.d.ts +3 -3
  202. package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
  203. package/node_modules/jose/dist/webapi/lib/key_to_jwk.js +4 -1
  204. package/node_modules/jose/package.json +1 -1
  205. package/package.json +2 -2
  206. package/profiles/customers/0.4.profile.json +7 -4
  207. package/profiles/customers/0.5.profile.json +131 -0
  208. package/profiles/customers/0.6.profile.json +153 -0
  209. package/profiles/email/0.4.profile.json +8 -4
  210. package/profiles/index.json +2 -0
  211. package/server.js +10 -0
  212. package/dist/ui/assets/index-CTmWS7W4.js +0 -105
  213. package/node_modules/@hono/node-server/dist/conninfo.js +0 -42
  214. package/node_modules/@hono/node-server/dist/globals.d.mts +0 -2
  215. package/node_modules/@hono/node-server/dist/globals.d.ts +0 -2
  216. package/node_modules/@hono/node-server/dist/globals.js +0 -29
  217. package/node_modules/@hono/node-server/dist/globals.mjs +0 -5
  218. package/node_modules/@hono/node-server/dist/index.d.ts +0 -8
  219. package/node_modules/@hono/node-server/dist/index.js +0 -702
  220. package/node_modules/@hono/node-server/dist/listener.d.mts +0 -13
  221. package/node_modules/@hono/node-server/dist/listener.d.ts +0 -13
  222. package/node_modules/@hono/node-server/dist/listener.js +0 -670
  223. package/node_modules/@hono/node-server/dist/listener.mjs +0 -635
  224. package/node_modules/@hono/node-server/dist/request.d.mts +0 -25
  225. package/node_modules/@hono/node-server/dist/request.d.ts +0 -25
  226. package/node_modules/@hono/node-server/dist/request.js +0 -238
  227. package/node_modules/@hono/node-server/dist/request.mjs +0 -206
  228. package/node_modules/@hono/node-server/dist/response.d.mts +0 -26
  229. package/node_modules/@hono/node-server/dist/response.d.ts +0 -26
  230. package/node_modules/@hono/node-server/dist/response.js +0 -112
  231. package/node_modules/@hono/node-server/dist/response.mjs +0 -85
  232. package/node_modules/@hono/node-server/dist/serve-static.d.ts +0 -17
  233. package/node_modules/@hono/node-server/dist/serve-static.js +0 -177
  234. package/node_modules/@hono/node-server/dist/server.d.mts +0 -10
  235. package/node_modules/@hono/node-server/dist/server.d.ts +0 -10
  236. package/node_modules/@hono/node-server/dist/server.js +0 -696
  237. package/node_modules/@hono/node-server/dist/server.mjs +0 -660
  238. package/node_modules/@hono/node-server/dist/types.d.mts +0 -44
  239. package/node_modules/@hono/node-server/dist/types.d.ts +0 -44
  240. package/node_modules/@hono/node-server/dist/types.js +0 -18
  241. package/node_modules/@hono/node-server/dist/types.mjs +0 -0
  242. package/node_modules/@hono/node-server/dist/utils/response/constants.d.mts +0 -3
  243. package/node_modules/@hono/node-server/dist/utils/response/constants.d.ts +0 -3
  244. package/node_modules/@hono/node-server/dist/utils/response/constants.js +0 -30
  245. package/node_modules/@hono/node-server/dist/utils/response/constants.mjs +0 -5
  246. package/node_modules/@hono/node-server/dist/utils/response.d.ts +0 -3
  247. package/node_modules/@hono/node-server/dist/utils/response.js +0 -37
  248. package/node_modules/@hono/node-server/dist/utils.d.mts +0 -9
  249. package/node_modules/@hono/node-server/dist/utils.d.ts +0 -9
  250. package/node_modules/@hono/node-server/dist/utils.js +0 -99
  251. package/node_modules/@hono/node-server/dist/utils.mjs +0 -71
  252. package/node_modules/@hono/node-server/dist/vercel.d.mts +0 -7
  253. package/node_modules/@hono/node-server/dist/vercel.d.ts +0 -7
  254. package/node_modules/@hono/node-server/dist/vercel.js +0 -677
  255. package/node_modules/@hono/node-server/dist/vercel.mjs +0 -640
@@ -34,6 +34,19 @@ var SPClient = class {
34
34
  setSessionCookie(cookie) {
35
35
  this.sessionCookie = cookie;
36
36
  }
37
+ /**
38
+ * Has anyone signed in yet this run?
39
+ *
40
+ * The control-plane pushes the session cookie on login, so its absence means
41
+ * the vault is locked — the gateway is up and answering, but it cannot read
42
+ * authorizations or credentials. Callers MUST distinguish that from "this
43
+ * user has no authorizations": the two look identical from the outside, and
44
+ * reporting the latter sends someone off to create authorizations when all
45
+ * they needed to do was enter their API key.
46
+ */
47
+ isUnlocked() {
48
+ return this.sessionCookie !== "";
49
+ }
37
50
  async fetch(path, init) {
38
51
  const url = `${this.baseUrl}${path}`;
39
52
  const headers = {
@@ -643,11 +656,131 @@ function windowCutoff(window, now) {
643
656
  return now - 24 * 60 * 60;
644
657
  case "weekly":
645
658
  return now - 7 * 24 * 60 * 60;
646
- case "monthly":
647
- return now - 30 * 24 * 60 * 60;
659
+ case "monthly": {
660
+ const d = new Date(now * 1e3);
661
+ return Math.floor(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1, 0, 0, 0, 0) / 1e3);
662
+ }
648
663
  }
649
664
  }
650
665
 
666
+ // src/lib/denial-log.ts
667
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, unlinkSync as unlinkSync3 } from "fs";
668
+ import { homedir as homedir3 } from "os";
669
+ import { dirname as dirname3, join as join3 } from "path";
670
+ import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes3 } from "crypto";
671
+ var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join3(homedir3(), ".suveren");
672
+ var MAX_RECORDS = 200;
673
+ var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
674
+ var DenialLog = class {
675
+ records = [];
676
+ baseDir;
677
+ vaultKey = null;
678
+ constructor(filePath) {
679
+ this.baseDir = filePath && filePath.endsWith(".json") ? dirname3(filePath) : filePath ?? DEFAULT_DIR3;
680
+ this.loadPlaintext();
681
+ }
682
+ setVaultKey(key) {
683
+ this.vaultKey = key;
684
+ if (existsSync3(this.encryptedFilePath)) {
685
+ this.loadEncrypted();
686
+ } else if (this.records.length > 0) {
687
+ this.persistEncrypted();
688
+ if (existsSync3(this.plaintextFilePath)) {
689
+ try {
690
+ unlinkSync3(this.plaintextFilePath);
691
+ } catch {
692
+ }
693
+ }
694
+ }
695
+ }
696
+ /** Record a read denial. Never throws — recording must not break the denial. */
697
+ record(rec) {
698
+ try {
699
+ this.records.push(rec);
700
+ this.prune(rec.ts);
701
+ this.persist();
702
+ } catch (err) {
703
+ console.error("[DenialLog] failed to record denial:", err);
704
+ }
705
+ }
706
+ /** Recent denials, newest first. `sinceMs` and `limit` are optional filters. */
707
+ getRecent(sinceMs, limit = MAX_RECORDS) {
708
+ let out = [...this.records].sort((a, b) => b.ts - a.ts);
709
+ if (sinceMs !== void 0) out = out.filter((r) => r.ts >= sinceMs);
710
+ return out.slice(0, limit);
711
+ }
712
+ get size() {
713
+ return this.records.length;
714
+ }
715
+ // ─── Retention ────────────────────────────────────────────────────────────
716
+ prune(now) {
717
+ const cutoff = now - MAX_AGE_MS;
718
+ this.records = this.records.filter((r) => r.ts >= cutoff);
719
+ if (this.records.length > MAX_RECORDS) {
720
+ this.records = this.records.slice(this.records.length - MAX_RECORDS);
721
+ }
722
+ }
723
+ // ─── Encryption (mirrors ExecutionLog) ──────────────────────────────────────
724
+ encrypt(plaintext) {
725
+ if (!this.vaultKey) throw new Error("No vault key");
726
+ const iv = randomBytes3(12);
727
+ const cipher = createCipheriv3("aes-256-gcm", this.vaultKey, iv);
728
+ const enc = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
729
+ return { iv: iv.toString("hex"), ciphertext: enc.toString("hex"), tag: cipher.getAuthTag().toString("hex") };
730
+ }
731
+ decrypt(blob) {
732
+ if (!this.vaultKey) throw new Error("No vault key");
733
+ const decipher = createDecipheriv3("aes-256-gcm", this.vaultKey, Buffer.from(blob.iv, "hex"));
734
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
735
+ return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
736
+ }
737
+ get plaintextFilePath() {
738
+ return join3(this.baseDir, "denials.json");
739
+ }
740
+ get encryptedFilePath() {
741
+ return join3(this.baseDir, "denials.enc.json");
742
+ }
743
+ loadPlaintext() {
744
+ if (!existsSync3(this.plaintextFilePath)) {
745
+ mkdirSync3(this.baseDir, { recursive: true });
746
+ return;
747
+ }
748
+ try {
749
+ const data = JSON.parse(readFileSync3(this.plaintextFilePath, "utf-8"));
750
+ this.records = data.records ?? [];
751
+ } catch {
752
+ console.error(`[DenialLog] Could not parse ${this.plaintextFilePath}, starting fresh`);
753
+ this.records = [];
754
+ }
755
+ }
756
+ loadEncrypted() {
757
+ if (!existsSync3(this.encryptedFilePath)) return;
758
+ try {
759
+ const data = JSON.parse(readFileSync3(this.encryptedFilePath, "utf-8"));
760
+ const logFile = JSON.parse(this.decrypt(data.blob));
761
+ this.records = logFile.records ?? [];
762
+ } catch (err) {
763
+ console.error(`[DenialLog] Could not decrypt ${this.encryptedFilePath}:`, err);
764
+ this.records = [];
765
+ }
766
+ }
767
+ persist() {
768
+ if (this.vaultKey) this.persistEncrypted();
769
+ else this.persistPlaintext();
770
+ }
771
+ persistPlaintext() {
772
+ const data = { version: 1, records: this.records };
773
+ mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
774
+ writeFileSync3(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
775
+ }
776
+ persistEncrypted() {
777
+ const logFile = { version: 1, records: this.records };
778
+ const data = { version: 1, blob: this.encrypt(JSON.stringify(logFile)) };
779
+ mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
780
+ writeFileSync3(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
781
+ }
782
+ };
783
+
651
784
  // src/lib/gatekeeper.ts
652
785
  import { verify } from "@hap/core";
653
786
  var MCPGatekeeper = class {
@@ -699,7 +832,15 @@ var MCPGatekeeper = class {
699
832
  frame,
700
833
  attestations: auth.attestations.map((a) => a.blob),
701
834
  execution,
702
- context: resolvedContext
835
+ context: resolvedContext,
836
+ // Scopes the local cumulative check to the same (profileId, path) key the
837
+ // ExecutionLog records under (see tool-proxy's record call). It cannot go
838
+ // inside `frame`: that is validated against the profile's boundsSchema,
839
+ // which declares no `path` field in any shipped profile, so an extra key
840
+ // there fails verification with "Unknown field". Omitting it entirely —
841
+ // the previous behaviour — made every cumulative lookup return zero, so
842
+ // the local gate never fired and the AS was doing all the enforcing.
843
+ path: auth.path
703
844
  };
704
845
  const result = await verify(request, publicKeyHex, void 0, this.executionLog);
705
846
  return { result, authorization: auth };
@@ -712,12 +853,14 @@ var SharedState = class {
712
853
  cache;
713
854
  gateStore;
714
855
  executionLog;
856
+ denialLog;
715
857
  gatekeeper;
716
858
  constructor(spUrl2, gateStorePath) {
717
859
  this.spClient = new SPClient(spUrl2);
718
860
  this.cache = new AttestationCache(this.spClient);
719
861
  this.gateStore = new GateStore(gateStorePath);
720
862
  this.executionLog = new ExecutionLog(gateStorePath);
863
+ this.denialLog = new DenialLog(gateStorePath);
721
864
  this.gatekeeper = new MCPGatekeeper(this.cache, this.executionLog);
722
865
  }
723
866
  setGateContent(path, authorizationId, profileId, content, opts) {
@@ -771,6 +914,7 @@ function getConsumptionState(auth, executionLog, profile) {
771
914
  const fieldDef = rawFieldDef;
772
915
  const bt = fieldDef.boundType;
773
916
  if (!bt) continue;
917
+ if (fieldDef.enforced === false) continue;
774
918
  if (bt.kind !== "cumulative_sum" && bt.kind !== "cumulative_count") continue;
775
919
  const boundValue = auth.frame[fieldName];
776
920
  const limit = typeof boundValue === "number" ? boundValue : null;
@@ -783,7 +927,10 @@ function getConsumptionState(auth, executionLog, profile) {
783
927
  now
784
928
  );
785
929
  entries.push({
786
- label: fieldDef.description ?? fieldName,
930
+ // displayName FIRST: description is prose meant for a human reading the
931
+ // profile, and rendering it here produced a paragraph where a label
932
+ // belongs ("Not currently enforced: reads carry no receipt… : 0 / 7").
933
+ label: fieldDef.displayName ?? fieldDef.description ?? fieldName,
787
934
  current,
788
935
  limit,
789
936
  window: bt.window,
@@ -821,17 +968,17 @@ function formatConsumptionFull(entries) {
821
968
  }
822
969
 
823
970
  // src/lib/context-loader.ts
824
- import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
825
- import { homedir as homedir3 } from "os";
826
- import { join as join3 } from "path";
827
- var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join3(homedir3(), ".suveren");
971
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
972
+ import { homedir as homedir4 } from "os";
973
+ import { join as join4 } from "path";
974
+ var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join4(homedir4(), ".suveren");
828
975
  var BRIEF_MAX_CHARS = 16 * 1024;
829
976
  function readContextFile(dataDir) {
830
- const dir = dataDir ?? DEFAULT_DIR3;
831
- const filePath = join3(dir, "context.md");
832
- if (!existsSync3(filePath)) return null;
977
+ const dir = dataDir ?? DEFAULT_DIR4;
978
+ const filePath = join4(dir, "context.md");
979
+ if (!existsSync4(filePath)) return null;
833
980
  try {
834
- const content = readFileSync3(filePath, "utf-8").trim();
981
+ const content = readFileSync4(filePath, "utf-8").trim();
835
982
  return content || null;
836
983
  } catch {
837
984
  return null;
@@ -929,8 +1076,22 @@ function buildMandateBrief(opts) {
929
1076
  return lines.join("\n");
930
1077
  }
931
1078
 
1079
+ // src/lib/locked-notice.ts
1080
+ function uiUrl() {
1081
+ const port2 = process.env.SUVEREN_CP_PORT ?? "3400";
1082
+ return `http://localhost:${port2}`;
1083
+ }
1084
+ function lockedNotice(action) {
1085
+ const what = action ? `Cannot ${action}: the` : "The";
1086
+ return `${what} Suveren gateway is running but LOCKED, so it cannot read any authorizations or credentials.
1087
+
1088
+ This is not the same as having no authorizations \u2014 yours are on disk, encrypted, and unreadable until someone unlocks them.
1089
+
1090
+ TELL THE USER: open ${uiUrl()} and enter your Suveren API key to unlock the gateway. It boots locked by design after every restart; the key is never stored.`;
1091
+ }
1092
+
932
1093
  // src/tools/authorizations.ts
933
- import { getProfile as getProfile3 } from "@hap/core";
1094
+ import { getProfile as getProfile4 } from "@hap/core";
934
1095
 
935
1096
  // src/lib/receipt-footer.ts
936
1097
  import { deriveIdentityLine } from "@hap/core";
@@ -1012,7 +1173,168 @@ function attachReceiptId(tool, args, receiptId) {
1012
1173
  return { ...args, receipt_id: receiptId };
1013
1174
  }
1014
1175
 
1176
+ // src/lib/scope-specificity.ts
1177
+ function tokenSet(value) {
1178
+ if (value === void 0 || value === null) return /* @__PURE__ */ new Set();
1179
+ return new Set(
1180
+ String(value).split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)
1181
+ );
1182
+ }
1183
+ function subsetOrEqual(a, b) {
1184
+ if (b.size === 0) return true;
1185
+ if (a.size === 0) return false;
1186
+ for (const x of a) if (!b.has(x)) return false;
1187
+ return true;
1188
+ }
1189
+ function atLeastAsSpecific(keys, a, b) {
1190
+ for (const k of keys) {
1191
+ if (!subsetOrEqual(tokenSet(a[k]), tokenSet(b[k]))) return false;
1192
+ }
1193
+ return true;
1194
+ }
1195
+ function strictlyMoreSpecific(keys, a, b) {
1196
+ return atLeastAsSpecific(keys, a, b) && !atLeastAsSpecific(keys, b, a);
1197
+ }
1198
+ function selectAuthorization(contextKeys, passers) {
1199
+ if (passers.length === 1) {
1200
+ return { chosen: passers[0], superseded: [], reason: "sole" };
1201
+ }
1202
+ const dominators = passers.filter(
1203
+ (a) => passers.every((b) => b === a || strictlyMoreSpecific(contextKeys, a.context, b.context))
1204
+ );
1205
+ if (dominators.length === 1) {
1206
+ const chosen2 = dominators[0];
1207
+ return { chosen: chosen2, superseded: passers.filter((p) => p !== chosen2), reason: "most-specific" };
1208
+ }
1209
+ const approvers = passers.filter((p) => p.requiresApproval);
1210
+ const pool = approvers.length > 0 ? approvers : passers;
1211
+ const chosen = pool.slice().sort((a, b) => a.id.localeCompare(b.id))[0];
1212
+ return {
1213
+ chosen,
1214
+ superseded: passers.filter((p) => p !== chosen),
1215
+ reason: approvers.length > 0 ? "fail-safe-approval" : "fail-safe-tiebreak"
1216
+ };
1217
+ }
1218
+
1219
+ // src/lib/read-gate.ts
1220
+ function boundsSatisfyReadGate(bounds, gate) {
1221
+ if (!gate.boundField) return true;
1222
+ const actual = bounds?.[gate.boundField];
1223
+ return actual !== void 0 && String(actual) === gate.requiredValue;
1224
+ }
1225
+ function readToolIsGoverned(g) {
1226
+ if (!g) return false;
1227
+ return Boolean(g.boundField) || g.read != null || g.readGovernance === "none";
1228
+ }
1229
+ function allowedResources(contexts, boundField) {
1230
+ const out = /* @__PURE__ */ new Set();
1231
+ for (const ctx of contexts) {
1232
+ for (const v of tokenSet(ctx?.[boundField])) out.add(v);
1233
+ }
1234
+ return out;
1235
+ }
1236
+ function deniedResources(requested, allowed) {
1237
+ return requested.filter((r) => !allowed.has(r));
1238
+ }
1239
+ function filterItemsByResource(items, idPath, allowed) {
1240
+ return items.filter((it) => {
1241
+ const id = getByDottedPath(it, idPath);
1242
+ return typeof id === "string" && allowed.has(id);
1243
+ });
1244
+ }
1245
+ function hasBlockedValue(item, valuesPath, blocked) {
1246
+ const v = getByDottedPath(item, valuesPath);
1247
+ const arr = Array.isArray(v) ? v : v != null ? [v] : [];
1248
+ return arr.some((x) => typeof x === "string" && blocked.has(x));
1249
+ }
1250
+ var MS_PER_DAY = 24 * 60 * 60 * 1e3;
1251
+ function parseMessageTimestamp(value) {
1252
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1253
+ if (typeof value === "string" && value.trim() !== "") {
1254
+ const n = Number(value);
1255
+ if (Number.isFinite(n)) return n;
1256
+ const iso = Date.parse(value);
1257
+ if (Number.isFinite(iso)) return iso;
1258
+ }
1259
+ return null;
1260
+ }
1261
+ function ageInDays(timestampMs, nowMs) {
1262
+ return Math.floor((nowMs - timestampMs) / MS_PER_DAY);
1263
+ }
1264
+ function maxReadAgeDays(boundsList, field) {
1265
+ let max = null;
1266
+ for (const b of boundsList) {
1267
+ const raw = b?.[field];
1268
+ const n = typeof raw === "number" ? raw : raw !== void 0 ? Number(raw) : NaN;
1269
+ if (Number.isFinite(n)) max = max === null ? n : Math.max(max, n);
1270
+ }
1271
+ return max;
1272
+ }
1273
+ function isOlderThanMaxAge(timestampMs, maxAgeDays, nowMs) {
1274
+ if (timestampMs === null) return true;
1275
+ return ageInDays(timestampMs, nowMs) > maxAgeDays;
1276
+ }
1277
+ var TRAILING_OPERATOR = /(?:^|\s)(?:OR|AND|NOT)$|[-+|&:]$/i;
1278
+ function parensBalanced(s) {
1279
+ let depth = 0;
1280
+ let inQuote = false;
1281
+ for (const c of s) {
1282
+ if (c === '"') {
1283
+ inQuote = !inQuote;
1284
+ continue;
1285
+ }
1286
+ if (inQuote) continue;
1287
+ if (c === "(") depth++;
1288
+ else if (c === ")" && --depth < 0) return false;
1289
+ }
1290
+ return !inQuote && depth === 0;
1291
+ }
1292
+ function composeReadQuery(agentQuery, clauses) {
1293
+ const injected = clauses.filter((c) => typeof c === "string" && c.trim() !== "").map((c) => c.trim());
1294
+ const base = (agentQuery ?? "").trim();
1295
+ if (base === "") return { ok: true, query: injected.join(" ") };
1296
+ if (!parensBalanced(base)) {
1297
+ return { ok: false, reason: "the requested search query has unbalanced parentheses" };
1298
+ }
1299
+ if (TRAILING_OPERATOR.test(base)) {
1300
+ return { ok: false, reason: "the requested search query ends in a dangling operator" };
1301
+ }
1302
+ if (injected.length === 0) return { ok: true, query: base };
1303
+ return { ok: true, query: `(${base}) ${injected.join(" ")}` };
1304
+ }
1305
+ function detectAgeConflict(agentQuery, conflictPattern, maxAgeDays) {
1306
+ if (!agentQuery || !conflictPattern || maxAgeDays === null) return null;
1307
+ let re;
1308
+ try {
1309
+ re = new RegExp(conflictPattern, "gi");
1310
+ } catch {
1311
+ return null;
1312
+ }
1313
+ let worst = null;
1314
+ for (const match of agentQuery.matchAll(re)) {
1315
+ const days = Number(match[1]);
1316
+ if (!Number.isFinite(days)) continue;
1317
+ if (days >= maxAgeDays && (worst === null || days > worst)) worst = days;
1318
+ }
1319
+ return worst;
1320
+ }
1321
+ function resolveAgeBoundField(boundsSchema, ageExecField) {
1322
+ const fields = boundsSchema?.fields;
1323
+ if (!fields) return null;
1324
+ for (const [name, def] of Object.entries(fields)) {
1325
+ if (def?.boundType?.kind === "per_transaction" && def.boundType.of === ageExecField) return name;
1326
+ }
1327
+ return null;
1328
+ }
1329
+ function getByDottedPath(obj, path) {
1330
+ return path.split(".").reduce(
1331
+ (o, k) => o == null ? void 0 : o[k],
1332
+ obj
1333
+ );
1334
+ }
1335
+
1015
1336
  // src/lib/tool-proxy.ts
1337
+ import { getProfile as getProfile3 } from "@hap/core";
1016
1338
  import { readFile } from "fs/promises";
1017
1339
  import { extname } from "path";
1018
1340
  import { randomUUID } from "crypto";
@@ -1075,6 +1397,30 @@ function applyMapping(m, value, execution) {
1075
1397
  function profileMatches(profileId, shortName) {
1076
1398
  return profileId === shortName || profileId.includes("/" + shortName + "@") || profileId.endsWith("/" + shortName);
1077
1399
  }
1400
+ function parseFirstJson(result) {
1401
+ const text = result.content?.find((c) => c.type === "text")?.text;
1402
+ if (typeof text !== "string") return void 0;
1403
+ try {
1404
+ return JSON.parse(text);
1405
+ } catch {
1406
+ return void 0;
1407
+ }
1408
+ }
1409
+ function denyRead(state2, tool, reason, detail, target) {
1410
+ try {
1411
+ state2.denialLog?.record({
1412
+ ts: Date.now(),
1413
+ tool: tool.originalName,
1414
+ integrationId: tool.integrationId,
1415
+ profile: tool.gating?.profile ?? null,
1416
+ reason,
1417
+ detail,
1418
+ target
1419
+ });
1420
+ } catch {
1421
+ }
1422
+ return { content: [{ type: "text", text: `Read blocked by Gatekeeper: ${detail}` }], isError: true };
1423
+ }
1078
1424
  function createGatedToolHandler(tool, integrationManager2, state2) {
1079
1425
  if (!tool.gating || !tool.gating.profile) {
1080
1426
  return async () => {
@@ -1088,8 +1434,37 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
1088
1434
  };
1089
1435
  }
1090
1436
  const { profile, executionMapping, staticExecution, category } = tool.gating;
1437
+ const lockedGuard = (inner) => async (args) => {
1438
+ if (!state2.spClient.isUnlocked()) {
1439
+ return {
1440
+ content: [{ type: "text", text: lockedNotice(`use ${tool.namespacedName}`) }],
1441
+ isError: true
1442
+ };
1443
+ }
1444
+ return inner(args);
1445
+ };
1446
+ if (category === "disabled") {
1447
+ return async () => ({
1448
+ content: [{
1449
+ type: "text",
1450
+ text: `Tool "${tool.namespacedName}" is disabled by the integration manifest and cannot be used.`
1451
+ }],
1452
+ isError: true
1453
+ });
1454
+ }
1091
1455
  if (category === "read") {
1092
- return async (args) => {
1456
+ const readGate = { boundField: tool.gating.boundField, requiredValue: tool.gating.requiredValue };
1457
+ const readAdapter = tool.gating.read;
1458
+ const governed = readToolIsGoverned(tool.gating);
1459
+ return lockedGuard(async (args) => {
1460
+ if (!governed) {
1461
+ return denyRead(
1462
+ state2,
1463
+ tool,
1464
+ "ungoverned",
1465
+ `${tool.originalName} declares no read governance (no static gate, no read adapter, no explicit exemption), so the gateway cannot bound what it returns. This is a manifest defect \u2014 the tool must declare how its reads are limited before it can be used.`
1466
+ );
1467
+ }
1093
1468
  const auths = state2.getEnrichedAuthorizations();
1094
1469
  const matchingAuths = auths.filter(
1095
1470
  (a) => a.complete && profileMatches(a.profileId, profile)
@@ -1103,10 +1478,135 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
1103
1478
  isError: true
1104
1479
  };
1105
1480
  }
1106
- return integrationManager2.callTool(tool.integrationId, tool.originalName, args);
1107
- };
1481
+ if (readGate.boundField) {
1482
+ const permitted = matchingAuths.some(
1483
+ (a) => boundsSatisfyReadGate(a.bounds ?? a.frame, readGate)
1484
+ );
1485
+ if (!permitted) {
1486
+ return denyRead(
1487
+ state2,
1488
+ tool,
1489
+ "read_gate",
1490
+ `using ${tool.originalName} requires an authorization with "${readGate.boundField}: ${readGate.requiredValue}", but no active authorization grants it.`
1491
+ );
1492
+ }
1493
+ }
1494
+ const readProfile = readAdapter ? getProfile3(matchingAuths[0].profileId) : void 0;
1495
+ const boundsSchema = readProfile?.boundsSchema;
1496
+ const ageBoundField = readAdapter?.ageField ? resolveAgeBoundField(boundsSchema, readAdapter.ageField) : null;
1497
+ let readMaxAge = null;
1498
+ if (ageBoundField) {
1499
+ readMaxAge = maxReadAgeDays(
1500
+ matchingAuths.map((a) => a.bounds ?? a.frame),
1501
+ ageBoundField
1502
+ );
1503
+ if (readMaxAge === null) {
1504
+ return denyRead(
1505
+ state2,
1506
+ tool,
1507
+ "unset_age",
1508
+ `no read-age window is set on your authorization (${ageBoundField}). Reads are not permitted with an unbounded window \u2014 set how far back the agent may read, then try again. Nothing was read.`
1509
+ );
1510
+ }
1511
+ }
1512
+ let outgoing = args;
1513
+ if (readAdapter?.resourceBound && (readAdapter.resourceArg || readAdapter.resourceArrayArg)) {
1514
+ const allowed = allowedResources(
1515
+ matchingAuths.map((a) => a.context ?? {}),
1516
+ readAdapter.resourceBound
1517
+ );
1518
+ const isArray = Boolean(readAdapter.resourceArrayArg);
1519
+ const argName = readAdapter.resourceArrayArg ?? readAdapter.resourceArg;
1520
+ const raw = args[argName];
1521
+ let requested;
1522
+ if (isArray) {
1523
+ requested = Array.isArray(raw) && raw.length > 0 ? raw.map(String) : readAdapter.resourceDefault ? [readAdapter.resourceDefault] : [];
1524
+ } else {
1525
+ requested = typeof raw === "string" && raw.trim() !== "" ? [raw] : readAdapter.resourceDefault ? [readAdapter.resourceDefault] : [];
1526
+ }
1527
+ const denied = deniedResources(requested, allowed);
1528
+ if (requested.length === 0 || denied.length > 0) {
1529
+ const why = requested.length === 0 ? "no container was resolved for this read" : `not in your permitted set (${denied.join(", ")})`;
1530
+ return denyRead(
1531
+ state2,
1532
+ tool,
1533
+ "resource",
1534
+ `${tool.originalName} may only read the containers your authorization permits (${readAdapter.resourceBound}); the requested one is ${why}. Nothing was read.`,
1535
+ denied.length > 0 ? denied.join(", ") : void 0
1536
+ );
1537
+ }
1538
+ outgoing = { ...args, [argName]: isArray ? requested : requested[0] };
1539
+ }
1540
+ if (readAdapter?.pinnedArgs) {
1541
+ outgoing = { ...outgoing, ...readAdapter.pinnedArgs };
1542
+ }
1543
+ if (readAdapter?.queryArg) {
1544
+ const clauses = [];
1545
+ if (ageBoundField && readAdapter.ageConstraint && readMaxAge !== null) {
1546
+ clauses.push(readAdapter.ageConstraint.replace("{days}", String(readMaxAge)));
1547
+ }
1548
+ if (clauses.length > 0) {
1549
+ const base = typeof args[readAdapter.queryArg] === "string" ? args[readAdapter.queryArg] : "";
1550
+ const conflictDays = detectAgeConflict(base, readAdapter.ageConflictPattern, readMaxAge);
1551
+ if (conflictDays !== null) {
1552
+ return denyRead(
1553
+ state2,
1554
+ tool,
1555
+ "age_window",
1556
+ `you asked for items older than ${conflictDays} days, but this authorization allows ${readMaxAge} days. Nothing outside that window can be read, so the search was not run \u2014 it would have returned an empty result that looks like "none exist". Items may well exist beyond ${readMaxAge} days; widen read_max_age_days if you need them.`
1557
+ );
1558
+ }
1559
+ const composed = composeReadQuery(base, clauses);
1560
+ if (!composed.ok) {
1561
+ return denyRead(
1562
+ state2,
1563
+ tool,
1564
+ "query_unsafe",
1565
+ `${composed.reason}. The gateway must AND its read limits onto your search, and this query could not be safely combined. Re-send it without a trailing operator or unbalanced parenthesis.`
1566
+ );
1567
+ }
1568
+ outgoing = { ...outgoing, [readAdapter.queryArg]: composed.query };
1569
+ }
1570
+ }
1571
+ const result = await integrationManager2.callTool(tool.integrationId, tool.originalName, outgoing);
1572
+ if (readAdapter?.blockResultValues && readAdapter.resultValuesPath) {
1573
+ const parsed = parseFirstJson(result);
1574
+ const blocked = new Set(readAdapter.blockResultValues);
1575
+ if (parsed !== void 0 && hasBlockedValue(parsed, readAdapter.resultValuesPath, blocked)) {
1576
+ return denyRead(
1577
+ state2,
1578
+ tool,
1579
+ "spam",
1580
+ `this item is in a container your authorization excludes (e.g. spam/trash). Its contents were not returned.`
1581
+ );
1582
+ }
1583
+ }
1584
+ if (readAdapter && ageBoundField && readAdapter.resultDatePath && readMaxAge !== null) {
1585
+ const parsed = parseFirstJson(result);
1586
+ const rawDate = parsed === void 0 ? void 0 : getByDottedPath(parsed, readAdapter.resultDatePath);
1587
+ if (isOlderThanMaxAge(parseMessageTimestamp(rawDate), readMaxAge, Date.now())) {
1588
+ return denyRead(
1589
+ state2,
1590
+ tool,
1591
+ "age",
1592
+ `this item is older than the ${readMaxAge}-day read window your authorization grants (read_max_age_days). Its contents were not returned.`
1593
+ );
1594
+ }
1595
+ }
1596
+ if (readAdapter?.resourceBound && readAdapter.resultResourcePath && !readAdapter.resourceArg && !readAdapter.resourceArrayArg) {
1597
+ const allowed = allowedResources(
1598
+ matchingAuths.map((a) => a.context ?? {}),
1599
+ readAdapter.resourceBound
1600
+ );
1601
+ const parsed = parseFirstJson(result);
1602
+ const items = Array.isArray(parsed) ? parsed : [];
1603
+ const kept = filterItemsByResource(items, readAdapter.resultResourcePath, allowed);
1604
+ return { ...result, content: [{ type: "text", text: JSON.stringify(kept) }] };
1605
+ }
1606
+ return result;
1607
+ });
1108
1608
  }
1109
- return async (args) => {
1609
+ return lockedGuard(async (args) => {
1110
1610
  const execution = { ...staticExecution };
1111
1611
  for (const [argName, mapping] of Object.entries(executionMapping)) {
1112
1612
  const value = args[argName];
@@ -1137,170 +1637,191 @@ function createGatedToolHandler(tool, integrationManager2, state2) {
1137
1637
  };
1138
1638
  }
1139
1639
  const errors = [];
1140
- for (const auth of matchingAuths) {
1141
- const { result } = await state2.gatekeeper.verifyExecution(auth.authorizationId, execution, {
1142
- bounds: auth.bounds,
1143
- context: auth.context
1640
+ const passers = [];
1641
+ for (const candidate of matchingAuths) {
1642
+ const { result } = await state2.gatekeeper.verifyExecution(candidate.authorizationId, execution, {
1643
+ bounds: candidate.bounds,
1644
+ context: candidate.context
1144
1645
  });
1145
- if (!result.approved) {
1146
- }
1147
1646
  if (result.approved) {
1148
- const authzId = auth.authorizationId;
1149
- if (isCommitmentDowngrade(auth)) {
1647
+ passers.push(candidate);
1648
+ } else {
1649
+ const reasons = result.errors.map((e) => {
1650
+ if (e.code === "BOUND_EXCEEDED") {
1651
+ return `${candidate.path}: ${e.field}: ${e.message}`;
1652
+ }
1653
+ return `${candidate.path}: ${e.message}`;
1654
+ });
1655
+ errors.push(...reasons);
1656
+ }
1657
+ }
1658
+ if (passers.length === 0) {
1659
+ return {
1660
+ content: [{
1661
+ type: "text",
1662
+ text: `Tool call rejected by Gatekeeper. Tried ${matchingAuths.length} authorization(s):
1663
+ ` + errors.map((e) => ` - ${e}`).join("\n")
1664
+ }],
1665
+ isError: true
1666
+ };
1667
+ }
1668
+ const contextKeys = getProfile3(passers[0].profileId)?.contextSchema?.keyOrder ?? [];
1669
+ const selection = selectAuthorization(
1670
+ contextKeys,
1671
+ passers.map((a) => ({
1672
+ id: a.authorizationId,
1673
+ auth: a,
1674
+ context: a.context ?? {},
1675
+ requiresApproval: (a.deferredCommitmentDomains ?? []).length > 0
1676
+ }))
1677
+ );
1678
+ const auth = selection.chosen.auth;
1679
+ if (selection.superseded.length > 0) {
1680
+ console.error(
1681
+ `[Suveren MCP] selection(${tool.namespacedName}): chose ${auth.authorizationId} (${selection.reason}) over [${selection.superseded.map((s) => s.id).join(", ")}]`
1682
+ );
1683
+ }
1684
+ {
1685
+ const authzId = auth.authorizationId;
1686
+ if (isCommitmentDowngrade(auth)) {
1687
+ return {
1688
+ content: [{
1689
+ type: "text",
1690
+ text: `Refusing to execute ${tool.originalName}: the signed authorization requires review (commitment_mode="${auth.signedCommitmentMode}") but the Authority Server returned no pending approvers. This inconsistency (a possible commitment-mode downgrade) is rejected fail-closed. Re-fetch the authorization or contact the Authority Server operator.`
1691
+ }],
1692
+ isError: true
1693
+ };
1694
+ }
1695
+ if ((auth.deferredCommitmentDomains ?? []).length > 0) {
1696
+ try {
1697
+ const enrichedArgs = await attachImagePreview(args);
1698
+ const { proposal } = await state2.spClient.submitProposal({
1699
+ authorizationId: authzId,
1700
+ profileId: auth.profileId,
1701
+ path: auth.path,
1702
+ pendingDomains: auth.deferredCommitmentDomains,
1703
+ tool: tool.namespacedName,
1704
+ toolArgs: enrichedArgs,
1705
+ executionContext: { ...execution }
1706
+ });
1150
1707
  return {
1151
1708
  content: [{
1152
1709
  type: "text",
1153
- text: `Refusing to execute ${tool.originalName}: the signed authorization requires review (commitment_mode="${auth.signedCommitmentMode}") but the Authority Server returned no pending approvers. This inconsistency (a possible commitment-mode downgrade) is rejected fail-closed. Re-fetch the authorization or contact the Authority Server operator.`
1154
- }],
1710
+ text: `Awaiting commitment from domain${auth.deferredCommitmentDomains.length > 1 ? "s" : ""} "${auth.deferredCommitmentDomains.join('", "')}" for tool ${tool.originalName}.
1711
+ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposal_id: "${proposal.id}").`
1712
+ }]
1713
+ };
1714
+ } catch (err) {
1715
+ return {
1716
+ content: [{ type: "text", text: `Failed to submit proposal: ${err instanceof Error ? err.message : String(err)}` }],
1155
1717
  isError: true
1156
1718
  };
1157
1719
  }
1158
- if ((auth.deferredCommitmentDomains ?? []).length > 0) {
1720
+ }
1721
+ let receiptId;
1722
+ try {
1723
+ const actionType = typeof execution.action_type === "string" ? execution.action_type : void 0;
1724
+ if (!actionType) {
1725
+ console.error(
1726
+ `[Suveren MCP] Warning: tool ${tool.namespacedName} has no action_type in staticExecution. Bounds check may be skipped. Fix the integration manifest.`
1727
+ );
1728
+ }
1729
+ const binding = computeContentBinding(auth.profileId, tool, args);
1730
+ const { receipt } = await state2.spClient.postReceipt({
1731
+ authorizationId: authzId,
1732
+ // Optional cross-check — the AS fails closed on a mismatch.
1733
+ boundsHash: auth.boundsHash,
1734
+ profileId: auth.profileId,
1735
+ action: tool.namespacedName,
1736
+ actionType,
1737
+ executionContext: { ...execution },
1738
+ amount: typeof execution.amount === "number" ? execution.amount : void 0,
1739
+ idempotencyKey: randomUUID(),
1740
+ ...binding ?? {}
1741
+ });
1742
+ receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
1743
+ } catch (err) {
1744
+ if (err instanceof SPReceiptError && err.statusCode === 409) {
1745
+ const spBody = err.body;
1746
+ let pendingApprovers = spBody.approvers ?? [];
1747
+ if (pendingApprovers.length === 0) {
1748
+ try {
1749
+ const summary = await state2.spClient.getAuthorizationSummary(authzId);
1750
+ if (summary?.approvers_frozen) {
1751
+ pendingApprovers = summary.approvers_frozen;
1752
+ }
1753
+ if (summary?.created_by) {
1754
+ pendingApprovers = [summary.created_by, ...pendingApprovers];
1755
+ }
1756
+ } catch {
1757
+ }
1758
+ } else {
1759
+ try {
1760
+ const summary = await state2.spClient.getAuthorizationSummary(authzId);
1761
+ if (summary?.created_by) {
1762
+ pendingApprovers = [summary.created_by, ...pendingApprovers];
1763
+ }
1764
+ } catch {
1765
+ }
1766
+ }
1767
+ const uniqueApprovers = [...new Set(pendingApprovers)];
1159
1768
  try {
1160
1769
  const enrichedArgs = await attachImagePreview(args);
1161
1770
  const { proposal } = await state2.spClient.submitProposal({
1162
1771
  authorizationId: authzId,
1163
1772
  profileId: auth.profileId,
1164
1773
  path: auth.path,
1165
- pendingDomains: auth.deferredCommitmentDomains,
1774
+ pendingDomains: [],
1166
1775
  tool: tool.namespacedName,
1167
1776
  toolArgs: enrichedArgs,
1168
- executionContext: { ...execution }
1777
+ executionContext: { ...execution },
1778
+ pendingApprovers: uniqueApprovers
1169
1779
  });
1170
1780
  return {
1171
1781
  content: [{
1172
1782
  type: "text",
1173
- text: `Awaiting commitment from domain${auth.deferredCommitmentDomains.length > 1 ? "s" : ""} "${auth.deferredCommitmentDomains.join('", "')}" for tool ${tool.originalName}.
1174
- Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposal_id: "${proposal.id}").`
1783
+ text: `Action exceeds team cap. Approval required from ${uniqueApprovers.length} reviewer(s).
1784
+ Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
1175
1785
  }]
1176
1786
  };
1177
- } catch (err) {
1787
+ } catch (proposalErr) {
1178
1788
  return {
1179
- content: [{ type: "text", text: `Failed to submit proposal: ${err instanceof Error ? err.message : String(err)}` }],
1789
+ content: [{ type: "text", text: `Failed to submit approval proposal: ${proposalErr instanceof Error ? proposalErr.message : String(proposalErr)}` }],
1180
1790
  isError: true
1181
1791
  };
1182
1792
  }
1183
1793
  }
1184
- let receiptId;
1185
- try {
1186
- const actionType = typeof execution.action_type === "string" ? execution.action_type : void 0;
1187
- if (!actionType) {
1188
- console.error(
1189
- `[Suveren MCP] Warning: tool ${tool.namespacedName} has no action_type in staticExecution. Bounds check may be skipped. Fix the integration manifest.`
1190
- );
1191
- }
1192
- const binding = computeContentBinding(auth.profileId, tool, args);
1193
- const { receipt } = await state2.spClient.postReceipt({
1194
- authorizationId: authzId,
1195
- // Optional cross-check — the AS fails closed on a mismatch.
1196
- boundsHash: auth.boundsHash,
1197
- profileId: auth.profileId,
1198
- action: tool.namespacedName,
1199
- actionType,
1200
- executionContext: { ...execution },
1201
- amount: typeof execution.amount === "number" ? execution.amount : void 0,
1202
- idempotencyKey: randomUUID(),
1203
- ...binding ?? {}
1204
- });
1205
- receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
1206
- } catch (err) {
1207
- if (err instanceof SPReceiptError && err.statusCode === 409) {
1208
- const spBody = err.body;
1209
- let pendingApprovers = spBody.approvers ?? [];
1210
- if (pendingApprovers.length === 0) {
1211
- try {
1212
- const summary = await state2.spClient.getAuthorizationSummary(authzId);
1213
- if (summary?.approvers_frozen) {
1214
- pendingApprovers = summary.approvers_frozen;
1215
- }
1216
- if (summary?.created_by) {
1217
- pendingApprovers = [summary.created_by, ...pendingApprovers];
1218
- }
1219
- } catch {
1220
- }
1221
- } else {
1222
- try {
1223
- const summary = await state2.spClient.getAuthorizationSummary(authzId);
1224
- if (summary?.created_by) {
1225
- pendingApprovers = [summary.created_by, ...pendingApprovers];
1226
- }
1227
- } catch {
1228
- }
1229
- }
1230
- const uniqueApprovers = [...new Set(pendingApprovers)];
1231
- try {
1232
- const enrichedArgs = await attachImagePreview(args);
1233
- const { proposal } = await state2.spClient.submitProposal({
1234
- authorizationId: authzId,
1235
- profileId: auth.profileId,
1236
- path: auth.path,
1237
- pendingDomains: [],
1238
- tool: tool.namespacedName,
1239
- toolArgs: enrichedArgs,
1240
- executionContext: { ...execution },
1241
- pendingApprovers: uniqueApprovers
1242
- });
1243
- return {
1244
- content: [{
1245
- type: "text",
1246
- text: `Action exceeds team cap. Approval required from ${uniqueApprovers.length} reviewer(s).
1247
- Proposal ID: ${proposal.id}. Use check-pending-commitments to track status.`
1248
- }]
1249
- };
1250
- } catch (proposalErr) {
1251
- return {
1252
- content: [{ type: "text", text: `Failed to submit approval proposal: ${proposalErr instanceof Error ? proposalErr.message : String(proposalErr)}` }],
1253
- isError: true
1254
- };
1255
- }
1256
- }
1257
- if (err instanceof SPReceiptError && err.statusCode === 422) {
1258
- return {
1259
- content: [{ type: "text", text: `Action blocked: ${err.message} (hard team ceiling \u2014 contact the team admin)` }],
1260
- isError: true
1261
- };
1262
- }
1263
- if (err instanceof SPReceiptError && err.statusCode === 403) {
1264
- if (/revoked/i.test(err.message)) {
1265
- state2.cache.invalidate(auth.authorizationId);
1266
- }
1267
- return {
1268
- content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
1269
- isError: true
1270
- };
1794
+ if (err instanceof SPReceiptError && err.statusCode === 422) {
1795
+ return {
1796
+ content: [{ type: "text", text: `Action blocked: ${err.message} (hard team ceiling \u2014 contact the team admin)` }],
1797
+ isError: true
1798
+ };
1799
+ }
1800
+ if (err instanceof SPReceiptError && err.statusCode === 403) {
1801
+ if (/revoked/i.test(err.message)) {
1802
+ state2.cache.invalidate(auth.authorizationId);
1271
1803
  }
1272
1804
  return {
1273
- content: [{ type: "text", text: `SP unavailable \u2014 tool call blocked. ${err instanceof Error ? err.message : ""}` }],
1805
+ content: [{ type: "text", text: `Blocked by SP: ${err.message}` }],
1274
1806
  isError: true
1275
1807
  };
1276
1808
  }
1277
- state2.executionLog.record({
1278
- profileId: auth.profileId,
1279
- path: auth.path,
1280
- execution: { ...execution },
1281
- timestamp: Math.floor(Date.now() / 1e3)
1282
- });
1283
- let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
1284
- if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
1285
- return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
1809
+ return {
1810
+ content: [{ type: "text", text: `SP unavailable \u2014 tool call blocked. ${err instanceof Error ? err.message : ""}` }],
1811
+ isError: true
1812
+ };
1286
1813
  }
1287
- const reasons = result.errors.map((e) => {
1288
- if (e.code === "BOUND_EXCEEDED") {
1289
- return `${auth.path}: ${e.field}: ${e.message}`;
1290
- }
1291
- return `${auth.path}: ${e.message}`;
1814
+ state2.executionLog.record({
1815
+ profileId: auth.profileId,
1816
+ path: auth.path,
1817
+ execution: { ...execution },
1818
+ timestamp: Math.floor(Date.now() / 1e3)
1292
1819
  });
1293
- errors.push(...reasons);
1820
+ let outgoingArgs = shouldAttachFooter() && receiptId ? appendVerificationFooter(tool, args, receiptId, auth.subjects?.[0]) : args;
1821
+ if (receiptId) outgoingArgs = attachReceiptId(tool, outgoingArgs, receiptId);
1822
+ return integrationManager2.callTool(tool.integrationId, tool.originalName, outgoingArgs);
1294
1823
  }
1295
- return {
1296
- content: [{
1297
- type: "text",
1298
- text: `Tool call rejected by Gatekeeper. Tried ${matchingAuths.length} authorization(s):
1299
- ` + errors.map((e) => ` - ${e}`).join("\n")
1300
- }],
1301
- isError: true
1302
- };
1303
- };
1824
+ });
1304
1825
  }
1305
1826
  function buildProxiedToolDescription(tool, state2) {
1306
1827
  if (!tool.gating || !tool.gating.profile) {
@@ -1392,6 +1913,9 @@ function buildCapabilityMap(profileId, toolGating, integrationManager2) {
1392
1913
  }
1393
1914
  function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
1394
1915
  return async (args) => {
1916
+ if (!state2.spClient.isUnlocked()) {
1917
+ return { content: [{ type: "text", text: lockedNotice("list authorizations") }] };
1918
+ }
1395
1919
  const authorizations = state2.getEnrichedAuthorizations();
1396
1920
  const now = Math.floor(Date.now() / 1e3);
1397
1921
  const domain = args?.domain;
@@ -1435,7 +1959,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
1435
1959
  } catch {
1436
1960
  }
1437
1961
  const shortName = shortProfileName2(auth.profileId);
1438
- const profile = getProfile3(auth.profileId) ?? getProfile3(shortName);
1962
+ const profile = getProfile4(auth.profileId) ?? getProfile4(shortName);
1439
1963
  if (profile) {
1440
1964
  const consumption = getConsumptionState(auth, state2.executionLog, profile);
1441
1965
  const consumptionText = formatConsumptionFull(consumption);
@@ -1491,7 +2015,7 @@ function listAuthorizationsHandler(state2, integrationManager2, contextDir) {
1491
2015
  lines.push(" Mode: review (each action requires your approval)");
1492
2016
  }
1493
2017
  const shortName = shortProfileName2(auth.profileId);
1494
- const profile = getProfile3(auth.profileId) ?? getProfile3(shortName);
2018
+ const profile = getProfile4(auth.profileId) ?? getProfile4(shortName);
1495
2019
  if (profile) {
1496
2020
  const consumption = getConsumptionState(auth, state2.executionLog, profile);
1497
2021
  const compact = formatConsumptionCompact(consumption);
@@ -1583,6 +2107,9 @@ function shortProfileName3(profileId) {
1583
2107
  }
1584
2108
  function listIntegrationsHandler(state2, integrationManager2) {
1585
2109
  return async () => {
2110
+ if (!state2.spClient.isUnlocked()) {
2111
+ return { content: [{ type: "text", text: lockedNotice("list integrations") }] };
2112
+ }
1586
2113
  if (!integrationManager2) {
1587
2114
  return { content: [{ type: "text", text: "No integration manager available." }] };
1588
2115
  }
@@ -1700,6 +2227,9 @@ Result: ${resultText}` };
1700
2227
  }
1701
2228
  function checkPendingCommitmentsHandler(state2, integrationManager2) {
1702
2229
  return async (args) => {
2230
+ if (!state2.spClient.isUnlocked()) {
2231
+ return { content: [{ type: "text", text: lockedNotice("check commitments") }] };
2232
+ }
1703
2233
  try {
1704
2234
  if (args.proposal_id) {
1705
2235
  const committed2 = await state2.spClient.getCommittedProposals();
@@ -1938,16 +2468,16 @@ function verifyGateContentHashes(content, auth) {
1938
2468
  }
1939
2469
 
1940
2470
  // src/lib/integration-registry.ts
1941
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
1942
- import { homedir as homedir4 } from "os";
1943
- import { dirname as dirname3, join as join4 } from "path";
1944
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join4(homedir4(), ".suveren");
2471
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2472
+ import { homedir as homedir5 } from "os";
2473
+ import { dirname as dirname4, join as join5 } from "path";
2474
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join5(homedir5(), ".suveren");
1945
2475
  var IntegrationRegistry = class {
1946
2476
  integrations = /* @__PURE__ */ new Map();
1947
2477
  filePath;
1948
2478
  constructor(dataDir) {
1949
2479
  const dir = dataDir ?? DEFAULT_DATA_DIR;
1950
- this.filePath = join4(dir, "integrations.json");
2480
+ this.filePath = join5(dir, "integrations.json");
1951
2481
  this.load();
1952
2482
  }
1953
2483
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -1978,12 +2508,12 @@ var IntegrationRegistry = class {
1978
2508
  }
1979
2509
  // ─── Load / Save ────────────────────────────────────────────────────────
1980
2510
  load() {
1981
- if (!existsSync4(this.filePath)) {
1982
- mkdirSync3(dirname3(this.filePath), { recursive: true });
2511
+ if (!existsSync5(this.filePath)) {
2512
+ mkdirSync4(dirname4(this.filePath), { recursive: true });
1983
2513
  return;
1984
2514
  }
1985
2515
  try {
1986
- const raw = readFileSync4(this.filePath, "utf-8");
2516
+ const raw = readFileSync5(this.filePath, "utf-8");
1987
2517
  const data = JSON.parse(raw);
1988
2518
  const migrated = data.integrations.map((i) => {
1989
2519
  if ("toolGating" in i && !("profile" in i)) {
@@ -2012,37 +2542,38 @@ var IntegrationRegistry = class {
2012
2542
  version: 1,
2013
2543
  integrations: Array.from(this.integrations.values())
2014
2544
  };
2015
- mkdirSync3(dirname3(this.filePath), { recursive: true });
2016
- writeFileSync3(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
2545
+ mkdirSync4(dirname4(this.filePath), { recursive: true });
2546
+ writeFileSync4(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
2017
2547
  }
2018
2548
  };
2019
2549
 
2020
2550
  // src/lib/integration-manager.ts
2021
- import { homedir as homedir5 } from "os";
2022
- import { join as join5 } from "path";
2023
- import { existsSync as existsSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "fs";
2024
- import { execSync } from "child_process";
2551
+ import { homedir as homedir6 } from "os";
2552
+ import { join as join6, delimiter } from "path";
2553
+ import { existsSync as existsSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync } from "fs";
2554
+ import { execFile } from "child_process";
2025
2555
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2026
2556
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2027
- import { getProfile as getProfile4 } from "@hap/core";
2028
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join5(homedir5(), ".suveren");
2029
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join5(DEFAULT_DATA_DIR2, "integrations");
2030
- var INTEGRATIONS_BIN = join5(INTEGRATIONS_DIR, "node_modules", ".bin");
2557
+ import { getProfile as getProfile5 } from "@hap/core";
2558
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join6(homedir6(), ".suveren");
2559
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join6(DEFAULT_DATA_DIR2, "integrations");
2560
+ var INTEGRATIONS_BIN = join6(INTEGRATIONS_DIR, "node_modules", ".bin");
2031
2561
  function buildPath() {
2032
2562
  const base = process.env.PATH ?? "";
2033
- return [INTEGRATIONS_BIN, base].join(":");
2563
+ return [INTEGRATIONS_BIN, base].join(delimiter);
2034
2564
  }
2035
2565
  function ensureIntegrationsDir() {
2036
- if (!existsSync5(INTEGRATIONS_DIR)) {
2037
- mkdirSync4(INTEGRATIONS_DIR, { recursive: true });
2566
+ if (!existsSync6(INTEGRATIONS_DIR)) {
2567
+ mkdirSync5(INTEGRATIONS_DIR, { recursive: true });
2038
2568
  }
2039
- const pkgPath = join5(INTEGRATIONS_DIR, "package.json");
2040
- if (!existsSync5(pkgPath)) {
2041
- writeFileSync4(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
2569
+ const pkgPath = join6(INTEGRATIONS_DIR, "package.json");
2570
+ if (!existsSync6(pkgPath)) {
2571
+ writeFileSync5(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
2042
2572
  }
2043
2573
  }
2044
2574
  var RESPAWN_DELAYS = [2e3, 4e3, 6e3];
2045
2575
  var MAX_RESPAWN_ATTEMPTS = 3;
2576
+ var CONNECT_TIMEOUT_MS = 3e4;
2046
2577
  var IntegrationManager = class {
2047
2578
  constructor(serviceCredentials2) {
2048
2579
  this.serviceCredentials = serviceCredentials2;
@@ -2057,25 +2588,141 @@ var IntegrationManager = class {
2057
2588
  this.onToolsChanged = cb;
2058
2589
  }
2059
2590
  /**
2060
- * Install an npm package into the managed integrations directory if not already present.
2061
- * Called automatically before spawning when config.npmPackage is set.
2591
+ * Is `npmPackage` present AND usable?
2592
+ *
2593
+ * The directory existing is not enough: an install that was interrupted
2594
+ * (timeout, Ctrl+C, antivirus lock) leaves the package directory behind
2595
+ * with no package.json and no bin shim. Treating that as "installed" made
2596
+ * every later start fail instantly on a missing binary, with no way to
2597
+ * self-repair — the user saw "Not running" and a Start button that did
2598
+ * nothing, forever. So we verify the manifest parses and its entry points
2599
+ * exist, and treat anything else as not-installed (and reinstallable).
2062
2600
  */
2063
- ensureInstalled(npmPackage) {
2601
+ isUsableInstall(npmPackage) {
2602
+ const pkgDir = join6(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
2603
+ const pkgJson = join6(pkgDir, "package.json");
2604
+ if (!existsSync6(pkgDir) || !existsSync6(pkgJson)) return false;
2605
+ try {
2606
+ const pkg = JSON.parse(readFileSync6(pkgJson, "utf8"));
2607
+ const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
2608
+ const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
2609
+ const binEntries = Object.entries(binMap);
2610
+ for (const [binName, target] of binEntries) {
2611
+ if (!existsSync6(join6(pkgDir, target))) return false;
2612
+ if (!this.binShimExists(binName)) return false;
2613
+ }
2614
+ if (binEntries.length === 0 && pkg.main && !existsSync6(join6(pkgDir, pkg.main))) return false;
2615
+ return true;
2616
+ } catch {
2617
+ return false;
2618
+ }
2619
+ }
2620
+ /**
2621
+ * Does the node_modules/.bin shim for `binName` exist? On Windows npm writes
2622
+ * `<name>`, `<name>.cmd`, and `<name>.ps1`; the .cmd variant is what actually
2623
+ * runs, so any of them present counts.
2624
+ */
2625
+ binShimExists(binName) {
2626
+ if (!binName) return false;
2627
+ const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
2628
+ return candidates.some((c) => existsSync6(join6(INTEGRATIONS_BIN, c)));
2629
+ }
2630
+ /**
2631
+ * Install an npm package into the managed integrations directory if not
2632
+ * already present. Called automatically before spawning when
2633
+ * config.npmPackage is set.
2634
+ *
2635
+ * Asynchronous ON PURPOSE. This used to be execSync, which blocked the MCP
2636
+ * server's event loop for the entire install — the port stayed bound but
2637
+ * nothing was answered, so the control plane's /internal/manifests call
2638
+ * failed and the UI showed "Couldn't load integrations". Measured on clean
2639
+ * CI runners: ~3.6s (Linux), ~5.4s (macOS), ~14s (Windows), and far worse
2640
+ * on machines with real-time antivirus.
2641
+ */
2642
+ async ensureInstalled(npmPackage) {
2064
2643
  ensureIntegrationsDir();
2065
- const binName = npmPackage.split("/").pop()?.replace(/^@/, "") ?? npmPackage;
2066
- const installed = existsSync5(join5(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/")));
2067
- if (installed) return;
2644
+ if (this.isUsableInstall(npmPackage)) return;
2645
+ const pkgDir = join6(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
2646
+ if (existsSync6(pkgDir)) {
2647
+ console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
2648
+ try {
2649
+ rmSync(pkgDir, { recursive: true, force: true });
2650
+ } catch (err) {
2651
+ console.error(`[IntegrationManager] Could not clean ${pkgDir}:`, err);
2652
+ }
2653
+ }
2654
+ if (!/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(npmPackage)) {
2655
+ throw new Error(`Refusing to install unsafe package name: ${JSON.stringify(npmPackage)}`);
2656
+ }
2068
2657
  console.error(`[IntegrationManager] Installing ${npmPackage}...`);
2658
+ const startedAt = Date.now();
2659
+ const isWin = process.platform === "win32";
2660
+ await new Promise((resolve3, reject) => {
2661
+ const child = execFile(
2662
+ "npm",
2663
+ ["install", "--no-fund", "--no-audit", npmPackage],
2664
+ {
2665
+ cwd: INTEGRATIONS_DIR,
2666
+ // Windows: a shell so cmd.exe finds npm.cmd (see EINVAL note above).
2667
+ // POSIX: no shell — npm is a normal executable and keeping shell off
2668
+ // means the validated package name is still passed as a bare argv.
2669
+ shell: isWin,
2670
+ // No timeout: a slow install on a machine with antivirus is not an
2671
+ // error, and killing it midway is what created broken installs in
2672
+ // the first place. Progress is visible in the log.
2673
+ maxBuffer: 10 * 1024 * 1024,
2674
+ windowsHide: true
2675
+ },
2676
+ (err, _stdout, stderr) => {
2677
+ if (err) {
2678
+ reject(new Error(`Failed to install ${npmPackage}: ${err.message}${stderr ? ` \u2014 ${stderr.trim()}` : ""}`));
2679
+ return;
2680
+ }
2681
+ resolve3();
2682
+ }
2683
+ );
2684
+ child.on("error", (err) => reject(new Error(`Failed to run npm for ${npmPackage}: ${err.message}`)));
2685
+ });
2686
+ if (!this.isUsableInstall(npmPackage)) {
2687
+ throw new Error(
2688
+ `Installed ${npmPackage} but it is not usable \u2014 package.json or its bin target is missing. Check ${INTEGRATIONS_DIR} for a partial install.`
2689
+ );
2690
+ }
2691
+ console.error(`[IntegrationManager] Installed ${npmPackage} (${((Date.now() - startedAt) / 1e3).toFixed(1)}s)`);
2692
+ }
2693
+ /**
2694
+ * Connect a client to its transport, but never wait indefinitely. Resolves
2695
+ * on a successful handshake; rejects if the transport errors (child crash /
2696
+ * spawn failure) or the handshake exceeds CONNECT_TIMEOUT_MS. On failure the
2697
+ * transport is closed so the child process can't linger.
2698
+ */
2699
+ async connectWithGuard(client, transport, config) {
2700
+ let timer;
2701
+ let settled = false;
2702
+ const guard = new Promise((_resolve, reject) => {
2703
+ timer = setTimeout(() => {
2704
+ if (settled) return;
2705
+ reject(new Error(
2706
+ `Timed out after ${CONNECT_TIMEOUT_MS}ms connecting to ${config.id} (${config.command} ${config.args.join(" ")}) \u2014 the downstream server did not complete the MCP handshake. It may have crashed on startup.`
2707
+ ));
2708
+ }, CONNECT_TIMEOUT_MS);
2709
+ transport.onerror = (err) => {
2710
+ if (settled) return;
2711
+ reject(new Error(`Transport error while connecting to ${config.id}: ${err.message}`));
2712
+ };
2713
+ });
2069
2714
  try {
2070
- execSync(`npm install --no-fund --no-audit ${npmPackage}`, {
2071
- cwd: INTEGRATIONS_DIR,
2072
- stdio: ["pipe", "pipe", "pipe"],
2073
- timeout: 12e4
2074
- });
2075
- console.error(`[IntegrationManager] Installed ${npmPackage}`);
2715
+ await Promise.race([client.connect(transport), guard]);
2076
2716
  } catch (err) {
2077
- const message = err instanceof Error ? err.message : String(err);
2078
- throw new Error(`Failed to install ${npmPackage}: ${message}`);
2717
+ try {
2718
+ await transport.close();
2719
+ } catch {
2720
+ }
2721
+ throw err;
2722
+ } finally {
2723
+ settled = true;
2724
+ if (timer) clearTimeout(timer);
2725
+ transport.onerror = void 0;
2079
2726
  }
2080
2727
  }
2081
2728
  /**
@@ -2088,12 +2735,14 @@ var IntegrationManager = class {
2088
2735
  await this.stopIntegration(config.id);
2089
2736
  }
2090
2737
  if (config.npmPackage) {
2091
- this.ensureInstalled(config.npmPackage);
2738
+ await this.ensureInstalled(config.npmPackage);
2092
2739
  }
2093
2740
  const env = this.resolveEnvKeys(config);
2741
+ const interpolate = (s) => s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? process.env[name] ?? "");
2742
+ const args = config.args.map(interpolate);
2094
2743
  const transport = new StdioClientTransport({
2095
2744
  command: config.command,
2096
- args: config.args,
2745
+ args,
2097
2746
  env: {
2098
2747
  ...process.env,
2099
2748
  PATH: buildPath(),
@@ -2106,10 +2755,10 @@ var IntegrationManager = class {
2106
2755
  { name: "suveren-gateway", version: "0.1.0" },
2107
2756
  { capabilities: {} }
2108
2757
  );
2109
- await client.connect(transport);
2758
+ await this.connectWithGuard(client, transport, config);
2110
2759
  console.error(`[IntegrationManager] Connected to ${config.id} (${config.command} ${config.args.join(" ")})`);
2111
2760
  const toolsResult = await client.listTools();
2112
- const profileGating = config.toolGating ?? (config.profile ? getProfile4(config.profile)?.toolGating ?? null : null);
2761
+ const profileGating = config.toolGating ?? (config.profile ? getProfile5(config.profile)?.toolGating ?? null : null);
2113
2762
  const tools = (toolsResult.tools ?? []).map((tool) => {
2114
2763
  const gating = this.resolveToolGating(config.profile, profileGating, tool.name);
2115
2764
  return {
@@ -2188,6 +2837,16 @@ var IntegrationManager = class {
2188
2837
  }
2189
2838
  return tools;
2190
2839
  }
2840
+ /**
2841
+ * RESERVED / NOT YET WIRED (see IntegrationConfig.readAgeDays). The local
2842
+ * read-age window (days) for a running integration, for a deferred feature
2843
+ * that moves the read age off the signed grant. The read path does NOT call
2844
+ * this yet — it still enforces the signed `read_max_age_days` bound.
2845
+ */
2846
+ getReadAgeDays(integrationId) {
2847
+ const days = this.running.get(integrationId)?.config.readAgeDays;
2848
+ return typeof days === "number" && Number.isFinite(days) ? days : null;
2849
+ }
2191
2850
  /**
2192
2851
  * Get status info for all known integrations.
2193
2852
  */
@@ -2279,11 +2938,20 @@ var IntegrationManager = class {
2279
2938
  if (!profileId || !profileGating) return null;
2280
2939
  if (profileGating.overrides && toolName in profileGating.overrides) {
2281
2940
  const override = profileGating.overrides[toolName];
2282
- if (override === null || override.category === "read") {
2941
+ const ext = override ?? {};
2942
+ if (ext.category === "disabled") {
2943
+ return { profile: profileId, executionMapping: {}, category: "disabled" };
2944
+ }
2945
+ if (override === null || ext.category === "read") {
2283
2946
  return {
2284
2947
  profile: profileId,
2285
2948
  executionMapping: {},
2286
- category: "read"
2949
+ category: "read",
2950
+ boundField: ext.boundField,
2951
+ requiredValue: ext.requiredValue,
2952
+ read: ext.read,
2953
+ readGovernance: ext.readGovernance,
2954
+ readGovernanceReason: ext.readGovernanceReason
2287
2955
  };
2288
2956
  }
2289
2957
  return {
@@ -2337,28 +3005,28 @@ var IntegrationManager = class {
2337
3005
  };
2338
3006
 
2339
3007
  // src/lib/profile-loader.ts
2340
- import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
2341
- import { join as join6, resolve } from "path";
3008
+ import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
3009
+ import { join as join7, resolve } from "path";
2342
3010
  import { registerProfile } from "@hap/core";
2343
3011
  function loadProfiles(profilesDir) {
2344
- const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join6(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
2345
- const indexPath = join6(dir, "index.json");
2346
- if (!existsSync6(indexPath)) {
3012
+ const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join7(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
3013
+ const indexPath = join7(dir, "index.json");
3014
+ if (!existsSync7(indexPath)) {
2347
3015
  warnNoProfiles(`No index.json at ${indexPath}`);
2348
3016
  return 0;
2349
3017
  }
2350
3018
  let index;
2351
3019
  try {
2352
- index = JSON.parse(readFileSync5(indexPath, "utf-8"));
3020
+ index = JSON.parse(readFileSync7(indexPath, "utf-8"));
2353
3021
  } catch (err) {
2354
3022
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
2355
3023
  return 0;
2356
3024
  }
2357
3025
  let loaded = 0;
2358
3026
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
2359
- const profilePath = join6(dir, relativePath);
3027
+ const profilePath = join7(dir, relativePath);
2360
3028
  try {
2361
- const profile = JSON.parse(readFileSync5(profilePath, "utf-8"));
3029
+ const profile = JSON.parse(readFileSync7(profilePath, "utf-8"));
2362
3030
  registerProfile(profileId, profile);
2363
3031
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
2364
3032
  if (shortName && shortName !== profileId) {
@@ -2391,30 +3059,30 @@ function warnNoProfiles(reason) {
2391
3059
  }
2392
3060
 
2393
3061
  // src/lib/manifest-loader.ts
2394
- import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
2395
- import { join as join7, resolve as resolve2 } from "path";
3062
+ import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
3063
+ import { join as join8, resolve as resolve2 } from "path";
2396
3064
  var manifests = /* @__PURE__ */ new Map();
2397
3065
  function loadManifests(integrationsDir) {
2398
3066
  const dir = resolve2(
2399
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join7(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3067
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join8(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
2400
3068
  );
2401
- const indexPath = join7(dir, "index.json");
2402
- if (!existsSync7(indexPath)) {
3069
+ const indexPath = join8(dir, "index.json");
3070
+ if (!existsSync8(indexPath)) {
2403
3071
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
2404
3072
  return 0;
2405
3073
  }
2406
3074
  let index;
2407
3075
  try {
2408
- index = JSON.parse(readFileSync6(indexPath, "utf-8"));
3076
+ index = JSON.parse(readFileSync8(indexPath, "utf-8"));
2409
3077
  } catch (err) {
2410
3078
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
2411
3079
  return 0;
2412
3080
  }
2413
3081
  let loaded = 0;
2414
3082
  for (const [id, relativePath] of Object.entries(index.integrations)) {
2415
- const manifestPath = join7(dir, relativePath);
3083
+ const manifestPath = join8(dir, relativePath);
2416
3084
  try {
2417
- const manifest = JSON.parse(readFileSync6(manifestPath, "utf-8"));
3085
+ const manifest = JSON.parse(readFileSync8(manifestPath, "utf-8"));
2418
3086
  manifests.set(id, manifest);
2419
3087
  loaded++;
2420
3088
  } catch (err) {
@@ -2492,8 +3160,10 @@ app.post("/internal/configure", internalOnly, (req, res) => {
2492
3160
  state.spClient.setSessionCookie(sessionCookie);
2493
3161
  console.error("[Suveren MCP] Session cookie configured by control-plane");
2494
3162
  if (vaultKeyHex) {
2495
- state.gateStore.setVaultKey(Buffer.from(vaultKeyHex, "hex"));
2496
- console.error("[Suveren MCP] Vault key configured \u2014 gate store encryption active");
3163
+ const key = Buffer.from(vaultKeyHex, "hex");
3164
+ state.gateStore.setVaultKey(key);
3165
+ state.denialLog.setVaultKey(key);
3166
+ console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log encryption active");
2497
3167
  }
2498
3168
  if (apiKey) {
2499
3169
  state.spClient.setApiKey(apiKey);