@suveren/gateway 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/README.md +33 -1
  2. package/bin/suveren-gateway.js +203 -5
  3. package/content/integrations/crm.json +47 -3
  4. package/content/integrations/erp.json +231 -0
  5. package/content/integrations/index.json +3 -1
  6. package/content/integrations/mail.json +147 -0
  7. package/dist/control-plane/index.mjs +688 -179
  8. package/dist/mcp-server/http.mjs +483 -259
  9. package/dist/ui/assets/index-CV9Mbbkd.css +1 -0
  10. package/dist/ui/assets/{index-CIiOCgjJ.js → index-sq564t5H.js} +23 -23
  11. package/dist/ui/index.html +2 -2
  12. package/lib/as-pairing.mjs +77 -0
  13. package/lib/config.mjs +48 -0
  14. package/node_modules/@types/node/README.md +1 -1
  15. package/node_modules/@types/node/package.json +2 -2
  16. package/node_modules/@types/node/zlib.d.ts +6 -0
  17. package/node_modules/follow-redirects/index.js +10 -1
  18. package/node_modules/follow-redirects/package.json +1 -1
  19. package/node_modules/ip-address/README.md +34 -34
  20. package/node_modules/ip-address/dist/ipv4.js +7 -0
  21. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  22. package/node_modules/ip-address/package.json +1 -1
  23. package/node_modules/undici/LICENSE +21 -0
  24. package/node_modules/undici/README.md +741 -0
  25. package/node_modules/undici/docs/docs/api/Agent.md +84 -0
  26. package/node_modules/undici/docs/docs/api/BalancedPool.md +99 -0
  27. package/node_modules/undici/docs/docs/api/CacheStorage.md +30 -0
  28. package/node_modules/undici/docs/docs/api/CacheStore.md +164 -0
  29. package/node_modules/undici/docs/docs/api/Client.md +288 -0
  30. package/node_modules/undici/docs/docs/api/ClientStats.md +27 -0
  31. package/node_modules/undici/docs/docs/api/Connector.md +115 -0
  32. package/node_modules/undici/docs/docs/api/ContentType.md +57 -0
  33. package/node_modules/undici/docs/docs/api/Cookies.md +128 -0
  34. package/node_modules/undici/docs/docs/api/Debug.md +62 -0
  35. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +317 -0
  36. package/node_modules/undici/docs/docs/api/Dispatcher.md +1393 -0
  37. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +159 -0
  38. package/node_modules/undici/docs/docs/api/Errors.md +49 -0
  39. package/node_modules/undici/docs/docs/api/EventSource.md +45 -0
  40. package/node_modules/undici/docs/docs/api/Fetch.md +60 -0
  41. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +139 -0
  42. package/node_modules/undici/docs/docs/api/H2CClient.md +263 -0
  43. package/node_modules/undici/docs/docs/api/MockAgent.md +603 -0
  44. package/node_modules/undici/docs/docs/api/MockCallHistory.md +197 -0
  45. package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +43 -0
  46. package/node_modules/undici/docs/docs/api/MockClient.md +81 -0
  47. package/node_modules/undici/docs/docs/api/MockErrors.md +12 -0
  48. package/node_modules/undici/docs/docs/api/MockPool.md +555 -0
  49. package/node_modules/undici/docs/docs/api/Pool.md +84 -0
  50. package/node_modules/undici/docs/docs/api/PoolStats.md +35 -0
  51. package/node_modules/undici/docs/docs/api/ProxyAgent.md +229 -0
  52. package/node_modules/undici/docs/docs/api/RedirectHandler.md +93 -0
  53. package/node_modules/undici/docs/docs/api/RetryAgent.md +50 -0
  54. package/node_modules/undici/docs/docs/api/RetryHandler.md +118 -0
  55. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +145 -0
  56. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +616 -0
  57. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +275 -0
  58. package/node_modules/undici/docs/docs/api/Util.md +25 -0
  59. package/node_modules/undici/docs/docs/api/WebSocket.md +141 -0
  60. package/node_modules/undici/docs/docs/api/api-lifecycle.md +91 -0
  61. package/node_modules/undici/docs/docs/best-practices/client-certificate.md +64 -0
  62. package/node_modules/undici/docs/docs/best-practices/crawling.md +58 -0
  63. package/node_modules/undici/docs/docs/best-practices/mocking-request.md +190 -0
  64. package/node_modules/undici/docs/docs/best-practices/proxy.md +127 -0
  65. package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +224 -0
  66. package/node_modules/undici/docs/docs/best-practices/writing-tests.md +63 -0
  67. package/node_modules/undici/index-fetch.js +65 -0
  68. package/node_modules/undici/index.d.ts +3 -0
  69. package/node_modules/undici/index.js +234 -0
  70. package/node_modules/undici/lib/api/abort-signal.js +59 -0
  71. package/node_modules/undici/lib/api/api-connect.js +110 -0
  72. package/node_modules/undici/lib/api/api-pipeline.js +252 -0
  73. package/node_modules/undici/lib/api/api-request.js +214 -0
  74. package/node_modules/undici/lib/api/api-stream.js +209 -0
  75. package/node_modules/undici/lib/api/api-upgrade.js +111 -0
  76. package/node_modules/undici/lib/api/index.js +7 -0
  77. package/node_modules/undici/lib/api/readable.js +580 -0
  78. package/node_modules/undici/lib/cache/memory-cache-store.js +279 -0
  79. package/node_modules/undici/lib/cache/sqlite-cache-store.js +467 -0
  80. package/node_modules/undici/lib/core/connect.js +153 -0
  81. package/node_modules/undici/lib/core/constants.js +143 -0
  82. package/node_modules/undici/lib/core/diagnostics.js +227 -0
  83. package/node_modules/undici/lib/core/errors.js +477 -0
  84. package/node_modules/undici/lib/core/request.js +485 -0
  85. package/node_modules/undici/lib/core/socks5-client.js +422 -0
  86. package/node_modules/undici/lib/core/socks5-utils.js +212 -0
  87. package/node_modules/undici/lib/core/symbols.js +75 -0
  88. package/node_modules/undici/lib/core/tree.js +160 -0
  89. package/node_modules/undici/lib/core/util.js +992 -0
  90. package/node_modules/undici/lib/dispatcher/agent.js +158 -0
  91. package/node_modules/undici/lib/dispatcher/balanced-pool.js +221 -0
  92. package/node_modules/undici/lib/dispatcher/client-h1.js +1746 -0
  93. package/node_modules/undici/lib/dispatcher/client-h2.js +1060 -0
  94. package/node_modules/undici/lib/dispatcher/client.js +668 -0
  95. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +184 -0
  96. package/node_modules/undici/lib/dispatcher/dispatcher.js +48 -0
  97. package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +146 -0
  98. package/node_modules/undici/lib/dispatcher/fixed-queue.js +135 -0
  99. package/node_modules/undici/lib/dispatcher/h2c-client.js +51 -0
  100. package/node_modules/undici/lib/dispatcher/pool-base.js +214 -0
  101. package/node_modules/undici/lib/dispatcher/pool.js +118 -0
  102. package/node_modules/undici/lib/dispatcher/proxy-agent.js +319 -0
  103. package/node_modules/undici/lib/dispatcher/retry-agent.js +35 -0
  104. package/node_modules/undici/lib/dispatcher/round-robin-pool.js +137 -0
  105. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +260 -0
  106. package/node_modules/undici/lib/encoding/index.js +33 -0
  107. package/node_modules/undici/lib/global.js +59 -0
  108. package/node_modules/undici/lib/handler/cache-handler.js +776 -0
  109. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +124 -0
  110. package/node_modules/undici/lib/handler/decorator-handler.js +67 -0
  111. package/node_modules/undici/lib/handler/deduplication-handler.js +460 -0
  112. package/node_modules/undici/lib/handler/redirect-handler.js +238 -0
  113. package/node_modules/undici/lib/handler/retry-handler.js +504 -0
  114. package/node_modules/undici/lib/handler/unwrap-handler.js +106 -0
  115. package/node_modules/undici/lib/handler/wrap-handler.js +105 -0
  116. package/node_modules/undici/lib/interceptor/cache.js +621 -0
  117. package/node_modules/undici/lib/interceptor/decompress.js +551 -0
  118. package/node_modules/undici/lib/interceptor/deduplicate.js +117 -0
  119. package/node_modules/undici/lib/interceptor/dns.js +571 -0
  120. package/node_modules/undici/lib/interceptor/dump.js +99 -0
  121. package/node_modules/undici/lib/interceptor/redirect.js +21 -0
  122. package/node_modules/undici/lib/interceptor/response-error.js +95 -0
  123. package/node_modules/undici/lib/interceptor/retry.js +19 -0
  124. package/node_modules/undici/lib/llhttp/.gitkeep +0 -0
  125. package/node_modules/undici/lib/llhttp/constants.d.ts +195 -0
  126. package/node_modules/undici/lib/llhttp/constants.js +531 -0
  127. package/node_modules/undici/lib/llhttp/llhttp-wasm.js +15 -0
  128. package/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +15 -0
  129. package/node_modules/undici/lib/llhttp/utils.d.ts +2 -0
  130. package/node_modules/undici/lib/llhttp/utils.js +12 -0
  131. package/node_modules/undici/lib/mock/mock-agent.js +232 -0
  132. package/node_modules/undici/lib/mock/mock-call-history.js +248 -0
  133. package/node_modules/undici/lib/mock/mock-client.js +68 -0
  134. package/node_modules/undici/lib/mock/mock-errors.js +29 -0
  135. package/node_modules/undici/lib/mock/mock-interceptor.js +209 -0
  136. package/node_modules/undici/lib/mock/mock-pool.js +68 -0
  137. package/node_modules/undici/lib/mock/mock-symbols.js +32 -0
  138. package/node_modules/undici/lib/mock/mock-utils.js +486 -0
  139. package/node_modules/undici/lib/mock/pending-interceptors-formatter.js +43 -0
  140. package/node_modules/undici/lib/mock/snapshot-agent.js +353 -0
  141. package/node_modules/undici/lib/mock/snapshot-recorder.js +588 -0
  142. package/node_modules/undici/lib/mock/snapshot-utils.js +158 -0
  143. package/node_modules/undici/lib/util/cache.js +676 -0
  144. package/node_modules/undici/lib/util/date.js +670 -0
  145. package/node_modules/undici/lib/util/promise.js +28 -0
  146. package/node_modules/undici/lib/util/runtime-features.js +124 -0
  147. package/node_modules/undici/lib/util/stats.js +32 -0
  148. package/node_modules/undici/lib/util/timers.js +425 -0
  149. package/node_modules/undici/lib/web/cache/cache.js +864 -0
  150. package/node_modules/undici/lib/web/cache/cachestorage.js +152 -0
  151. package/node_modules/undici/lib/web/cache/util.js +45 -0
  152. package/node_modules/undici/lib/web/cookies/constants.js +12 -0
  153. package/node_modules/undici/lib/web/cookies/index.js +199 -0
  154. package/node_modules/undici/lib/web/cookies/parse.js +314 -0
  155. package/node_modules/undici/lib/web/cookies/util.js +352 -0
  156. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +494 -0
  157. package/node_modules/undici/lib/web/eventsource/eventsource.js +501 -0
  158. package/node_modules/undici/lib/web/eventsource/util.js +29 -0
  159. package/node_modules/undici/lib/web/fetch/LICENSE +21 -0
  160. package/node_modules/undici/lib/web/fetch/body.js +509 -0
  161. package/node_modules/undici/lib/web/fetch/constants.js +131 -0
  162. package/node_modules/undici/lib/web/fetch/data-url.js +596 -0
  163. package/node_modules/undici/lib/web/fetch/formdata-parser.js +586 -0
  164. package/node_modules/undici/lib/web/fetch/formdata.js +259 -0
  165. package/node_modules/undici/lib/web/fetch/global.js +40 -0
  166. package/node_modules/undici/lib/web/fetch/headers.js +719 -0
  167. package/node_modules/undici/lib/web/fetch/index.js +2413 -0
  168. package/node_modules/undici/lib/web/fetch/request.js +1115 -0
  169. package/node_modules/undici/lib/web/fetch/response.js +641 -0
  170. package/node_modules/undici/lib/web/fetch/util.js +1522 -0
  171. package/node_modules/undici/lib/web/infra/index.js +229 -0
  172. package/node_modules/undici/lib/web/subresource-integrity/Readme.md +9 -0
  173. package/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +307 -0
  174. package/node_modules/undici/lib/web/webidl/index.js +1006 -0
  175. package/node_modules/undici/lib/web/websocket/connection.js +339 -0
  176. package/node_modules/undici/lib/web/websocket/constants.js +126 -0
  177. package/node_modules/undici/lib/web/websocket/events.js +331 -0
  178. package/node_modules/undici/lib/web/websocket/frame.js +133 -0
  179. package/node_modules/undici/lib/web/websocket/permessage-deflate.js +105 -0
  180. package/node_modules/undici/lib/web/websocket/receiver.js +507 -0
  181. package/node_modules/undici/lib/web/websocket/sender.js +109 -0
  182. package/node_modules/undici/lib/web/websocket/stream/websocketerror.js +104 -0
  183. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +496 -0
  184. package/node_modules/undici/lib/web/websocket/util.js +347 -0
  185. package/node_modules/undici/lib/web/websocket/websocket.js +758 -0
  186. package/node_modules/undici/package.json +152 -0
  187. package/node_modules/undici/scripts/strip-comments.js +10 -0
  188. package/node_modules/undici/types/README.md +6 -0
  189. package/node_modules/undici/types/agent.d.ts +32 -0
  190. package/node_modules/undici/types/api.d.ts +43 -0
  191. package/node_modules/undici/types/balanced-pool.d.ts +30 -0
  192. package/node_modules/undici/types/cache-interceptor.d.ts +179 -0
  193. package/node_modules/undici/types/cache.d.ts +36 -0
  194. package/node_modules/undici/types/client-stats.d.ts +15 -0
  195. package/node_modules/undici/types/client.d.ts +139 -0
  196. package/node_modules/undici/types/connector.d.ts +36 -0
  197. package/node_modules/undici/types/content-type.d.ts +21 -0
  198. package/node_modules/undici/types/cookies.d.ts +30 -0
  199. package/node_modules/undici/types/diagnostics-channel.d.ts +74 -0
  200. package/node_modules/undici/types/dispatcher.d.ts +275 -0
  201. package/node_modules/undici/types/env-http-proxy-agent.d.ts +22 -0
  202. package/node_modules/undici/types/errors.d.ts +177 -0
  203. package/node_modules/undici/types/eventsource.d.ts +66 -0
  204. package/node_modules/undici/types/fetch.d.ts +231 -0
  205. package/node_modules/undici/types/formdata.d.ts +114 -0
  206. package/node_modules/undici/types/global-dispatcher.d.ts +9 -0
  207. package/node_modules/undici/types/global-origin.d.ts +7 -0
  208. package/node_modules/undici/types/h2c-client.d.ts +73 -0
  209. package/node_modules/undici/types/handlers.d.ts +14 -0
  210. package/node_modules/undici/types/header.d.ts +160 -0
  211. package/node_modules/undici/types/index.d.ts +91 -0
  212. package/node_modules/undici/types/interceptors.d.ts +82 -0
  213. package/node_modules/undici/types/mock-agent.d.ts +68 -0
  214. package/node_modules/undici/types/mock-call-history.d.ts +111 -0
  215. package/node_modules/undici/types/mock-client.d.ts +27 -0
  216. package/node_modules/undici/types/mock-errors.d.ts +12 -0
  217. package/node_modules/undici/types/mock-interceptor.d.ts +94 -0
  218. package/node_modules/undici/types/mock-pool.d.ts +27 -0
  219. package/node_modules/undici/types/patch.d.ts +29 -0
  220. package/node_modules/undici/types/pool-stats.d.ts +19 -0
  221. package/node_modules/undici/types/pool.d.ts +41 -0
  222. package/node_modules/undici/types/proxy-agent.d.ts +29 -0
  223. package/node_modules/undici/types/readable.d.ts +68 -0
  224. package/node_modules/undici/types/retry-agent.d.ts +8 -0
  225. package/node_modules/undici/types/retry-handler.d.ts +125 -0
  226. package/node_modules/undici/types/round-robin-pool.d.ts +41 -0
  227. package/node_modules/undici/types/snapshot-agent.d.ts +109 -0
  228. package/node_modules/undici/types/socks5-proxy-agent.d.ts +25 -0
  229. package/node_modules/undici/types/util.d.ts +18 -0
  230. package/node_modules/undici/types/utility.d.ts +7 -0
  231. package/node_modules/undici/types/webidl.d.ts +347 -0
  232. package/node_modules/undici/types/websocket.d.ts +188 -0
  233. package/package.json +4 -2
  234. package/profiles/customers/0.8.profile.json +187 -0
  235. package/profiles/email/0.7.profile.json +254 -0
  236. package/profiles/index.json +4 -1
  237. package/profiles/sales/0.2.profile.json +282 -0
  238. package/dist/ui/assets/index-BJqF5rmx.css +0 -1
@@ -112,6 +112,279 @@ function clientVersionHeaders() {
112
112
  };
113
113
  }
114
114
 
115
+ // src/lib/as-pairing.ts
116
+ import { createHash } from "crypto";
117
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
118
+ import { dirname, join as join2 } from "path";
119
+ function pairingPath(dataDir2) {
120
+ return join2(dataDir2, "as-pairing.json");
121
+ }
122
+ function readPairing(dataDir2) {
123
+ const path = pairingPath(dataDir2);
124
+ if (!existsSync2(path)) return null;
125
+ try {
126
+ const data = JSON.parse(readFileSync2(path, "utf-8"));
127
+ if (typeof data.asUrl !== "string" || typeof data.publicKeyHex !== "string") return null;
128
+ return {
129
+ asUrl: data.asUrl,
130
+ publicKeyHex: data.publicKeyHex,
131
+ pairedAt: data.pairedAt ?? "",
132
+ ...typeof data.tlsSpkiPinHex === "string" ? { tlsSpkiPinHex: data.tlsSpkiPinHex } : {}
133
+ };
134
+ } catch {
135
+ return null;
136
+ }
137
+ }
138
+ function fingerprintOf(publicKeyHex) {
139
+ const raw = Buffer.from(publicKeyHex, "hex");
140
+ const digest = createHash("sha256").update(raw).digest("hex").toUpperCase();
141
+ return digest.match(/.{1,4}/g)?.join(":") ?? digest;
142
+ }
143
+
144
+ // src/lib/as-config.ts
145
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
146
+ import { dirname as dirname2, join as join3 } from "path";
147
+ var DEFAULT_AS_URL = "https://www.suveren.ai";
148
+ function configPath(dataDir2) {
149
+ return join3(dataDir2, "config.json");
150
+ }
151
+ function readAsConfigStrict(dataDir2) {
152
+ const path = configPath(dataDir2);
153
+ if (!existsSync3(path)) return {};
154
+ let data;
155
+ try {
156
+ data = JSON.parse(readFileSync3(path, "utf-8"));
157
+ } catch (err) {
158
+ throw new Error(
159
+ `Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
160
+ );
161
+ }
162
+ const out = {};
163
+ if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
164
+ if (typeof data.caFile === "string") out.caFile = data.caFile;
165
+ if (typeof data.pinTls === "boolean") out.pinTls = data.pinTls;
166
+ if (typeof data.pinTlsExpectedFingerprint === "string") out.pinTlsExpectedFingerprint = data.pinTlsExpectedFingerprint;
167
+ return out;
168
+ }
169
+ function readAsConfig(dataDir2) {
170
+ const path = configPath(dataDir2);
171
+ if (!existsSync3(path)) return {};
172
+ try {
173
+ const data = JSON.parse(readFileSync3(path, "utf-8"));
174
+ const out = {};
175
+ if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
176
+ if (typeof data.caFile === "string") out.caFile = data.caFile;
177
+ if (typeof data.pinTls === "boolean") out.pinTls = data.pinTls;
178
+ if (typeof data.pinTlsExpectedFingerprint === "string") out.pinTlsExpectedFingerprint = data.pinTlsExpectedFingerprint;
179
+ return out;
180
+ } catch {
181
+ return {};
182
+ }
183
+ }
184
+ function validateAsUrl(candidate) {
185
+ const trimmed = candidate.trim();
186
+ if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
187
+ let parsed;
188
+ try {
189
+ parsed = new URL(trimmed);
190
+ } catch {
191
+ return { ok: false, error: `"${trimmed}" is not a valid URL.` };
192
+ }
193
+ const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
194
+ if (parsed.protocol !== "https:" && !isLocalHttp) {
195
+ return {
196
+ ok: false,
197
+ error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
198
+ };
199
+ }
200
+ let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
201
+ if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
202
+ normalized = normalized.slice(0, -1);
203
+ } else if (normalized === `${parsed.origin}/`) {
204
+ normalized = parsed.origin;
205
+ }
206
+ return { ok: true, url: normalized };
207
+ }
208
+ function resolveAsUrl(dataDir2, flag) {
209
+ if (flag) {
210
+ const v = validateAsUrl(flag);
211
+ if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
212
+ return v.url;
213
+ }
214
+ const envUrl = process.env.SUVEREN_AS_URL;
215
+ if (envUrl) {
216
+ const v = validateAsUrl(envUrl);
217
+ if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
218
+ return v.url;
219
+ }
220
+ const saved = readAsConfigStrict(dataDir2).asUrl;
221
+ if (saved) {
222
+ const v = validateAsUrl(saved);
223
+ if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir2}/config.json: ${v.error}`);
224
+ return v.url;
225
+ }
226
+ return DEFAULT_AS_URL;
227
+ }
228
+ function resolvePinTls(dataDir2) {
229
+ return readAsConfig(dataDir2).pinTls === true;
230
+ }
231
+ function validatePinTlsForUrl(pinTls, asUrl) {
232
+ if (!pinTls) return;
233
+ if (!asUrl.startsWith("https:")) {
234
+ throw new Error(
235
+ `config.json sets "pinTls": true, but the Authority Server URL (${asUrl}) is not https:// \u2014 there is no TLS certificate to pin. Fix the saved as-url, or turn pin-tls off: \`suveren-gateway config set pin-tls off\`.`
236
+ );
237
+ }
238
+ }
239
+
240
+ // src/lib/as-tls-pin.ts
241
+ import { createHash as createHash2 } from "crypto";
242
+ import { checkServerIdentity as defaultCheckServerIdentity, rootCertificates } from "tls";
243
+ import { Agent as HttpsAgent } from "https";
244
+ import { readFileSync as readFileSync4 } from "fs";
245
+ import { Agent, fetch as undiciFetch } from "undici";
246
+ function effectiveCa() {
247
+ const path = process.env.NODE_EXTRA_CA_CERTS;
248
+ if (!path) return void 0;
249
+ try {
250
+ return [...rootCertificates, readFileSync4(path, "utf-8")];
251
+ } catch {
252
+ return void 0;
253
+ }
254
+ }
255
+ var PIN_CHECK_ERROR_CODE = "EAS_TLS_PIN_CHECK";
256
+ function taggedCheckError(err) {
257
+ err.code = PIN_CHECK_ERROR_CODE;
258
+ return err;
259
+ }
260
+ function isPinCheckError(err) {
261
+ let cur = err;
262
+ for (let i = 0; i < 5 && cur; i++) {
263
+ if (cur instanceof Error && cur.code === PIN_CHECK_ERROR_CODE) return true;
264
+ cur = cur instanceof Error ? cur.cause : void 0;
265
+ }
266
+ return false;
267
+ }
268
+ var AsTlsMismatchError = class extends Error {
269
+ constructor(message) {
270
+ super(message);
271
+ this.name = "AsTlsMismatchError";
272
+ }
273
+ };
274
+ function spkiSha256Hex(cert) {
275
+ if (!cert.pubkey) {
276
+ throw new Error("peer certificate carries no pubkey (expected the detailed certificate object)");
277
+ }
278
+ return createHash2("sha256").update(cert.pubkey).digest("hex");
279
+ }
280
+ function formatPinFingerprint(hex) {
281
+ const upper = hex.toUpperCase();
282
+ return upper.match(/.{1,4}/g)?.join(":") ?? upper;
283
+ }
284
+ function pinnedCheckServerIdentity(pinnedSpkiHex) {
285
+ const wantHex = pinnedSpkiHex.toLowerCase();
286
+ return (hostname, cert) => {
287
+ const defaultErr = defaultCheckServerIdentity(hostname, cert);
288
+ if (defaultErr) return taggedCheckError(defaultErr);
289
+ let actualHex;
290
+ try {
291
+ actualHex = spkiSha256Hex(cert);
292
+ } catch (err) {
293
+ return taggedCheckError(new Error(
294
+ `could not read the server certificate's public key to check the TLS pin \u2014 ${err instanceof Error ? err.message : String(err)}`
295
+ ));
296
+ }
297
+ if (actualHex !== wantHex) {
298
+ return taggedCheckError(new Error(
299
+ `TLS certificate pin mismatch for ${hostname}: expected SPKI sha256 ${formatPinFingerprint(wantHex)}, got ${formatPinFingerprint(actualHex)}.`
300
+ ));
301
+ }
302
+ return void 0;
303
+ };
304
+ }
305
+ function buildPinnedDispatcher(pinnedSpkiHex) {
306
+ return new Agent({
307
+ connect: {
308
+ ca: effectiveCa(),
309
+ checkServerIdentity: pinnedCheckServerIdentity(pinnedSpkiHex)
310
+ }
311
+ });
312
+ }
313
+ function buildCapturingDispatcher() {
314
+ let captured;
315
+ const dispatcher = new Agent({
316
+ connect: {
317
+ ca: effectiveCa(),
318
+ checkServerIdentity: (hostname, cert) => {
319
+ captured = cert;
320
+ const defaultErr = defaultCheckServerIdentity(hostname, cert);
321
+ return defaultErr ? taggedCheckError(defaultErr) : void 0;
322
+ }
323
+ }
324
+ });
325
+ return { dispatcher, getCapturedCert: () => captured };
326
+ }
327
+ var DEFAULT_AS_FETCH_TIMEOUT_MS = 15e3;
328
+ function withBoundedSignal(init) {
329
+ const timeout = AbortSignal.timeout(DEFAULT_AS_FETCH_TIMEOUT_MS);
330
+ const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
331
+ return { ...init, signal };
332
+ }
333
+ function closeAfter(dispatcher, pending) {
334
+ pending.catch(() => {
335
+ }).finally(() => {
336
+ void dispatcher.close().catch(() => {
337
+ });
338
+ });
339
+ }
340
+ async function fetchAs(url, init, pinning) {
341
+ if (!pinning.enforce && !pinning.capture) {
342
+ const res2 = await fetch(url, withBoundedSignal(init));
343
+ return { res: res2 };
344
+ }
345
+ if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
346
+ throw new AsTlsMismatchError(
347
+ `pin-tls is on but no certificate is pinned yet for ${url} \u2014 sign in again to establish it (re-pairing required).`
348
+ );
349
+ }
350
+ if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
351
+ const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
352
+ const pending2 = undiciFetch(url, { ...withBoundedSignal(init), dispatcher: dispatcher2 });
353
+ closeAfter(dispatcher2, pending2);
354
+ try {
355
+ const res2 = await pending2;
356
+ return { res: res2 };
357
+ } catch (err) {
358
+ if (!isPinCheckError(err)) throw err;
359
+ throw new AsTlsMismatchError(
360
+ `TLS pin check failed for ${url} \u2014 ${describeFetchError(err)}. Refusing the connection; the API key / session cookie were never sent. If the Authority Server's certificate changed intentionally (a new key, not just renewal), an operator must clear the pairing and sign in again to re-pin it.`
361
+ );
362
+ }
363
+ }
364
+ const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
365
+ const pending = undiciFetch(url, { ...withBoundedSignal(init), dispatcher });
366
+ closeAfter(dispatcher, pending);
367
+ let res;
368
+ try {
369
+ res = await pending;
370
+ } catch (err) {
371
+ if (!isPinCheckError(err)) throw err;
372
+ throw new AsTlsMismatchError(
373
+ `could not verify the Authority Server at ${url} while establishing its TLS pin \u2014 ${describeFetchError(err)}.`
374
+ );
375
+ }
376
+ const cert = getCapturedCert();
377
+ return { res, capturedSpkiHex: cert ? spkiSha256Hex(cert) : void 0 };
378
+ }
379
+ function describeFetchError(err) {
380
+ if (err instanceof Error) {
381
+ const cause = err.cause;
382
+ if (cause instanceof Error) return cause.message;
383
+ return err.message;
384
+ }
385
+ return String(err);
386
+ }
387
+
115
388
  // src/lib/sp-client.ts
116
389
  var SPReceiptError = class extends Error {
117
390
  constructor(message, statusCode, body) {
@@ -127,8 +400,9 @@ var DEFAULT_RECEIPT_RETRY = {
127
400
  };
128
401
  var sleep = (ms) => ms > 0 ? new Promise((resolve3) => setTimeout(resolve3, ms)) : Promise.resolve();
129
402
  var SPClient = class {
130
- constructor(baseUrl, receiptRetry = {}) {
403
+ constructor(baseUrl, receiptRetry = {}, dataDir2) {
131
404
  this.baseUrl = baseUrl;
405
+ this.dataDir = dataDir2;
132
406
  this.receiptRetry = { ...DEFAULT_RECEIPT_RETRY, ...receiptRetry };
133
407
  }
134
408
  sessionCookie = "";
@@ -141,6 +415,22 @@ var SPClient = class {
141
415
  * was active tells the control plane. Concurrent in-flight calls that were
142
416
  * all sent under the same now-dead cookie would otherwise all report it. */
143
417
  sessionExpiredNotified = false;
418
+ /** Same single-flight shape, for a TLS pin mismatch (as-tls-pin.ts). */
419
+ tlsMismatchNotified = false;
420
+ /** Opt-in TLS pinning (as-tls-pin.ts) for every call THIS client makes.
421
+ * Re-read live on each call rather than cached — same reasoning as
422
+ * AttestationCache.getPublicKey's pin check: a pin or setting change
423
+ * must take effect on the very next call, not the next restart. Never
424
+ * allowed to CAPTURE a new pin here — only the control plane's sign-in
425
+ * exchange (as-challenge.ts) does that; this client only enforces. */
426
+ tlsPinning() {
427
+ if (!this.dataDir) return { enforce: false };
428
+ const pin = readPairing(this.dataDir);
429
+ return {
430
+ enforce: resolvePinTls(this.dataDir),
431
+ pinnedSpkiHex: pin && pin.asUrl === this.baseUrl ? pin.tlsSpkiPinHex : void 0
432
+ };
433
+ }
144
434
  /** The AS base URL this client talks to (for provenance records). */
145
435
  get url() {
146
436
  return this.baseUrl;
@@ -152,6 +442,7 @@ var SPClient = class {
152
442
  this.sessionCookie = cookie;
153
443
  this.lockReason = null;
154
444
  this.sessionExpiredNotified = false;
445
+ this.tlsMismatchNotified = false;
155
446
  }
156
447
  /**
157
448
  * Drop the session because the control plane learned (by whatever path —
@@ -205,10 +496,21 @@ var SPClient = class {
205
496
  headers["X-API-Key"] = this.apiKey;
206
497
  }
207
498
  const hadSession = this.sessionCookie !== "";
208
- const res = await globalThis.fetch(url, {
209
- ...init,
210
- headers
211
- });
499
+ let res;
500
+ try {
501
+ const result = await fetchAs(url, { ...init, headers }, this.tlsPinning());
502
+ res = result.res;
503
+ } catch (err) {
504
+ if (err instanceof AsTlsMismatchError) {
505
+ this.sessionCookie = "";
506
+ this.lockReason = "as-tls-mismatch";
507
+ if (!this.tlsMismatchNotified) {
508
+ this.tlsMismatchNotified = true;
509
+ void notifyControlPlane("as-tls-mismatch");
510
+ }
511
+ }
512
+ throw err;
513
+ }
212
514
  if (res.status === 401 && hadSession) {
213
515
  this.sessionCookie = "";
214
516
  this.lockReason = "expired";
@@ -371,32 +673,6 @@ var SPClient = class {
371
673
 
372
674
  // src/lib/attestation-cache.ts
373
675
  import { decodeAttestationBlob } from "@hap/core";
374
-
375
- // src/lib/as-pairing.ts
376
- import { createHash } from "crypto";
377
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
378
- import { dirname, join as join2 } from "path";
379
- function pairingPath(dataDir2) {
380
- return join2(dataDir2, "as-pairing.json");
381
- }
382
- function readPairing(dataDir2) {
383
- const path = pairingPath(dataDir2);
384
- if (!existsSync2(path)) return null;
385
- try {
386
- const data = JSON.parse(readFileSync2(path, "utf-8"));
387
- if (typeof data.asUrl !== "string" || typeof data.publicKeyHex !== "string") return null;
388
- return { asUrl: data.asUrl, publicKeyHex: data.publicKeyHex, pairedAt: data.pairedAt ?? "" };
389
- } catch {
390
- return null;
391
- }
392
- }
393
- function fingerprintOf(publicKeyHex) {
394
- const raw = Buffer.from(publicKeyHex, "hex");
395
- const digest = createHash("sha256").update(raw).digest("hex").toUpperCase();
396
- return digest.match(/.{1,4}/g)?.join(":") ?? digest;
397
- }
398
-
399
- // src/lib/attestation-cache.ts
400
676
  var AsKeyMismatchError = class extends Error {
401
677
  constructor(message) {
402
678
  super(message);
@@ -558,18 +834,18 @@ var AttestationCache = class {
558
834
  };
559
835
 
560
836
  // src/lib/gate-store.ts
561
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
837
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "fs";
562
838
  import { homedir } from "os";
563
- import { dirname as dirname2, join as join3 } from "path";
839
+ import { dirname as dirname3, join as join4 } from "path";
564
840
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
565
- var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join3(homedir(), ".suveren");
841
+ var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join4(homedir(), ".suveren");
566
842
  var GateStore = class {
567
843
  entries = /* @__PURE__ */ new Map();
568
844
  baseDir;
569
845
  vaultKey = null;
570
846
  constructor(filePath) {
571
847
  if (filePath && filePath.endsWith(".json")) {
572
- this.baseDir = dirname2(filePath);
848
+ this.baseDir = dirname3(filePath);
573
849
  } else {
574
850
  this.baseDir = filePath ?? DEFAULT_DIR;
575
851
  }
@@ -580,11 +856,11 @@ var GateStore = class {
580
856
  */
581
857
  setVaultKey(key) {
582
858
  this.vaultKey = key;
583
- if (existsSync3(this.encryptedFilePath)) {
859
+ if (existsSync4(this.encryptedFilePath)) {
584
860
  this.loadEncrypted();
585
861
  } else if (this.entries.size > 0) {
586
862
  this.persistEncrypted();
587
- if (existsSync3(this.plaintextFilePath)) {
863
+ if (existsSync4(this.plaintextFilePath)) {
588
864
  try {
589
865
  unlinkSync2(this.plaintextFilePath);
590
866
  } catch {
@@ -622,11 +898,11 @@ var GateStore = class {
622
898
  clearAll() {
623
899
  this.entries = /* @__PURE__ */ new Map();
624
900
  try {
625
- if (existsSync3(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
901
+ if (existsSync4(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
626
902
  } catch {
627
903
  }
628
904
  try {
629
- if (existsSync3(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
905
+ if (existsSync4(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
630
906
  } catch {
631
907
  }
632
908
  }
@@ -659,19 +935,19 @@ var GateStore = class {
659
935
  }
660
936
  // ─── File paths ─────────────────────────────────────────────────────────
661
937
  get plaintextFilePath() {
662
- return join3(this.baseDir, "gates.json");
938
+ return join4(this.baseDir, "gates.json");
663
939
  }
664
940
  get encryptedFilePath() {
665
- return join3(this.baseDir, "gates.enc.json");
941
+ return join4(this.baseDir, "gates.enc.json");
666
942
  }
667
943
  // ─── Load / Persist ─────────────────────────────────────────────────────
668
944
  loadPlaintext() {
669
- if (!existsSync3(this.plaintextFilePath)) {
670
- mkdirSync2(this.baseDir, { recursive: true });
945
+ if (!existsSync4(this.plaintextFilePath)) {
946
+ mkdirSync3(this.baseDir, { recursive: true });
671
947
  return;
672
948
  }
673
949
  try {
674
- const raw = readFileSync3(this.plaintextFilePath, "utf-8");
950
+ const raw = readFileSync5(this.plaintextFilePath, "utf-8");
675
951
  const data = JSON.parse(raw);
676
952
  this.entries = new Map(Object.entries(data.entries));
677
953
  } catch {
@@ -680,9 +956,9 @@ var GateStore = class {
680
956
  }
681
957
  }
682
958
  loadEncrypted() {
683
- if (!existsSync3(this.encryptedFilePath)) return;
959
+ if (!existsSync4(this.encryptedFilePath)) return;
684
960
  try {
685
- const raw = readFileSync3(this.encryptedFilePath, "utf-8");
961
+ const raw = readFileSync5(this.encryptedFilePath, "utf-8");
686
962
  const data = JSON.parse(raw);
687
963
  this.entries = /* @__PURE__ */ new Map();
688
964
  for (const [key, blob] of Object.entries(data.entries)) {
@@ -706,8 +982,8 @@ var GateStore = class {
706
982
  version: 1,
707
983
  entries: Object.fromEntries(this.entries)
708
984
  };
709
- mkdirSync2(this.baseDir, { recursive: true, mode: 448 });
710
- writeFileSync2(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
985
+ mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
986
+ writeFileSync3(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
711
987
  }
712
988
  persistEncrypted() {
713
989
  const data = {
@@ -717,17 +993,17 @@ var GateStore = class {
717
993
  for (const [key, entry] of this.entries) {
718
994
  data.entries[key] = this.encrypt(JSON.stringify(entry));
719
995
  }
720
- mkdirSync2(this.baseDir, { recursive: true, mode: 448 });
721
- writeFileSync2(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
996
+ mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
997
+ writeFileSync3(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
722
998
  }
723
999
  };
724
1000
 
725
1001
  // src/lib/execution-log.ts
726
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync3 } from "fs";
1002
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync5, unlinkSync as unlinkSync3 } from "fs";
727
1003
  import { homedir as homedir2 } from "os";
728
- import { dirname as dirname3, join as join4 } from "path";
1004
+ import { dirname as dirname4, join as join5 } from "path";
729
1005
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2 } from "crypto";
730
- var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join4(homedir2(), ".suveren");
1006
+ var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join5(homedir2(), ".suveren");
731
1007
  var MAX_AGE_SECONDS = 31 * 24 * 60 * 60;
732
1008
  var ExecutionLog = class {
733
1009
  entries = [];
@@ -735,7 +1011,7 @@ var ExecutionLog = class {
735
1011
  vaultKey = null;
736
1012
  constructor(filePath) {
737
1013
  if (filePath && filePath.endsWith(".json")) {
738
- this.baseDir = dirname3(filePath);
1014
+ this.baseDir = dirname4(filePath);
739
1015
  } else {
740
1016
  this.baseDir = filePath ?? DEFAULT_DIR2;
741
1017
  }
@@ -746,11 +1022,11 @@ var ExecutionLog = class {
746
1022
  */
747
1023
  setVaultKey(key) {
748
1024
  this.vaultKey = key;
749
- if (existsSync4(this.encryptedFilePath)) {
1025
+ if (existsSync5(this.encryptedFilePath)) {
750
1026
  this.loadEncrypted();
751
1027
  } else if (this.entries.length > 0) {
752
1028
  this.persistEncrypted();
753
- if (existsSync4(this.plaintextFilePath)) {
1029
+ if (existsSync5(this.plaintextFilePath)) {
754
1030
  try {
755
1031
  unlinkSync3(this.plaintextFilePath);
756
1032
  } catch {
@@ -854,19 +1130,19 @@ var ExecutionLog = class {
854
1130
  }
855
1131
  // ─── File paths ─────────────────────────────────────────────────────────
856
1132
  get plaintextFilePath() {
857
- return join4(this.baseDir, "execution-log.json");
1133
+ return join5(this.baseDir, "execution-log.json");
858
1134
  }
859
1135
  get encryptedFilePath() {
860
- return join4(this.baseDir, "execution-log.enc.json");
1136
+ return join5(this.baseDir, "execution-log.enc.json");
861
1137
  }
862
1138
  // ─── Load / Persist ─────────────────────────────────────────────────────
863
1139
  loadPlaintext() {
864
- if (!existsSync4(this.plaintextFilePath)) {
865
- mkdirSync3(this.baseDir, { recursive: true });
1140
+ if (!existsSync5(this.plaintextFilePath)) {
1141
+ mkdirSync4(this.baseDir, { recursive: true });
866
1142
  return;
867
1143
  }
868
1144
  try {
869
- const raw = readFileSync4(this.plaintextFilePath, "utf-8");
1145
+ const raw = readFileSync6(this.plaintextFilePath, "utf-8");
870
1146
  const data = JSON.parse(raw);
871
1147
  this.entries = data.entries;
872
1148
  this.prune();
@@ -876,9 +1152,9 @@ var ExecutionLog = class {
876
1152
  }
877
1153
  }
878
1154
  loadEncrypted() {
879
- if (!existsSync4(this.encryptedFilePath)) return;
1155
+ if (!existsSync5(this.encryptedFilePath)) return;
880
1156
  try {
881
- const raw = readFileSync4(this.encryptedFilePath, "utf-8");
1157
+ const raw = readFileSync6(this.encryptedFilePath, "utf-8");
882
1158
  const data = JSON.parse(raw);
883
1159
  const decrypted = this.decrypt(data.blob);
884
1160
  const logFile = JSON.parse(decrypted);
@@ -898,8 +1174,8 @@ var ExecutionLog = class {
898
1174
  }
899
1175
  persistPlaintext() {
900
1176
  const data = { version: 1, entries: this.entries };
901
- mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
902
- writeFileSync3(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1177
+ mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
1178
+ writeFileSync4(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
903
1179
  }
904
1180
  persistEncrypted() {
905
1181
  const logFile = { version: 1, entries: this.entries };
@@ -907,8 +1183,8 @@ var ExecutionLog = class {
907
1183
  version: 1,
908
1184
  blob: this.encrypt(JSON.stringify(logFile))
909
1185
  };
910
- mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
911
- writeFileSync3(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1186
+ mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
1187
+ writeFileSync4(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
912
1188
  }
913
1189
  };
914
1190
  function windowCutoff(window, now) {
@@ -925,11 +1201,11 @@ function windowCutoff(window, now) {
925
1201
  }
926
1202
 
927
1203
  // src/lib/denial-log.ts
928
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, unlinkSync as unlinkSync4 } from "fs";
1204
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync4 } from "fs";
929
1205
  import { homedir as homedir3 } from "os";
930
- import { dirname as dirname4, join as join5 } from "path";
1206
+ import { dirname as dirname5, join as join6 } from "path";
931
1207
  import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes3 } from "crypto";
932
- var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join5(homedir3(), ".suveren");
1208
+ var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join6(homedir3(), ".suveren");
933
1209
  var MAX_RECORDS = 200;
934
1210
  var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
935
1211
  var DenialLog = class {
@@ -937,16 +1213,16 @@ var DenialLog = class {
937
1213
  baseDir;
938
1214
  vaultKey = null;
939
1215
  constructor(filePath) {
940
- this.baseDir = filePath && filePath.endsWith(".json") ? dirname4(filePath) : filePath ?? DEFAULT_DIR3;
1216
+ this.baseDir = filePath && filePath.endsWith(".json") ? dirname5(filePath) : filePath ?? DEFAULT_DIR3;
941
1217
  this.loadPlaintext();
942
1218
  }
943
1219
  setVaultKey(key) {
944
1220
  this.vaultKey = key;
945
- if (existsSync5(this.encryptedFilePath)) {
1221
+ if (existsSync6(this.encryptedFilePath)) {
946
1222
  this.loadEncrypted();
947
1223
  } else if (this.records.length > 0) {
948
1224
  this.persistEncrypted();
949
- if (existsSync5(this.plaintextFilePath)) {
1225
+ if (existsSync6(this.plaintextFilePath)) {
950
1226
  try {
951
1227
  unlinkSync4(this.plaintextFilePath);
952
1228
  } catch {
@@ -996,18 +1272,18 @@ var DenialLog = class {
996
1272
  return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
997
1273
  }
998
1274
  get plaintextFilePath() {
999
- return join5(this.baseDir, "denials.json");
1275
+ return join6(this.baseDir, "denials.json");
1000
1276
  }
1001
1277
  get encryptedFilePath() {
1002
- return join5(this.baseDir, "denials.enc.json");
1278
+ return join6(this.baseDir, "denials.enc.json");
1003
1279
  }
1004
1280
  loadPlaintext() {
1005
- if (!existsSync5(this.plaintextFilePath)) {
1006
- mkdirSync4(this.baseDir, { recursive: true });
1281
+ if (!existsSync6(this.plaintextFilePath)) {
1282
+ mkdirSync5(this.baseDir, { recursive: true });
1007
1283
  return;
1008
1284
  }
1009
1285
  try {
1010
- const data = JSON.parse(readFileSync5(this.plaintextFilePath, "utf-8"));
1286
+ const data = JSON.parse(readFileSync7(this.plaintextFilePath, "utf-8"));
1011
1287
  this.records = data.records ?? [];
1012
1288
  } catch {
1013
1289
  console.error(`[DenialLog] Could not parse ${this.plaintextFilePath}, starting fresh`);
@@ -1015,9 +1291,9 @@ var DenialLog = class {
1015
1291
  }
1016
1292
  }
1017
1293
  loadEncrypted() {
1018
- if (!existsSync5(this.encryptedFilePath)) return;
1294
+ if (!existsSync6(this.encryptedFilePath)) return;
1019
1295
  try {
1020
- const data = JSON.parse(readFileSync5(this.encryptedFilePath, "utf-8"));
1296
+ const data = JSON.parse(readFileSync7(this.encryptedFilePath, "utf-8"));
1021
1297
  const logFile = JSON.parse(this.decrypt(data.blob));
1022
1298
  this.records = logFile.records ?? [];
1023
1299
  } catch (err) {
@@ -1031,23 +1307,23 @@ var DenialLog = class {
1031
1307
  }
1032
1308
  persistPlaintext() {
1033
1309
  const data = { version: 1, records: this.records };
1034
- mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
1035
- writeFileSync4(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1310
+ mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
1311
+ writeFileSync5(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1036
1312
  }
1037
1313
  persistEncrypted() {
1038
1314
  const logFile = { version: 1, records: this.records };
1039
1315
  const data = { version: 1, blob: this.encrypt(JSON.stringify(logFile)) };
1040
- mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
1041
- writeFileSync4(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1316
+ mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
1317
+ writeFileSync5(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1042
1318
  }
1043
1319
  };
1044
1320
 
1045
1321
  // src/lib/receipt-archive.ts
1046
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync5 } from "fs";
1322
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync7, unlinkSync as unlinkSync5 } from "fs";
1047
1323
  import { homedir as homedir4 } from "os";
1048
- import { dirname as dirname5, join as join6 } from "path";
1324
+ import { dirname as dirname6, join as join7 } from "path";
1049
1325
  import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes4 } from "crypto";
1050
- var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join6(homedir4(), ".suveren");
1326
+ var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
1051
1327
  function stripPreviews(proposal) {
1052
1328
  const args = proposal.toolArgs;
1053
1329
  if (!args || typeof args !== "object") return proposal;
@@ -1061,7 +1337,7 @@ var ReceiptArchive = class {
1061
1337
  vaultKey = null;
1062
1338
  constructor(filePath) {
1063
1339
  if (filePath && filePath.endsWith(".json")) {
1064
- this.baseDir = dirname5(filePath);
1340
+ this.baseDir = dirname6(filePath);
1065
1341
  } else {
1066
1342
  this.baseDir = filePath ?? DEFAULT_DIR4;
1067
1343
  }
@@ -1072,11 +1348,11 @@ var ReceiptArchive = class {
1072
1348
  */
1073
1349
  setVaultKey(key) {
1074
1350
  this.vaultKey = key;
1075
- if (existsSync6(this.encryptedFilePath)) {
1351
+ if (existsSync7(this.encryptedFilePath)) {
1076
1352
  this.loadEncrypted();
1077
1353
  } else if (this.data.receipts.length > 0 || Object.keys(this.data.authorizations).length > 0) {
1078
1354
  this.persistEncrypted();
1079
- if (existsSync6(this.plaintextFilePath)) {
1355
+ if (existsSync7(this.plaintextFilePath)) {
1080
1356
  try {
1081
1357
  unlinkSync5(this.plaintextFilePath);
1082
1358
  } catch {
@@ -1090,7 +1366,7 @@ var ReceiptArchive = class {
1090
1366
  * this state — that would report a hollow success over unreadable evidence.
1091
1367
  */
1092
1368
  isLocked() {
1093
- return this.vaultKey === null && existsSync6(this.encryptedFilePath);
1369
+ return this.vaultKey === null && existsSync7(this.encryptedFilePath);
1094
1370
  }
1095
1371
  /**
1096
1372
  * Archive a signed receipt (and the grant it was issued under).
@@ -1191,19 +1467,19 @@ var ReceiptArchive = class {
1191
1467
  }
1192
1468
  // ─── File paths ─────────────────────────────────────────────────────────
1193
1469
  get plaintextFilePath() {
1194
- return join6(this.baseDir, "receipt-archive.json");
1470
+ return join7(this.baseDir, "receipt-archive.json");
1195
1471
  }
1196
1472
  get encryptedFilePath() {
1197
- return join6(this.baseDir, "receipt-archive.enc.json");
1473
+ return join7(this.baseDir, "receipt-archive.enc.json");
1198
1474
  }
1199
1475
  // ─── Load / Persist ─────────────────────────────────────────────────────
1200
1476
  loadPlaintext() {
1201
- if (!existsSync6(this.plaintextFilePath)) {
1202
- mkdirSync5(this.baseDir, { recursive: true });
1477
+ if (!existsSync7(this.plaintextFilePath)) {
1478
+ mkdirSync6(this.baseDir, { recursive: true });
1203
1479
  return;
1204
1480
  }
1205
1481
  try {
1206
- const raw = readFileSync6(this.plaintextFilePath, "utf-8");
1482
+ const raw = readFileSync8(this.plaintextFilePath, "utf-8");
1207
1483
  const parsed = JSON.parse(raw);
1208
1484
  this.data = {
1209
1485
  version: 1,
@@ -1213,18 +1489,18 @@ var ReceiptArchive = class {
1213
1489
  } catch {
1214
1490
  console.error(`[ReceiptArchive] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
1215
1491
  try {
1216
- writeFileSync5(
1492
+ writeFileSync6(
1217
1493
  `${this.plaintextFilePath}.corrupt-${Date.now()}`,
1218
- readFileSync6(this.plaintextFilePath)
1494
+ readFileSync8(this.plaintextFilePath)
1219
1495
  );
1220
1496
  } catch {
1221
1497
  }
1222
1498
  }
1223
1499
  }
1224
1500
  loadEncrypted() {
1225
- if (!existsSync6(this.encryptedFilePath)) return;
1501
+ if (!existsSync7(this.encryptedFilePath)) return;
1226
1502
  try {
1227
- const raw = readFileSync6(this.encryptedFilePath, "utf-8");
1503
+ const raw = readFileSync8(this.encryptedFilePath, "utf-8");
1228
1504
  const data = JSON.parse(raw);
1229
1505
  const decrypted = this.decrypt(data.blob);
1230
1506
  const parsed = JSON.parse(decrypted);
@@ -1236,9 +1512,9 @@ var ReceiptArchive = class {
1236
1512
  } catch (err) {
1237
1513
  console.error(`[ReceiptArchive] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
1238
1514
  try {
1239
- writeFileSync5(
1515
+ writeFileSync6(
1240
1516
  `${this.encryptedFilePath}.corrupt-${Date.now()}`,
1241
- readFileSync6(this.encryptedFilePath)
1517
+ readFileSync8(this.encryptedFilePath)
1242
1518
  );
1243
1519
  } catch {
1244
1520
  }
@@ -1252,29 +1528,29 @@ var ReceiptArchive = class {
1252
1528
  }
1253
1529
  }
1254
1530
  persistPlaintext() {
1255
- mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
1256
- writeFileSync5(this.plaintextFilePath, JSON.stringify(this.data, null, 2), { encoding: "utf-8", mode: 384 });
1531
+ mkdirSync6(this.baseDir, { recursive: true, mode: 448 });
1532
+ writeFileSync6(this.plaintextFilePath, JSON.stringify(this.data, null, 2), { encoding: "utf-8", mode: 384 });
1257
1533
  }
1258
1534
  persistEncrypted() {
1259
1535
  const data = {
1260
1536
  version: 1,
1261
1537
  blob: this.encrypt(JSON.stringify(this.data))
1262
1538
  };
1263
- mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
1264
- writeFileSync5(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1539
+ mkdirSync6(this.baseDir, { recursive: true, mode: 448 });
1540
+ writeFileSync6(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1265
1541
  }
1266
1542
  };
1267
1543
 
1268
1544
  // src/lib/execution-journal.ts
1269
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, renameSync } from "fs";
1545
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, renameSync } from "fs";
1270
1546
  import { homedir as homedir5 } from "os";
1271
- import { dirname as dirname6, join as join7 } from "path";
1272
- import { createHash as createHash2 } from "crypto";
1273
- var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join7(homedir5(), ".suveren");
1547
+ import { dirname as dirname7, join as join8 } from "path";
1548
+ import { createHash as createHash3 } from "crypto";
1549
+ var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
1274
1550
  var FILE_NAME = "execution-journal.json";
1275
1551
  var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
1276
1552
  function hashToolArgs(args) {
1277
- return "sha256:" + createHash2("sha256").update(stableStringify(args)).digest("hex");
1553
+ return "sha256:" + createHash3("sha256").update(stableStringify(args)).digest("hex");
1278
1554
  }
1279
1555
  function stableStringify(value) {
1280
1556
  if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
@@ -1286,8 +1562,8 @@ function stableStringify(value) {
1286
1562
  var ExecutionJournal = class {
1287
1563
  filePath;
1288
1564
  constructor(filePath) {
1289
- const baseDir = filePath && filePath.endsWith(".json") ? dirname6(filePath) : filePath ?? DEFAULT_DIR5;
1290
- this.filePath = join7(baseDir, FILE_NAME);
1565
+ const baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR5;
1566
+ this.filePath = join8(baseDir, FILE_NAME);
1291
1567
  }
1292
1568
  /** Look a ticket up. Always reads from disk — another process may have written. */
1293
1569
  get(ticketId) {
@@ -1326,9 +1602,9 @@ var ExecutionJournal = class {
1326
1602
  }
1327
1603
  // ─── Persistence ────────────────────────────────────────────────────────
1328
1604
  load() {
1329
- if (!existsSync7(this.filePath)) return { version: 1, entries: [] };
1605
+ if (!existsSync8(this.filePath)) return { version: 1, entries: [] };
1330
1606
  try {
1331
- const parsed = JSON.parse(readFileSync7(this.filePath, "utf8"));
1607
+ const parsed = JSON.parse(readFileSync9(this.filePath, "utf8"));
1332
1608
  if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
1333
1609
  throw new Error("unexpected shape");
1334
1610
  }
@@ -1342,9 +1618,9 @@ var ExecutionJournal = class {
1342
1618
  persist(file) {
1343
1619
  const cutoff = nowSec() - MAX_AGE_SECONDS2;
1344
1620
  file.entries = file.entries.filter((e) => e.startedAt >= cutoff);
1345
- mkdirSync6(dirname6(this.filePath), { recursive: true, mode: 448 });
1621
+ mkdirSync7(dirname7(this.filePath), { recursive: true, mode: 448 });
1346
1622
  const tmp = this.filePath + ".tmp";
1347
- writeFileSync6(tmp, JSON.stringify(file, null, 2), { mode: 384 });
1623
+ writeFileSync7(tmp, JSON.stringify(file, null, 2), { mode: 384 });
1348
1624
  renameSync(tmp, this.filePath);
1349
1625
  }
1350
1626
  };
@@ -1353,10 +1629,10 @@ function nowSec() {
1353
1629
  }
1354
1630
 
1355
1631
  // src/lib/proposal-submission-store.ts
1356
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, renameSync as renameSync2 } from "fs";
1632
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, renameSync as renameSync2 } from "fs";
1357
1633
  import { homedir as homedir6 } from "os";
1358
- import { dirname as dirname7, join as join8 } from "path";
1359
- var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join8(homedir6(), ".suveren");
1634
+ import { dirname as dirname8, join as join9 } from "path";
1635
+ var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
1360
1636
  var FILE_NAME2 = "proposal-submissions.json";
1361
1637
  var MAX_AGE_SECONDS3 = 90 * 24 * 60 * 60;
1362
1638
  function nowSec2() {
@@ -1365,8 +1641,8 @@ function nowSec2() {
1365
1641
  var ProposalSubmissionStore = class {
1366
1642
  filePath;
1367
1643
  constructor(filePath) {
1368
- const baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR6;
1369
- this.filePath = join8(baseDir, FILE_NAME2);
1644
+ const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR6;
1645
+ this.filePath = join9(baseDir, FILE_NAME2);
1370
1646
  }
1371
1647
  /** Record what was submitted, right after a successful submitProposal(). */
1372
1648
  record(entry) {
@@ -1389,9 +1665,9 @@ var ProposalSubmissionStore = class {
1389
1665
  return this.load().entries.find((e) => e.proposalId === proposalId);
1390
1666
  }
1391
1667
  load() {
1392
- if (!existsSync8(this.filePath)) return { version: 1, entries: [] };
1668
+ if (!existsSync9(this.filePath)) return { version: 1, entries: [] };
1393
1669
  try {
1394
- const parsed = JSON.parse(readFileSync8(this.filePath, "utf-8"));
1670
+ const parsed = JSON.parse(readFileSync10(this.filePath, "utf-8"));
1395
1671
  const cutoff = nowSec2() - MAX_AGE_SECONDS3;
1396
1672
  parsed.entries = (parsed.entries ?? []).filter((e) => e.submittedAt >= cutoff);
1397
1673
  return parsed;
@@ -1400,9 +1676,9 @@ var ProposalSubmissionStore = class {
1400
1676
  }
1401
1677
  }
1402
1678
  persist(file) {
1403
- mkdirSync7(dirname7(this.filePath), { recursive: true, mode: 448 });
1679
+ mkdirSync8(dirname8(this.filePath), { recursive: true, mode: 448 });
1404
1680
  const tmpPath = `${this.filePath}.tmp`;
1405
- writeFileSync7(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
1681
+ writeFileSync8(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
1406
1682
  renameSync2(tmpPath, this.filePath);
1407
1683
  }
1408
1684
  };
@@ -1514,12 +1790,13 @@ var SharedState = class {
1514
1790
  gatekeeper;
1515
1791
  /**
1516
1792
  * @param dataDir Passed to the AttestationCache so it can enforce AS key
1517
- * pinning (as-pairing.ts). Optional for backward compat with existing
1518
- * constructions/tests that pass only a URL — those get unpinned
1519
- * trust-on-first-use behavior, same as before pinning existed.
1793
+ * pinning (as-pairing.ts), and to the SPClient so it can enforce opt-in
1794
+ * TLS pinning (as-tls-pin.ts). Optional for backward compat with
1795
+ * existing constructions/tests that pass only a URL — those get
1796
+ * unpinned trust-on-first-use behavior, same as before pinning existed.
1520
1797
  */
1521
1798
  constructor(spUrl2, gateStorePath, dataDir2) {
1522
- this.spClient = new SPClient(spUrl2);
1799
+ this.spClient = new SPClient(spUrl2, void 0, dataDir2);
1523
1800
  this.cache = new AttestationCache(this.spClient, dataDir2);
1524
1801
  this.gateStore = new GateStore(gateStorePath);
1525
1802
  this.executionLog = new ExecutionLog(gateStorePath);
@@ -1679,17 +1956,17 @@ function formatConsumptionFull(entries) {
1679
1956
  }
1680
1957
 
1681
1958
  // src/lib/context-loader.ts
1682
- import { readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
1959
+ import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
1683
1960
  import { homedir as homedir7 } from "os";
1684
- import { join as join9 } from "path";
1685
- var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join9(homedir7(), ".suveren");
1961
+ import { join as join10 } from "path";
1962
+ var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
1686
1963
  var BRIEF_MAX_CHARS = 16 * 1024;
1687
1964
  function readContextFile(dataDir2) {
1688
1965
  const dir = dataDir2 ?? DEFAULT_DIR7;
1689
- const filePath = join9(dir, "context.md");
1690
- if (!existsSync9(filePath)) return null;
1966
+ const filePath = join10(dir, "context.md");
1967
+ if (!existsSync10(filePath)) return null;
1691
1968
  try {
1692
- const content = readFileSync9(filePath, "utf-8").trim();
1969
+ const content = readFileSync11(filePath, "utf-8").trim();
1693
1970
  return content || null;
1694
1971
  } catch {
1695
1972
  return null;
@@ -1809,6 +2086,11 @@ function asKeyMismatchNotice(what) {
1809
2086
 
1810
2087
  TELL THE USER: this needs an operator to check whether the Authority Server's key changed intentionally (e.g. a reinstall) or something else is answering at that address, at ${uiUrl()}. Nothing ran under the unrecognized key.`;
1811
2088
  }
2089
+ function asTlsMismatchNotice(what) {
2090
+ return `${what} Suveren gateway is LOCKED because a connection to the Authority Server presented a TLS certificate that does not match the one pinned when this gateway last signed in (opt-in pin-tls). This is NOT an ended sign-in \u2014 signing in again will be refused until the mismatch is resolved.
2091
+
2092
+ TELL THE USER: this needs an operator to check whether the Authority Server's certificate changed intentionally (e.g. a new TLS key, not just a renewal) or something else is answering at that address, at ${uiUrl()}. Nothing was sent under the unrecognized certificate.`;
2093
+ }
1812
2094
  function asUrlChangedNotice(what) {
1813
2095
  return `${what} Suveren gateway is LOCKED because the Authority Server it points at has changed since it last signed in. Cached mandates from the old server were cleared.
1814
2096
 
@@ -1821,6 +2103,8 @@ function lockedNotice(action, reason = "restart") {
1821
2103
  return expiredSessionNotice(what);
1822
2104
  case "as-key-mismatch":
1823
2105
  return asKeyMismatchNotice(what);
2106
+ case "as-tls-mismatch":
2107
+ return asTlsMismatchNotice(what);
1824
2108
  case "as-url-changed":
1825
2109
  return asUrlChangedNotice(what);
1826
2110
  default:
@@ -1989,6 +2273,11 @@ async function verifyTicket(cache, receipt, expected) {
1989
2273
  `Ticket proposalId "${String(receipt.proposalId)}" does not match the proposal being executed "${expected.proposalId}" \u2014 refusing to execute.`
1990
2274
  );
1991
2275
  }
2276
+ if (expected.idempotencyKey !== void 0 && receipt.idempotencyKey !== expected.idempotencyKey) {
2277
+ throw new TicketBindingMismatchError(
2278
+ `Ticket idempotencyKey "${String(receipt.idempotencyKey)}" does not match the one this call sent "${expected.idempotencyKey}" \u2014 refusing to execute.`
2279
+ );
2280
+ }
1992
2281
  if (expected.contentHash !== void 0) {
1993
2282
  if (receipt.contentHash !== expected.contentHash) {
1994
2283
  throw new TicketBindingMismatchError(
@@ -2766,6 +3055,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
2766
3055
  try {
2767
3056
  const actionType = declaredActionType;
2768
3057
  const binding = computeContentBinding(auth.profileId, tool, args, actionType);
3058
+ const idempotencyKey = randomUUID();
2769
3059
  const { receipt } = await state2.spClient.postReceipt({
2770
3060
  authorizationId: authzId,
2771
3061
  // Optional cross-check — the AS fails closed on a mismatch.
@@ -2775,7 +3065,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
2775
3065
  actionType,
2776
3066
  executionContext: { ...execution },
2777
3067
  amount: typeof execution.amount === "number" ? execution.amount : void 0,
2778
- idempotencyKey: randomUUID(),
3068
+ idempotencyKey,
2779
3069
  // Privacy: send the hash and how to reproduce it — never the
2780
3070
  // preimage. `binding` also carries `boundContent`, the plaintext
2781
3071
  // that was hashed (an email's to/cc/subject/body); it stays on
@@ -2789,6 +3079,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
2789
3079
  executionContext: { ...execution },
2790
3080
  authorizationId: authzId,
2791
3081
  profileId: auth.profileId,
3082
+ idempotencyKey,
2792
3083
  contentHash: binding?.contentHash,
2793
3084
  contentBinding: binding?.contentBinding
2794
3085
  });
@@ -3750,16 +4041,16 @@ function verifyGateContentHashes(content, auth) {
3750
4041
  }
3751
4042
 
3752
4043
  // src/lib/integration-registry.ts
3753
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync10 } from "fs";
4044
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11 } from "fs";
3754
4045
  import { homedir as homedir8 } from "os";
3755
- import { dirname as dirname8, join as join10 } from "path";
3756
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join10(homedir8(), ".suveren");
4046
+ import { dirname as dirname9, join as join11 } from "path";
4047
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
3757
4048
  var IntegrationRegistry = class {
3758
4049
  integrations = /* @__PURE__ */ new Map();
3759
4050
  filePath;
3760
4051
  constructor(dataDir2) {
3761
4052
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
3762
- this.filePath = join10(dir, "integrations.json");
4053
+ this.filePath = join11(dir, "integrations.json");
3763
4054
  this.load();
3764
4055
  }
3765
4056
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -3790,12 +4081,12 @@ var IntegrationRegistry = class {
3790
4081
  }
3791
4082
  // ─── Load / Save ────────────────────────────────────────────────────────
3792
4083
  load() {
3793
- if (!existsSync10(this.filePath)) {
3794
- mkdirSync8(dirname8(this.filePath), { recursive: true });
4084
+ if (!existsSync11(this.filePath)) {
4085
+ mkdirSync9(dirname9(this.filePath), { recursive: true });
3795
4086
  return;
3796
4087
  }
3797
4088
  try {
3798
- const raw = readFileSync10(this.filePath, "utf-8");
4089
+ const raw = readFileSync12(this.filePath, "utf-8");
3799
4090
  const data = JSON.parse(raw);
3800
4091
  const migrated = data.integrations.map((i) => {
3801
4092
  if ("toolGating" in i && !("profile" in i)) {
@@ -3824,45 +4115,45 @@ var IntegrationRegistry = class {
3824
4115
  version: 1,
3825
4116
  integrations: Array.from(this.integrations.values())
3826
4117
  };
3827
- mkdirSync8(dirname8(this.filePath), { recursive: true });
3828
- writeFileSync8(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
4118
+ mkdirSync9(dirname9(this.filePath), { recursive: true });
4119
+ writeFileSync9(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
3829
4120
  }
3830
4121
  };
3831
4122
 
3832
4123
  // src/lib/integration-manager.ts
3833
4124
  import { homedir as homedir9 } from "os";
3834
- import { join as join12, delimiter } from "path";
3835
- import { existsSync as existsSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync12, rmSync } from "fs";
4125
+ import { join as join13, delimiter } from "path";
4126
+ import { existsSync as existsSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync } from "fs";
3836
4127
  import { execFile } from "child_process";
3837
4128
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3838
4129
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3839
4130
  import { getProfile as getProfile5 } from "@hap/core";
3840
4131
 
3841
4132
  // src/lib/manifest-loader.ts
3842
- import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
3843
- import { join as join11, resolve } from "path";
4133
+ import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
4134
+ import { join as join12, resolve } from "path";
3844
4135
  var manifests = /* @__PURE__ */ new Map();
3845
4136
  function loadManifests(integrationsDir) {
3846
4137
  const dir = resolve(
3847
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
4138
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join12(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3848
4139
  );
3849
- const indexPath = join11(dir, "index.json");
3850
- if (!existsSync11(indexPath)) {
4140
+ const indexPath = join12(dir, "index.json");
4141
+ if (!existsSync12(indexPath)) {
3851
4142
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
3852
4143
  return 0;
3853
4144
  }
3854
4145
  let index;
3855
4146
  try {
3856
- index = JSON.parse(readFileSync11(indexPath, "utf-8"));
4147
+ index = JSON.parse(readFileSync13(indexPath, "utf-8"));
3857
4148
  } catch (err) {
3858
4149
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
3859
4150
  return 0;
3860
4151
  }
3861
4152
  let loaded = 0;
3862
4153
  for (const [id, relativePath] of Object.entries(index.integrations)) {
3863
- const manifestPath = join11(dir, relativePath);
4154
+ const manifestPath = join12(dir, relativePath);
3864
4155
  try {
3865
- const manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
4156
+ const manifest = JSON.parse(readFileSync13(manifestPath, "utf-8"));
3866
4157
  manifests.set(id, manifest);
3867
4158
  loaded++;
3868
4159
  } catch (err) {
@@ -3950,26 +4241,26 @@ function safeHost(url) {
3950
4241
  }
3951
4242
 
3952
4243
  // src/lib/integration-manager.ts
3953
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join12(homedir9(), ".suveren");
3954
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join12(DEFAULT_DATA_DIR2, "integrations");
4244
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
4245
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join13(DEFAULT_DATA_DIR2, "integrations");
3955
4246
  var installLock = Promise.resolve();
3956
4247
  function withInstallLock(task) {
3957
4248
  const run = installLock.then(task, task);
3958
4249
  installLock = run.then(() => void 0, () => void 0);
3959
4250
  return run;
3960
4251
  }
3961
- var INTEGRATIONS_BIN = join12(INTEGRATIONS_DIR, "node_modules", ".bin");
4252
+ var INTEGRATIONS_BIN = join13(INTEGRATIONS_DIR, "node_modules", ".bin");
3962
4253
  function buildPath() {
3963
4254
  const base = process.env.PATH ?? "";
3964
4255
  return [INTEGRATIONS_BIN, base].join(delimiter);
3965
4256
  }
3966
4257
  function ensureIntegrationsDir() {
3967
- if (!existsSync12(INTEGRATIONS_DIR)) {
3968
- mkdirSync9(INTEGRATIONS_DIR, { recursive: true });
4258
+ if (!existsSync13(INTEGRATIONS_DIR)) {
4259
+ mkdirSync10(INTEGRATIONS_DIR, { recursive: true });
3969
4260
  }
3970
- const pkgPath = join12(INTEGRATIONS_DIR, "package.json");
3971
- if (!existsSync12(pkgPath)) {
3972
- writeFileSync9(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
4261
+ const pkgPath = join13(INTEGRATIONS_DIR, "package.json");
4262
+ if (!existsSync13(pkgPath)) {
4263
+ writeFileSync10(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
3973
4264
  }
3974
4265
  }
3975
4266
  function readAgeOf(config) {
@@ -4035,19 +4326,19 @@ var IntegrationManager = class {
4035
4326
  * exist, and treat anything else as not-installed (and reinstallable).
4036
4327
  */
4037
4328
  isUsableInstall(npmPackage) {
4038
- const pkgDir = join12(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4039
- const pkgJson = join12(pkgDir, "package.json");
4040
- if (!existsSync12(pkgDir) || !existsSync12(pkgJson)) return false;
4329
+ const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4330
+ const pkgJson = join13(pkgDir, "package.json");
4331
+ if (!existsSync13(pkgDir) || !existsSync13(pkgJson)) return false;
4041
4332
  try {
4042
- const pkg = JSON.parse(readFileSync12(pkgJson, "utf8"));
4333
+ const pkg = JSON.parse(readFileSync14(pkgJson, "utf8"));
4043
4334
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
4044
4335
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
4045
4336
  const binEntries = Object.entries(binMap);
4046
4337
  for (const [binName, target] of binEntries) {
4047
- if (!existsSync12(join12(pkgDir, target))) return false;
4338
+ if (!existsSync13(join13(pkgDir, target))) return false;
4048
4339
  if (!this.binShimExists(binName)) return false;
4049
4340
  }
4050
- if (binEntries.length === 0 && pkg.main && !existsSync12(join12(pkgDir, pkg.main))) return false;
4341
+ if (binEntries.length === 0 && pkg.main && !existsSync13(join13(pkgDir, pkg.main))) return false;
4051
4342
  return true;
4052
4343
  } catch {
4053
4344
  return false;
@@ -4061,7 +4352,7 @@ var IntegrationManager = class {
4061
4352
  binShimExists(binName) {
4062
4353
  if (!binName) return false;
4063
4354
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
4064
- return candidates.some((c) => existsSync12(join12(INTEGRATIONS_BIN, c)));
4355
+ return candidates.some((c) => existsSync13(join13(INTEGRATIONS_BIN, c)));
4065
4356
  }
4066
4357
  /**
4067
4358
  * Install an npm package into the managed integrations directory if not
@@ -4085,8 +4376,8 @@ var IntegrationManager = class {
4085
4376
  }
4086
4377
  /** The actual install. Callers MUST hold the install lock. */
4087
4378
  async installNow(npmPackage) {
4088
- const pkgDir = join12(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4089
- if (existsSync12(pkgDir)) {
4379
+ const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4380
+ if (existsSync13(pkgDir)) {
4090
4381
  console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
4091
4382
  try {
4092
4383
  rmSync(pkgDir, { recursive: true, force: true });
@@ -4521,28 +4812,28 @@ var IntegrationManager = class {
4521
4812
  };
4522
4813
 
4523
4814
  // src/lib/profile-loader.ts
4524
- import { readFileSync as readFileSync13, existsSync as existsSync13 } from "fs";
4525
- import { join as join13, resolve as resolve2 } from "path";
4815
+ import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
4816
+ import { join as join14, resolve as resolve2 } from "path";
4526
4817
  import { registerProfile } from "@hap/core";
4527
4818
  function loadProfiles(profilesDir) {
4528
- const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join13(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
4529
- const indexPath = join13(dir, "index.json");
4530
- if (!existsSync13(indexPath)) {
4819
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
4820
+ const indexPath = join14(dir, "index.json");
4821
+ if (!existsSync14(indexPath)) {
4531
4822
  warnNoProfiles(`No index.json at ${indexPath}`);
4532
4823
  return 0;
4533
4824
  }
4534
4825
  let index;
4535
4826
  try {
4536
- index = JSON.parse(readFileSync13(indexPath, "utf-8"));
4827
+ index = JSON.parse(readFileSync15(indexPath, "utf-8"));
4537
4828
  } catch (err) {
4538
4829
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
4539
4830
  return 0;
4540
4831
  }
4541
4832
  let loaded = 0;
4542
4833
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
4543
- const profilePath = join13(dir, relativePath);
4834
+ const profilePath = join14(dir, relativePath);
4544
4835
  try {
4545
- const profile = JSON.parse(readFileSync13(profilePath, "utf-8"));
4836
+ const profile = JSON.parse(readFileSync15(profilePath, "utf-8"));
4546
4837
  registerProfile(profileId, profile);
4547
4838
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
4548
4839
  if (shortName && shortName !== profileId) {
@@ -4578,8 +4869,8 @@ function warnNoProfiles(reason) {
4578
4869
  import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
4579
4870
 
4580
4871
  // src/lib/committed-executor.ts
4581
- import { existsSync as existsSync14, mkdirSync as mkdirSync10, readFileSync as readFileSync14, unlinkSync as unlinkSync6, writeFileSync as writeFileSync10 } from "fs";
4582
- import { dirname as dirname9, join as join14 } from "path";
4872
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync16, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "fs";
4873
+ import { dirname as dirname10, join as join15 } from "path";
4583
4874
  var CommittedExecutor = class {
4584
4875
  constructor(run) {
4585
4876
  this.run = run;
@@ -4607,7 +4898,7 @@ var ExecutorLock = class {
4607
4898
  path;
4608
4899
  held = false;
4609
4900
  constructor(dataDir2) {
4610
- this.path = join14(dataDir2, "executor.lock");
4901
+ this.path = join15(dataDir2, "executor.lock");
4611
4902
  }
4612
4903
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
4613
4904
  acquire() {
@@ -4615,9 +4906,9 @@ var ExecutorLock = class {
4615
4906
  if (current && current.pid !== process.pid && isAlive(current.pid)) {
4616
4907
  return { held: false, holderPid: current.pid };
4617
4908
  }
4618
- mkdirSync10(dirname9(this.path), { recursive: true, mode: 448 });
4909
+ mkdirSync11(dirname10(this.path), { recursive: true, mode: 448 });
4619
4910
  const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
4620
- writeFileSync10(this.path, JSON.stringify(lock), { mode: 384 });
4911
+ writeFileSync11(this.path, JSON.stringify(lock), { mode: 384 });
4621
4912
  this.held = true;
4622
4913
  return { held: true };
4623
4914
  }
@@ -4639,9 +4930,9 @@ var ExecutorLock = class {
4639
4930
  }
4640
4931
  }
4641
4932
  read() {
4642
- if (!existsSync14(this.path)) return null;
4933
+ if (!existsSync15(this.path)) return null;
4643
4934
  try {
4644
- const parsed = JSON.parse(readFileSync14(this.path, "utf8"));
4935
+ const parsed = JSON.parse(readFileSync16(this.path, "utf8"));
4645
4936
  return typeof parsed?.pid === "number" ? parsed : null;
4646
4937
  } catch {
4647
4938
  return null;
@@ -4661,76 +4952,8 @@ function isAlive(pid) {
4661
4952
  import { homedir as homedir10 } from "os";
4662
4953
  import { join as join17 } from "path";
4663
4954
 
4664
- // src/lib/as-config.ts
4665
- import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
4666
- import { dirname as dirname10, join as join15 } from "path";
4667
- var DEFAULT_AS_URL = "https://www.suveren.ai";
4668
- function configPath(dataDir2) {
4669
- return join15(dataDir2, "config.json");
4670
- }
4671
- function readAsConfigStrict(dataDir2) {
4672
- const path = configPath(dataDir2);
4673
- if (!existsSync15(path)) return {};
4674
- let data;
4675
- try {
4676
- data = JSON.parse(readFileSync15(path, "utf-8"));
4677
- } catch (err) {
4678
- throw new Error(
4679
- `Could not parse ${path}: ${err instanceof Error ? err.message : String(err)}`
4680
- );
4681
- }
4682
- const out = {};
4683
- if (typeof data.asUrl === "string") out.asUrl = data.asUrl;
4684
- if (typeof data.caFile === "string") out.caFile = data.caFile;
4685
- return out;
4686
- }
4687
- function validateAsUrl(candidate) {
4688
- const trimmed = candidate.trim();
4689
- if (!trimmed) return { ok: false, error: "The Authority Server URL is empty." };
4690
- let parsed;
4691
- try {
4692
- parsed = new URL(trimmed);
4693
- } catch {
4694
- return { ok: false, error: `"${trimmed}" is not a valid URL.` };
4695
- }
4696
- const isLocalHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
4697
- if (parsed.protocol !== "https:" && !isLocalHttp) {
4698
- return {
4699
- ok: false,
4700
- error: `"${trimmed}" must use https:// (http:// is only allowed for http://localhost or http://127.0.0.1).`
4701
- };
4702
- }
4703
- let normalized = `${parsed.origin}${parsed.pathname}${parsed.search}${parsed.hash}`;
4704
- if (normalized.endsWith("/") && normalized !== `${parsed.origin}/`) {
4705
- normalized = normalized.slice(0, -1);
4706
- } else if (normalized === `${parsed.origin}/`) {
4707
- normalized = parsed.origin;
4708
- }
4709
- return { ok: true, url: normalized };
4710
- }
4711
- function resolveAsUrl(dataDir2, flag) {
4712
- if (flag) {
4713
- const v = validateAsUrl(flag);
4714
- if (!v.ok) throw new Error(`Invalid --as-url: ${v.error}`);
4715
- return v.url;
4716
- }
4717
- const envUrl = process.env.SUVEREN_AS_URL;
4718
- if (envUrl) {
4719
- const v = validateAsUrl(envUrl);
4720
- if (!v.ok) throw new Error(`Invalid SUVEREN_AS_URL: ${v.error}`);
4721
- return v.url;
4722
- }
4723
- const saved = readAsConfigStrict(dataDir2).asUrl;
4724
- if (saved) {
4725
- const v = validateAsUrl(saved);
4726
- if (!v.ok) throw new Error(`Invalid saved as-url "${saved}" in ${dataDir2}/config.json: ${v.error}`);
4727
- return v.url;
4728
- }
4729
- return DEFAULT_AS_URL;
4730
- }
4731
-
4732
4955
  // src/lib/mcp-as-tracker.ts
4733
- import { existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync12 } from "fs";
4956
+ import { existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "fs";
4734
4957
  import { dirname as dirname11, join as join16 } from "path";
4735
4958
  function trackerPath(dataDir2) {
4736
4959
  return join16(dataDir2, "mcp-as-pairing.json");
@@ -4739,7 +4962,7 @@ function readMcpPairedAsUrl(dataDir2) {
4739
4962
  const path = trackerPath(dataDir2);
4740
4963
  if (!existsSync16(path)) return null;
4741
4964
  try {
4742
- const data = JSON.parse(readFileSync16(path, "utf-8"));
4965
+ const data = JSON.parse(readFileSync17(path, "utf-8"));
4743
4966
  return typeof data.asUrl === "string" ? data.asUrl : null;
4744
4967
  } catch {
4745
4968
  return null;
@@ -4754,6 +4977,7 @@ function writeMcpPairedAsUrl(dataDir2, asUrl) {
4754
4977
  // bin/http.ts
4755
4978
  var dataDir = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
4756
4979
  var spUrl = resolveAsUrl(dataDir);
4980
+ validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
4757
4981
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
4758
4982
  setAsBaseUrl(spUrl);
4759
4983
  var state = new SharedState(spUrl, void 0, dataDir);
@@ -4838,7 +5062,7 @@ app.post("/internal/configure", internalOnly, (req, res) => {
4838
5062
  });
4839
5063
  app.post("/internal/clear-session", internalOnly, (req, res) => {
4840
5064
  const reason = req.body?.reason;
4841
- const validReason = reason === "as-key-mismatch" || reason === "as-url-changed" ? reason : void 0;
5065
+ const validReason = reason === "as-key-mismatch" || reason === "as-tls-mismatch" || reason === "as-url-changed" ? reason : void 0;
4842
5066
  state.spClient.clearSession(validReason);
4843
5067
  console.error(`[Suveren MCP] Session cleared by control-plane (${validReason ?? "expired"})`);
4844
5068
  res.json({ ok: true });