@suveren/gateway 0.9.0 → 0.10.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 (221) hide show
  1. package/README.md +33 -1
  2. package/bin/suveren-gateway.js +203 -5
  3. package/content/integrations/erp.json +184 -0
  4. package/content/integrations/index.json +2 -1
  5. package/dist/control-plane/index.mjs +653 -179
  6. package/dist/mcp-server/http.mjs +466 -259
  7. package/dist/ui/assets/{index-CIiOCgjJ.js → index-B1qNl7zo.js} +1 -1
  8. package/dist/ui/index.html +1 -1
  9. package/lib/as-pairing.mjs +77 -0
  10. package/lib/config.mjs +48 -0
  11. package/node_modules/undici/LICENSE +21 -0
  12. package/node_modules/undici/README.md +741 -0
  13. package/node_modules/undici/docs/docs/api/Agent.md +84 -0
  14. package/node_modules/undici/docs/docs/api/BalancedPool.md +99 -0
  15. package/node_modules/undici/docs/docs/api/CacheStorage.md +30 -0
  16. package/node_modules/undici/docs/docs/api/CacheStore.md +164 -0
  17. package/node_modules/undici/docs/docs/api/Client.md +288 -0
  18. package/node_modules/undici/docs/docs/api/ClientStats.md +27 -0
  19. package/node_modules/undici/docs/docs/api/Connector.md +115 -0
  20. package/node_modules/undici/docs/docs/api/ContentType.md +57 -0
  21. package/node_modules/undici/docs/docs/api/Cookies.md +128 -0
  22. package/node_modules/undici/docs/docs/api/Debug.md +62 -0
  23. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +317 -0
  24. package/node_modules/undici/docs/docs/api/Dispatcher.md +1393 -0
  25. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +159 -0
  26. package/node_modules/undici/docs/docs/api/Errors.md +49 -0
  27. package/node_modules/undici/docs/docs/api/EventSource.md +45 -0
  28. package/node_modules/undici/docs/docs/api/Fetch.md +60 -0
  29. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +139 -0
  30. package/node_modules/undici/docs/docs/api/H2CClient.md +263 -0
  31. package/node_modules/undici/docs/docs/api/MockAgent.md +603 -0
  32. package/node_modules/undici/docs/docs/api/MockCallHistory.md +197 -0
  33. package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +43 -0
  34. package/node_modules/undici/docs/docs/api/MockClient.md +81 -0
  35. package/node_modules/undici/docs/docs/api/MockErrors.md +12 -0
  36. package/node_modules/undici/docs/docs/api/MockPool.md +555 -0
  37. package/node_modules/undici/docs/docs/api/Pool.md +84 -0
  38. package/node_modules/undici/docs/docs/api/PoolStats.md +35 -0
  39. package/node_modules/undici/docs/docs/api/ProxyAgent.md +229 -0
  40. package/node_modules/undici/docs/docs/api/RedirectHandler.md +93 -0
  41. package/node_modules/undici/docs/docs/api/RetryAgent.md +50 -0
  42. package/node_modules/undici/docs/docs/api/RetryHandler.md +118 -0
  43. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +145 -0
  44. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +616 -0
  45. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +275 -0
  46. package/node_modules/undici/docs/docs/api/Util.md +25 -0
  47. package/node_modules/undici/docs/docs/api/WebSocket.md +141 -0
  48. package/node_modules/undici/docs/docs/api/api-lifecycle.md +91 -0
  49. package/node_modules/undici/docs/docs/best-practices/client-certificate.md +64 -0
  50. package/node_modules/undici/docs/docs/best-practices/crawling.md +58 -0
  51. package/node_modules/undici/docs/docs/best-practices/mocking-request.md +190 -0
  52. package/node_modules/undici/docs/docs/best-practices/proxy.md +127 -0
  53. package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +224 -0
  54. package/node_modules/undici/docs/docs/best-practices/writing-tests.md +63 -0
  55. package/node_modules/undici/index-fetch.js +65 -0
  56. package/node_modules/undici/index.d.ts +3 -0
  57. package/node_modules/undici/index.js +234 -0
  58. package/node_modules/undici/lib/api/abort-signal.js +59 -0
  59. package/node_modules/undici/lib/api/api-connect.js +110 -0
  60. package/node_modules/undici/lib/api/api-pipeline.js +252 -0
  61. package/node_modules/undici/lib/api/api-request.js +214 -0
  62. package/node_modules/undici/lib/api/api-stream.js +209 -0
  63. package/node_modules/undici/lib/api/api-upgrade.js +111 -0
  64. package/node_modules/undici/lib/api/index.js +7 -0
  65. package/node_modules/undici/lib/api/readable.js +580 -0
  66. package/node_modules/undici/lib/cache/memory-cache-store.js +279 -0
  67. package/node_modules/undici/lib/cache/sqlite-cache-store.js +467 -0
  68. package/node_modules/undici/lib/core/connect.js +153 -0
  69. package/node_modules/undici/lib/core/constants.js +143 -0
  70. package/node_modules/undici/lib/core/diagnostics.js +227 -0
  71. package/node_modules/undici/lib/core/errors.js +477 -0
  72. package/node_modules/undici/lib/core/request.js +485 -0
  73. package/node_modules/undici/lib/core/socks5-client.js +422 -0
  74. package/node_modules/undici/lib/core/socks5-utils.js +212 -0
  75. package/node_modules/undici/lib/core/symbols.js +75 -0
  76. package/node_modules/undici/lib/core/tree.js +160 -0
  77. package/node_modules/undici/lib/core/util.js +992 -0
  78. package/node_modules/undici/lib/dispatcher/agent.js +158 -0
  79. package/node_modules/undici/lib/dispatcher/balanced-pool.js +221 -0
  80. package/node_modules/undici/lib/dispatcher/client-h1.js +1746 -0
  81. package/node_modules/undici/lib/dispatcher/client-h2.js +1060 -0
  82. package/node_modules/undici/lib/dispatcher/client.js +668 -0
  83. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +184 -0
  84. package/node_modules/undici/lib/dispatcher/dispatcher.js +48 -0
  85. package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +146 -0
  86. package/node_modules/undici/lib/dispatcher/fixed-queue.js +135 -0
  87. package/node_modules/undici/lib/dispatcher/h2c-client.js +51 -0
  88. package/node_modules/undici/lib/dispatcher/pool-base.js +214 -0
  89. package/node_modules/undici/lib/dispatcher/pool.js +118 -0
  90. package/node_modules/undici/lib/dispatcher/proxy-agent.js +319 -0
  91. package/node_modules/undici/lib/dispatcher/retry-agent.js +35 -0
  92. package/node_modules/undici/lib/dispatcher/round-robin-pool.js +137 -0
  93. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +260 -0
  94. package/node_modules/undici/lib/encoding/index.js +33 -0
  95. package/node_modules/undici/lib/global.js +59 -0
  96. package/node_modules/undici/lib/handler/cache-handler.js +776 -0
  97. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +124 -0
  98. package/node_modules/undici/lib/handler/decorator-handler.js +67 -0
  99. package/node_modules/undici/lib/handler/deduplication-handler.js +460 -0
  100. package/node_modules/undici/lib/handler/redirect-handler.js +238 -0
  101. package/node_modules/undici/lib/handler/retry-handler.js +504 -0
  102. package/node_modules/undici/lib/handler/unwrap-handler.js +106 -0
  103. package/node_modules/undici/lib/handler/wrap-handler.js +105 -0
  104. package/node_modules/undici/lib/interceptor/cache.js +621 -0
  105. package/node_modules/undici/lib/interceptor/decompress.js +551 -0
  106. package/node_modules/undici/lib/interceptor/deduplicate.js +117 -0
  107. package/node_modules/undici/lib/interceptor/dns.js +571 -0
  108. package/node_modules/undici/lib/interceptor/dump.js +99 -0
  109. package/node_modules/undici/lib/interceptor/redirect.js +21 -0
  110. package/node_modules/undici/lib/interceptor/response-error.js +95 -0
  111. package/node_modules/undici/lib/interceptor/retry.js +19 -0
  112. package/node_modules/undici/lib/llhttp/.gitkeep +0 -0
  113. package/node_modules/undici/lib/llhttp/constants.d.ts +195 -0
  114. package/node_modules/undici/lib/llhttp/constants.js +531 -0
  115. package/node_modules/undici/lib/llhttp/llhttp-wasm.js +15 -0
  116. package/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +15 -0
  117. package/node_modules/undici/lib/llhttp/utils.d.ts +2 -0
  118. package/node_modules/undici/lib/llhttp/utils.js +12 -0
  119. package/node_modules/undici/lib/mock/mock-agent.js +232 -0
  120. package/node_modules/undici/lib/mock/mock-call-history.js +248 -0
  121. package/node_modules/undici/lib/mock/mock-client.js +68 -0
  122. package/node_modules/undici/lib/mock/mock-errors.js +29 -0
  123. package/node_modules/undici/lib/mock/mock-interceptor.js +209 -0
  124. package/node_modules/undici/lib/mock/mock-pool.js +68 -0
  125. package/node_modules/undici/lib/mock/mock-symbols.js +32 -0
  126. package/node_modules/undici/lib/mock/mock-utils.js +486 -0
  127. package/node_modules/undici/lib/mock/pending-interceptors-formatter.js +43 -0
  128. package/node_modules/undici/lib/mock/snapshot-agent.js +353 -0
  129. package/node_modules/undici/lib/mock/snapshot-recorder.js +588 -0
  130. package/node_modules/undici/lib/mock/snapshot-utils.js +158 -0
  131. package/node_modules/undici/lib/util/cache.js +676 -0
  132. package/node_modules/undici/lib/util/date.js +670 -0
  133. package/node_modules/undici/lib/util/promise.js +28 -0
  134. package/node_modules/undici/lib/util/runtime-features.js +124 -0
  135. package/node_modules/undici/lib/util/stats.js +32 -0
  136. package/node_modules/undici/lib/util/timers.js +425 -0
  137. package/node_modules/undici/lib/web/cache/cache.js +864 -0
  138. package/node_modules/undici/lib/web/cache/cachestorage.js +152 -0
  139. package/node_modules/undici/lib/web/cache/util.js +45 -0
  140. package/node_modules/undici/lib/web/cookies/constants.js +12 -0
  141. package/node_modules/undici/lib/web/cookies/index.js +199 -0
  142. package/node_modules/undici/lib/web/cookies/parse.js +314 -0
  143. package/node_modules/undici/lib/web/cookies/util.js +352 -0
  144. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +494 -0
  145. package/node_modules/undici/lib/web/eventsource/eventsource.js +501 -0
  146. package/node_modules/undici/lib/web/eventsource/util.js +29 -0
  147. package/node_modules/undici/lib/web/fetch/LICENSE +21 -0
  148. package/node_modules/undici/lib/web/fetch/body.js +509 -0
  149. package/node_modules/undici/lib/web/fetch/constants.js +131 -0
  150. package/node_modules/undici/lib/web/fetch/data-url.js +596 -0
  151. package/node_modules/undici/lib/web/fetch/formdata-parser.js +586 -0
  152. package/node_modules/undici/lib/web/fetch/formdata.js +259 -0
  153. package/node_modules/undici/lib/web/fetch/global.js +40 -0
  154. package/node_modules/undici/lib/web/fetch/headers.js +719 -0
  155. package/node_modules/undici/lib/web/fetch/index.js +2413 -0
  156. package/node_modules/undici/lib/web/fetch/request.js +1115 -0
  157. package/node_modules/undici/lib/web/fetch/response.js +641 -0
  158. package/node_modules/undici/lib/web/fetch/util.js +1522 -0
  159. package/node_modules/undici/lib/web/infra/index.js +229 -0
  160. package/node_modules/undici/lib/web/subresource-integrity/Readme.md +9 -0
  161. package/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +307 -0
  162. package/node_modules/undici/lib/web/webidl/index.js +1006 -0
  163. package/node_modules/undici/lib/web/websocket/connection.js +339 -0
  164. package/node_modules/undici/lib/web/websocket/constants.js +126 -0
  165. package/node_modules/undici/lib/web/websocket/events.js +331 -0
  166. package/node_modules/undici/lib/web/websocket/frame.js +133 -0
  167. package/node_modules/undici/lib/web/websocket/permessage-deflate.js +105 -0
  168. package/node_modules/undici/lib/web/websocket/receiver.js +507 -0
  169. package/node_modules/undici/lib/web/websocket/sender.js +109 -0
  170. package/node_modules/undici/lib/web/websocket/stream/websocketerror.js +104 -0
  171. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +496 -0
  172. package/node_modules/undici/lib/web/websocket/util.js +347 -0
  173. package/node_modules/undici/lib/web/websocket/websocket.js +758 -0
  174. package/node_modules/undici/package.json +152 -0
  175. package/node_modules/undici/scripts/strip-comments.js +10 -0
  176. package/node_modules/undici/types/README.md +6 -0
  177. package/node_modules/undici/types/agent.d.ts +32 -0
  178. package/node_modules/undici/types/api.d.ts +43 -0
  179. package/node_modules/undici/types/balanced-pool.d.ts +30 -0
  180. package/node_modules/undici/types/cache-interceptor.d.ts +179 -0
  181. package/node_modules/undici/types/cache.d.ts +36 -0
  182. package/node_modules/undici/types/client-stats.d.ts +15 -0
  183. package/node_modules/undici/types/client.d.ts +139 -0
  184. package/node_modules/undici/types/connector.d.ts +36 -0
  185. package/node_modules/undici/types/content-type.d.ts +21 -0
  186. package/node_modules/undici/types/cookies.d.ts +30 -0
  187. package/node_modules/undici/types/diagnostics-channel.d.ts +74 -0
  188. package/node_modules/undici/types/dispatcher.d.ts +275 -0
  189. package/node_modules/undici/types/env-http-proxy-agent.d.ts +22 -0
  190. package/node_modules/undici/types/errors.d.ts +177 -0
  191. package/node_modules/undici/types/eventsource.d.ts +66 -0
  192. package/node_modules/undici/types/fetch.d.ts +231 -0
  193. package/node_modules/undici/types/formdata.d.ts +114 -0
  194. package/node_modules/undici/types/global-dispatcher.d.ts +9 -0
  195. package/node_modules/undici/types/global-origin.d.ts +7 -0
  196. package/node_modules/undici/types/h2c-client.d.ts +73 -0
  197. package/node_modules/undici/types/handlers.d.ts +14 -0
  198. package/node_modules/undici/types/header.d.ts +160 -0
  199. package/node_modules/undici/types/index.d.ts +91 -0
  200. package/node_modules/undici/types/interceptors.d.ts +82 -0
  201. package/node_modules/undici/types/mock-agent.d.ts +68 -0
  202. package/node_modules/undici/types/mock-call-history.d.ts +111 -0
  203. package/node_modules/undici/types/mock-client.d.ts +27 -0
  204. package/node_modules/undici/types/mock-errors.d.ts +12 -0
  205. package/node_modules/undici/types/mock-interceptor.d.ts +94 -0
  206. package/node_modules/undici/types/mock-pool.d.ts +27 -0
  207. package/node_modules/undici/types/patch.d.ts +29 -0
  208. package/node_modules/undici/types/pool-stats.d.ts +19 -0
  209. package/node_modules/undici/types/pool.d.ts +41 -0
  210. package/node_modules/undici/types/proxy-agent.d.ts +29 -0
  211. package/node_modules/undici/types/readable.d.ts +68 -0
  212. package/node_modules/undici/types/retry-agent.d.ts +8 -0
  213. package/node_modules/undici/types/retry-handler.d.ts +125 -0
  214. package/node_modules/undici/types/round-robin-pool.d.ts +41 -0
  215. package/node_modules/undici/types/snapshot-agent.d.ts +109 -0
  216. package/node_modules/undici/types/socks5-proxy-agent.d.ts +25 -0
  217. package/node_modules/undici/types/util.d.ts +18 -0
  218. package/node_modules/undici/types/utility.d.ts +7 -0
  219. package/node_modules/undici/types/webidl.d.ts +347 -0
  220. package/node_modules/undici/types/websocket.d.ts +188 -0
  221. package/package.json +4 -2
@@ -112,6 +112,262 @@ 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
+ async function fetchAs(url, init, pinning) {
328
+ if (!pinning.enforce && !pinning.capture) {
329
+ const res2 = await fetch(url, init);
330
+ return { res: res2 };
331
+ }
332
+ if (pinning.enforce && !pinning.capture && !pinning.pinnedSpkiHex) {
333
+ throw new AsTlsMismatchError(
334
+ `pin-tls is on but no certificate is pinned yet for ${url} \u2014 sign in again to establish it (re-pairing required).`
335
+ );
336
+ }
337
+ if (pinning.pinnedSpkiHex && (pinning.enforce || !pinning.capture)) {
338
+ const dispatcher2 = buildPinnedDispatcher(pinning.pinnedSpkiHex);
339
+ try {
340
+ const res2 = await undiciFetch(url, { ...init, dispatcher: dispatcher2 });
341
+ return { res: res2 };
342
+ } catch (err) {
343
+ if (!isPinCheckError(err)) throw err;
344
+ throw new AsTlsMismatchError(
345
+ `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.`
346
+ );
347
+ }
348
+ }
349
+ const { dispatcher, getCapturedCert } = buildCapturingDispatcher();
350
+ let res;
351
+ try {
352
+ res = await undiciFetch(url, { ...init, dispatcher });
353
+ } catch (err) {
354
+ if (!isPinCheckError(err)) throw err;
355
+ throw new AsTlsMismatchError(
356
+ `could not verify the Authority Server at ${url} while establishing its TLS pin \u2014 ${describeFetchError(err)}.`
357
+ );
358
+ }
359
+ const cert = getCapturedCert();
360
+ return { res, capturedSpkiHex: cert ? spkiSha256Hex(cert) : void 0 };
361
+ }
362
+ function describeFetchError(err) {
363
+ if (err instanceof Error) {
364
+ const cause = err.cause;
365
+ if (cause instanceof Error) return cause.message;
366
+ return err.message;
367
+ }
368
+ return String(err);
369
+ }
370
+
115
371
  // src/lib/sp-client.ts
116
372
  var SPReceiptError = class extends Error {
117
373
  constructor(message, statusCode, body) {
@@ -127,8 +383,9 @@ var DEFAULT_RECEIPT_RETRY = {
127
383
  };
128
384
  var sleep = (ms) => ms > 0 ? new Promise((resolve3) => setTimeout(resolve3, ms)) : Promise.resolve();
129
385
  var SPClient = class {
130
- constructor(baseUrl, receiptRetry = {}) {
386
+ constructor(baseUrl, receiptRetry = {}, dataDir2) {
131
387
  this.baseUrl = baseUrl;
388
+ this.dataDir = dataDir2;
132
389
  this.receiptRetry = { ...DEFAULT_RECEIPT_RETRY, ...receiptRetry };
133
390
  }
134
391
  sessionCookie = "";
@@ -141,6 +398,22 @@ var SPClient = class {
141
398
  * was active tells the control plane. Concurrent in-flight calls that were
142
399
  * all sent under the same now-dead cookie would otherwise all report it. */
143
400
  sessionExpiredNotified = false;
401
+ /** Same single-flight shape, for a TLS pin mismatch (as-tls-pin.ts). */
402
+ tlsMismatchNotified = false;
403
+ /** Opt-in TLS pinning (as-tls-pin.ts) for every call THIS client makes.
404
+ * Re-read live on each call rather than cached — same reasoning as
405
+ * AttestationCache.getPublicKey's pin check: a pin or setting change
406
+ * must take effect on the very next call, not the next restart. Never
407
+ * allowed to CAPTURE a new pin here — only the control plane's sign-in
408
+ * exchange (as-challenge.ts) does that; this client only enforces. */
409
+ tlsPinning() {
410
+ if (!this.dataDir) return { enforce: false };
411
+ const pin = readPairing(this.dataDir);
412
+ return {
413
+ enforce: resolvePinTls(this.dataDir),
414
+ pinnedSpkiHex: pin && pin.asUrl === this.baseUrl ? pin.tlsSpkiPinHex : void 0
415
+ };
416
+ }
144
417
  /** The AS base URL this client talks to (for provenance records). */
145
418
  get url() {
146
419
  return this.baseUrl;
@@ -152,6 +425,7 @@ var SPClient = class {
152
425
  this.sessionCookie = cookie;
153
426
  this.lockReason = null;
154
427
  this.sessionExpiredNotified = false;
428
+ this.tlsMismatchNotified = false;
155
429
  }
156
430
  /**
157
431
  * Drop the session because the control plane learned (by whatever path —
@@ -205,10 +479,21 @@ var SPClient = class {
205
479
  headers["X-API-Key"] = this.apiKey;
206
480
  }
207
481
  const hadSession = this.sessionCookie !== "";
208
- const res = await globalThis.fetch(url, {
209
- ...init,
210
- headers
211
- });
482
+ let res;
483
+ try {
484
+ const result = await fetchAs(url, { ...init, headers }, this.tlsPinning());
485
+ res = result.res;
486
+ } catch (err) {
487
+ if (err instanceof AsTlsMismatchError) {
488
+ this.sessionCookie = "";
489
+ this.lockReason = "as-tls-mismatch";
490
+ if (!this.tlsMismatchNotified) {
491
+ this.tlsMismatchNotified = true;
492
+ void notifyControlPlane("as-tls-mismatch");
493
+ }
494
+ }
495
+ throw err;
496
+ }
212
497
  if (res.status === 401 && hadSession) {
213
498
  this.sessionCookie = "";
214
499
  this.lockReason = "expired";
@@ -371,32 +656,6 @@ var SPClient = class {
371
656
 
372
657
  // src/lib/attestation-cache.ts
373
658
  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
659
  var AsKeyMismatchError = class extends Error {
401
660
  constructor(message) {
402
661
  super(message);
@@ -558,18 +817,18 @@ var AttestationCache = class {
558
817
  };
559
818
 
560
819
  // src/lib/gate-store.ts
561
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
820
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync2 } from "fs";
562
821
  import { homedir } from "os";
563
- import { dirname as dirname2, join as join3 } from "path";
822
+ import { dirname as dirname3, join as join4 } from "path";
564
823
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
565
- var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join3(homedir(), ".suveren");
824
+ var DEFAULT_DIR = process.env.SUVEREN_DATA_DIR ?? join4(homedir(), ".suveren");
566
825
  var GateStore = class {
567
826
  entries = /* @__PURE__ */ new Map();
568
827
  baseDir;
569
828
  vaultKey = null;
570
829
  constructor(filePath) {
571
830
  if (filePath && filePath.endsWith(".json")) {
572
- this.baseDir = dirname2(filePath);
831
+ this.baseDir = dirname3(filePath);
573
832
  } else {
574
833
  this.baseDir = filePath ?? DEFAULT_DIR;
575
834
  }
@@ -580,11 +839,11 @@ var GateStore = class {
580
839
  */
581
840
  setVaultKey(key) {
582
841
  this.vaultKey = key;
583
- if (existsSync3(this.encryptedFilePath)) {
842
+ if (existsSync4(this.encryptedFilePath)) {
584
843
  this.loadEncrypted();
585
844
  } else if (this.entries.size > 0) {
586
845
  this.persistEncrypted();
587
- if (existsSync3(this.plaintextFilePath)) {
846
+ if (existsSync4(this.plaintextFilePath)) {
588
847
  try {
589
848
  unlinkSync2(this.plaintextFilePath);
590
849
  } catch {
@@ -622,11 +881,11 @@ var GateStore = class {
622
881
  clearAll() {
623
882
  this.entries = /* @__PURE__ */ new Map();
624
883
  try {
625
- if (existsSync3(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
884
+ if (existsSync4(this.plaintextFilePath)) unlinkSync2(this.plaintextFilePath);
626
885
  } catch {
627
886
  }
628
887
  try {
629
- if (existsSync3(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
888
+ if (existsSync4(this.encryptedFilePath)) unlinkSync2(this.encryptedFilePath);
630
889
  } catch {
631
890
  }
632
891
  }
@@ -659,19 +918,19 @@ var GateStore = class {
659
918
  }
660
919
  // ─── File paths ─────────────────────────────────────────────────────────
661
920
  get plaintextFilePath() {
662
- return join3(this.baseDir, "gates.json");
921
+ return join4(this.baseDir, "gates.json");
663
922
  }
664
923
  get encryptedFilePath() {
665
- return join3(this.baseDir, "gates.enc.json");
924
+ return join4(this.baseDir, "gates.enc.json");
666
925
  }
667
926
  // ─── Load / Persist ─────────────────────────────────────────────────────
668
927
  loadPlaintext() {
669
- if (!existsSync3(this.plaintextFilePath)) {
670
- mkdirSync2(this.baseDir, { recursive: true });
928
+ if (!existsSync4(this.plaintextFilePath)) {
929
+ mkdirSync3(this.baseDir, { recursive: true });
671
930
  return;
672
931
  }
673
932
  try {
674
- const raw = readFileSync3(this.plaintextFilePath, "utf-8");
933
+ const raw = readFileSync5(this.plaintextFilePath, "utf-8");
675
934
  const data = JSON.parse(raw);
676
935
  this.entries = new Map(Object.entries(data.entries));
677
936
  } catch {
@@ -680,9 +939,9 @@ var GateStore = class {
680
939
  }
681
940
  }
682
941
  loadEncrypted() {
683
- if (!existsSync3(this.encryptedFilePath)) return;
942
+ if (!existsSync4(this.encryptedFilePath)) return;
684
943
  try {
685
- const raw = readFileSync3(this.encryptedFilePath, "utf-8");
944
+ const raw = readFileSync5(this.encryptedFilePath, "utf-8");
686
945
  const data = JSON.parse(raw);
687
946
  this.entries = /* @__PURE__ */ new Map();
688
947
  for (const [key, blob] of Object.entries(data.entries)) {
@@ -706,8 +965,8 @@ var GateStore = class {
706
965
  version: 1,
707
966
  entries: Object.fromEntries(this.entries)
708
967
  };
709
- mkdirSync2(this.baseDir, { recursive: true, mode: 448 });
710
- writeFileSync2(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
968
+ mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
969
+ writeFileSync3(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
711
970
  }
712
971
  persistEncrypted() {
713
972
  const data = {
@@ -717,17 +976,17 @@ var GateStore = class {
717
976
  for (const [key, entry] of this.entries) {
718
977
  data.entries[key] = this.encrypt(JSON.stringify(entry));
719
978
  }
720
- mkdirSync2(this.baseDir, { recursive: true, mode: 448 });
721
- writeFileSync2(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
979
+ mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
980
+ writeFileSync3(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
722
981
  }
723
982
  };
724
983
 
725
984
  // src/lib/execution-log.ts
726
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4, unlinkSync as unlinkSync3 } from "fs";
985
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4, existsSync as existsSync5, unlinkSync as unlinkSync3 } from "fs";
727
986
  import { homedir as homedir2 } from "os";
728
- import { dirname as dirname3, join as join4 } from "path";
987
+ import { dirname as dirname4, join as join5 } from "path";
729
988
  import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, randomBytes as randomBytes2 } from "crypto";
730
- var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join4(homedir2(), ".suveren");
989
+ var DEFAULT_DIR2 = process.env.SUVEREN_DATA_DIR ?? join5(homedir2(), ".suveren");
731
990
  var MAX_AGE_SECONDS = 31 * 24 * 60 * 60;
732
991
  var ExecutionLog = class {
733
992
  entries = [];
@@ -735,7 +994,7 @@ var ExecutionLog = class {
735
994
  vaultKey = null;
736
995
  constructor(filePath) {
737
996
  if (filePath && filePath.endsWith(".json")) {
738
- this.baseDir = dirname3(filePath);
997
+ this.baseDir = dirname4(filePath);
739
998
  } else {
740
999
  this.baseDir = filePath ?? DEFAULT_DIR2;
741
1000
  }
@@ -746,11 +1005,11 @@ var ExecutionLog = class {
746
1005
  */
747
1006
  setVaultKey(key) {
748
1007
  this.vaultKey = key;
749
- if (existsSync4(this.encryptedFilePath)) {
1008
+ if (existsSync5(this.encryptedFilePath)) {
750
1009
  this.loadEncrypted();
751
1010
  } else if (this.entries.length > 0) {
752
1011
  this.persistEncrypted();
753
- if (existsSync4(this.plaintextFilePath)) {
1012
+ if (existsSync5(this.plaintextFilePath)) {
754
1013
  try {
755
1014
  unlinkSync3(this.plaintextFilePath);
756
1015
  } catch {
@@ -854,19 +1113,19 @@ var ExecutionLog = class {
854
1113
  }
855
1114
  // ─── File paths ─────────────────────────────────────────────────────────
856
1115
  get plaintextFilePath() {
857
- return join4(this.baseDir, "execution-log.json");
1116
+ return join5(this.baseDir, "execution-log.json");
858
1117
  }
859
1118
  get encryptedFilePath() {
860
- return join4(this.baseDir, "execution-log.enc.json");
1119
+ return join5(this.baseDir, "execution-log.enc.json");
861
1120
  }
862
1121
  // ─── Load / Persist ─────────────────────────────────────────────────────
863
1122
  loadPlaintext() {
864
- if (!existsSync4(this.plaintextFilePath)) {
865
- mkdirSync3(this.baseDir, { recursive: true });
1123
+ if (!existsSync5(this.plaintextFilePath)) {
1124
+ mkdirSync4(this.baseDir, { recursive: true });
866
1125
  return;
867
1126
  }
868
1127
  try {
869
- const raw = readFileSync4(this.plaintextFilePath, "utf-8");
1128
+ const raw = readFileSync6(this.plaintextFilePath, "utf-8");
870
1129
  const data = JSON.parse(raw);
871
1130
  this.entries = data.entries;
872
1131
  this.prune();
@@ -876,9 +1135,9 @@ var ExecutionLog = class {
876
1135
  }
877
1136
  }
878
1137
  loadEncrypted() {
879
- if (!existsSync4(this.encryptedFilePath)) return;
1138
+ if (!existsSync5(this.encryptedFilePath)) return;
880
1139
  try {
881
- const raw = readFileSync4(this.encryptedFilePath, "utf-8");
1140
+ const raw = readFileSync6(this.encryptedFilePath, "utf-8");
882
1141
  const data = JSON.parse(raw);
883
1142
  const decrypted = this.decrypt(data.blob);
884
1143
  const logFile = JSON.parse(decrypted);
@@ -898,8 +1157,8 @@ var ExecutionLog = class {
898
1157
  }
899
1158
  persistPlaintext() {
900
1159
  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 });
1160
+ mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
1161
+ writeFileSync4(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
903
1162
  }
904
1163
  persistEncrypted() {
905
1164
  const logFile = { version: 1, entries: this.entries };
@@ -907,8 +1166,8 @@ var ExecutionLog = class {
907
1166
  version: 1,
908
1167
  blob: this.encrypt(JSON.stringify(logFile))
909
1168
  };
910
- mkdirSync3(this.baseDir, { recursive: true, mode: 448 });
911
- writeFileSync3(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1169
+ mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
1170
+ writeFileSync4(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
912
1171
  }
913
1172
  };
914
1173
  function windowCutoff(window, now) {
@@ -925,11 +1184,11 @@ function windowCutoff(window, now) {
925
1184
  }
926
1185
 
927
1186
  // src/lib/denial-log.ts
928
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, unlinkSync as unlinkSync4 } from "fs";
1187
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync4 } from "fs";
929
1188
  import { homedir as homedir3 } from "os";
930
- import { dirname as dirname4, join as join5 } from "path";
1189
+ import { dirname as dirname5, join as join6 } from "path";
931
1190
  import { createCipheriv as createCipheriv3, createDecipheriv as createDecipheriv3, randomBytes as randomBytes3 } from "crypto";
932
- var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join5(homedir3(), ".suveren");
1191
+ var DEFAULT_DIR3 = process.env.SUVEREN_DATA_DIR ?? join6(homedir3(), ".suveren");
933
1192
  var MAX_RECORDS = 200;
934
1193
  var MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
935
1194
  var DenialLog = class {
@@ -937,16 +1196,16 @@ var DenialLog = class {
937
1196
  baseDir;
938
1197
  vaultKey = null;
939
1198
  constructor(filePath) {
940
- this.baseDir = filePath && filePath.endsWith(".json") ? dirname4(filePath) : filePath ?? DEFAULT_DIR3;
1199
+ this.baseDir = filePath && filePath.endsWith(".json") ? dirname5(filePath) : filePath ?? DEFAULT_DIR3;
941
1200
  this.loadPlaintext();
942
1201
  }
943
1202
  setVaultKey(key) {
944
1203
  this.vaultKey = key;
945
- if (existsSync5(this.encryptedFilePath)) {
1204
+ if (existsSync6(this.encryptedFilePath)) {
946
1205
  this.loadEncrypted();
947
1206
  } else if (this.records.length > 0) {
948
1207
  this.persistEncrypted();
949
- if (existsSync5(this.plaintextFilePath)) {
1208
+ if (existsSync6(this.plaintextFilePath)) {
950
1209
  try {
951
1210
  unlinkSync4(this.plaintextFilePath);
952
1211
  } catch {
@@ -996,18 +1255,18 @@ var DenialLog = class {
996
1255
  return Buffer.concat([decipher.update(Buffer.from(blob.ciphertext, "hex")), decipher.final()]).toString("utf8");
997
1256
  }
998
1257
  get plaintextFilePath() {
999
- return join5(this.baseDir, "denials.json");
1258
+ return join6(this.baseDir, "denials.json");
1000
1259
  }
1001
1260
  get encryptedFilePath() {
1002
- return join5(this.baseDir, "denials.enc.json");
1261
+ return join6(this.baseDir, "denials.enc.json");
1003
1262
  }
1004
1263
  loadPlaintext() {
1005
- if (!existsSync5(this.plaintextFilePath)) {
1006
- mkdirSync4(this.baseDir, { recursive: true });
1264
+ if (!existsSync6(this.plaintextFilePath)) {
1265
+ mkdirSync5(this.baseDir, { recursive: true });
1007
1266
  return;
1008
1267
  }
1009
1268
  try {
1010
- const data = JSON.parse(readFileSync5(this.plaintextFilePath, "utf-8"));
1269
+ const data = JSON.parse(readFileSync7(this.plaintextFilePath, "utf-8"));
1011
1270
  this.records = data.records ?? [];
1012
1271
  } catch {
1013
1272
  console.error(`[DenialLog] Could not parse ${this.plaintextFilePath}, starting fresh`);
@@ -1015,9 +1274,9 @@ var DenialLog = class {
1015
1274
  }
1016
1275
  }
1017
1276
  loadEncrypted() {
1018
- if (!existsSync5(this.encryptedFilePath)) return;
1277
+ if (!existsSync6(this.encryptedFilePath)) return;
1019
1278
  try {
1020
- const data = JSON.parse(readFileSync5(this.encryptedFilePath, "utf-8"));
1279
+ const data = JSON.parse(readFileSync7(this.encryptedFilePath, "utf-8"));
1021
1280
  const logFile = JSON.parse(this.decrypt(data.blob));
1022
1281
  this.records = logFile.records ?? [];
1023
1282
  } catch (err) {
@@ -1031,23 +1290,23 @@ var DenialLog = class {
1031
1290
  }
1032
1291
  persistPlaintext() {
1033
1292
  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 });
1293
+ mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
1294
+ writeFileSync5(this.plaintextFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1036
1295
  }
1037
1296
  persistEncrypted() {
1038
1297
  const logFile = { version: 1, records: this.records };
1039
1298
  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 });
1299
+ mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
1300
+ writeFileSync5(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1042
1301
  }
1043
1302
  };
1044
1303
 
1045
1304
  // src/lib/receipt-archive.ts
1046
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, unlinkSync as unlinkSync5 } from "fs";
1305
+ import { mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync7, unlinkSync as unlinkSync5 } from "fs";
1047
1306
  import { homedir as homedir4 } from "os";
1048
- import { dirname as dirname5, join as join6 } from "path";
1307
+ import { dirname as dirname6, join as join7 } from "path";
1049
1308
  import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes4 } from "crypto";
1050
- var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join6(homedir4(), ".suveren");
1309
+ var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join7(homedir4(), ".suveren");
1051
1310
  function stripPreviews(proposal) {
1052
1311
  const args = proposal.toolArgs;
1053
1312
  if (!args || typeof args !== "object") return proposal;
@@ -1061,7 +1320,7 @@ var ReceiptArchive = class {
1061
1320
  vaultKey = null;
1062
1321
  constructor(filePath) {
1063
1322
  if (filePath && filePath.endsWith(".json")) {
1064
- this.baseDir = dirname5(filePath);
1323
+ this.baseDir = dirname6(filePath);
1065
1324
  } else {
1066
1325
  this.baseDir = filePath ?? DEFAULT_DIR4;
1067
1326
  }
@@ -1072,11 +1331,11 @@ var ReceiptArchive = class {
1072
1331
  */
1073
1332
  setVaultKey(key) {
1074
1333
  this.vaultKey = key;
1075
- if (existsSync6(this.encryptedFilePath)) {
1334
+ if (existsSync7(this.encryptedFilePath)) {
1076
1335
  this.loadEncrypted();
1077
1336
  } else if (this.data.receipts.length > 0 || Object.keys(this.data.authorizations).length > 0) {
1078
1337
  this.persistEncrypted();
1079
- if (existsSync6(this.plaintextFilePath)) {
1338
+ if (existsSync7(this.plaintextFilePath)) {
1080
1339
  try {
1081
1340
  unlinkSync5(this.plaintextFilePath);
1082
1341
  } catch {
@@ -1090,7 +1349,7 @@ var ReceiptArchive = class {
1090
1349
  * this state — that would report a hollow success over unreadable evidence.
1091
1350
  */
1092
1351
  isLocked() {
1093
- return this.vaultKey === null && existsSync6(this.encryptedFilePath);
1352
+ return this.vaultKey === null && existsSync7(this.encryptedFilePath);
1094
1353
  }
1095
1354
  /**
1096
1355
  * Archive a signed receipt (and the grant it was issued under).
@@ -1191,19 +1450,19 @@ var ReceiptArchive = class {
1191
1450
  }
1192
1451
  // ─── File paths ─────────────────────────────────────────────────────────
1193
1452
  get plaintextFilePath() {
1194
- return join6(this.baseDir, "receipt-archive.json");
1453
+ return join7(this.baseDir, "receipt-archive.json");
1195
1454
  }
1196
1455
  get encryptedFilePath() {
1197
- return join6(this.baseDir, "receipt-archive.enc.json");
1456
+ return join7(this.baseDir, "receipt-archive.enc.json");
1198
1457
  }
1199
1458
  // ─── Load / Persist ─────────────────────────────────────────────────────
1200
1459
  loadPlaintext() {
1201
- if (!existsSync6(this.plaintextFilePath)) {
1202
- mkdirSync5(this.baseDir, { recursive: true });
1460
+ if (!existsSync7(this.plaintextFilePath)) {
1461
+ mkdirSync6(this.baseDir, { recursive: true });
1203
1462
  return;
1204
1463
  }
1205
1464
  try {
1206
- const raw = readFileSync6(this.plaintextFilePath, "utf-8");
1465
+ const raw = readFileSync8(this.plaintextFilePath, "utf-8");
1207
1466
  const parsed = JSON.parse(raw);
1208
1467
  this.data = {
1209
1468
  version: 1,
@@ -1213,18 +1472,18 @@ var ReceiptArchive = class {
1213
1472
  } catch {
1214
1473
  console.error(`[ReceiptArchive] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
1215
1474
  try {
1216
- writeFileSync5(
1475
+ writeFileSync6(
1217
1476
  `${this.plaintextFilePath}.corrupt-${Date.now()}`,
1218
- readFileSync6(this.plaintextFilePath)
1477
+ readFileSync8(this.plaintextFilePath)
1219
1478
  );
1220
1479
  } catch {
1221
1480
  }
1222
1481
  }
1223
1482
  }
1224
1483
  loadEncrypted() {
1225
- if (!existsSync6(this.encryptedFilePath)) return;
1484
+ if (!existsSync7(this.encryptedFilePath)) return;
1226
1485
  try {
1227
- const raw = readFileSync6(this.encryptedFilePath, "utf-8");
1486
+ const raw = readFileSync8(this.encryptedFilePath, "utf-8");
1228
1487
  const data = JSON.parse(raw);
1229
1488
  const decrypted = this.decrypt(data.blob);
1230
1489
  const parsed = JSON.parse(decrypted);
@@ -1236,9 +1495,9 @@ var ReceiptArchive = class {
1236
1495
  } catch (err) {
1237
1496
  console.error(`[ReceiptArchive] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
1238
1497
  try {
1239
- writeFileSync5(
1498
+ writeFileSync6(
1240
1499
  `${this.encryptedFilePath}.corrupt-${Date.now()}`,
1241
- readFileSync6(this.encryptedFilePath)
1500
+ readFileSync8(this.encryptedFilePath)
1242
1501
  );
1243
1502
  } catch {
1244
1503
  }
@@ -1252,29 +1511,29 @@ var ReceiptArchive = class {
1252
1511
  }
1253
1512
  }
1254
1513
  persistPlaintext() {
1255
- mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
1256
- writeFileSync5(this.plaintextFilePath, JSON.stringify(this.data, null, 2), { encoding: "utf-8", mode: 384 });
1514
+ mkdirSync6(this.baseDir, { recursive: true, mode: 448 });
1515
+ writeFileSync6(this.plaintextFilePath, JSON.stringify(this.data, null, 2), { encoding: "utf-8", mode: 384 });
1257
1516
  }
1258
1517
  persistEncrypted() {
1259
1518
  const data = {
1260
1519
  version: 1,
1261
1520
  blob: this.encrypt(JSON.stringify(this.data))
1262
1521
  };
1263
- mkdirSync5(this.baseDir, { recursive: true, mode: 448 });
1264
- writeFileSync5(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1522
+ mkdirSync6(this.baseDir, { recursive: true, mode: 448 });
1523
+ writeFileSync6(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1265
1524
  }
1266
1525
  };
1267
1526
 
1268
1527
  // src/lib/execution-journal.ts
1269
- import { mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, renameSync } from "fs";
1528
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, renameSync } from "fs";
1270
1529
  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");
1530
+ import { dirname as dirname7, join as join8 } from "path";
1531
+ import { createHash as createHash3 } from "crypto";
1532
+ var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join8(homedir5(), ".suveren");
1274
1533
  var FILE_NAME = "execution-journal.json";
1275
1534
  var MAX_AGE_SECONDS2 = 90 * 24 * 60 * 60;
1276
1535
  function hashToolArgs(args) {
1277
- return "sha256:" + createHash2("sha256").update(stableStringify(args)).digest("hex");
1536
+ return "sha256:" + createHash3("sha256").update(stableStringify(args)).digest("hex");
1278
1537
  }
1279
1538
  function stableStringify(value) {
1280
1539
  if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "undefined";
@@ -1286,8 +1545,8 @@ function stableStringify(value) {
1286
1545
  var ExecutionJournal = class {
1287
1546
  filePath;
1288
1547
  constructor(filePath) {
1289
- const baseDir = filePath && filePath.endsWith(".json") ? dirname6(filePath) : filePath ?? DEFAULT_DIR5;
1290
- this.filePath = join7(baseDir, FILE_NAME);
1548
+ const baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR5;
1549
+ this.filePath = join8(baseDir, FILE_NAME);
1291
1550
  }
1292
1551
  /** Look a ticket up. Always reads from disk — another process may have written. */
1293
1552
  get(ticketId) {
@@ -1326,9 +1585,9 @@ var ExecutionJournal = class {
1326
1585
  }
1327
1586
  // ─── Persistence ────────────────────────────────────────────────────────
1328
1587
  load() {
1329
- if (!existsSync7(this.filePath)) return { version: 1, entries: [] };
1588
+ if (!existsSync8(this.filePath)) return { version: 1, entries: [] };
1330
1589
  try {
1331
- const parsed = JSON.parse(readFileSync7(this.filePath, "utf8"));
1590
+ const parsed = JSON.parse(readFileSync9(this.filePath, "utf8"));
1332
1591
  if (parsed?.version !== 1 || !Array.isArray(parsed.entries)) {
1333
1592
  throw new Error("unexpected shape");
1334
1593
  }
@@ -1342,9 +1601,9 @@ var ExecutionJournal = class {
1342
1601
  persist(file) {
1343
1602
  const cutoff = nowSec() - MAX_AGE_SECONDS2;
1344
1603
  file.entries = file.entries.filter((e) => e.startedAt >= cutoff);
1345
- mkdirSync6(dirname6(this.filePath), { recursive: true, mode: 448 });
1604
+ mkdirSync7(dirname7(this.filePath), { recursive: true, mode: 448 });
1346
1605
  const tmp = this.filePath + ".tmp";
1347
- writeFileSync6(tmp, JSON.stringify(file, null, 2), { mode: 384 });
1606
+ writeFileSync7(tmp, JSON.stringify(file, null, 2), { mode: 384 });
1348
1607
  renameSync(tmp, this.filePath);
1349
1608
  }
1350
1609
  };
@@ -1353,10 +1612,10 @@ function nowSec() {
1353
1612
  }
1354
1613
 
1355
1614
  // 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";
1615
+ import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, renameSync as renameSync2 } from "fs";
1357
1616
  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");
1617
+ import { dirname as dirname8, join as join9 } from "path";
1618
+ var DEFAULT_DIR6 = process.env.SUVEREN_DATA_DIR ?? join9(homedir6(), ".suveren");
1360
1619
  var FILE_NAME2 = "proposal-submissions.json";
1361
1620
  var MAX_AGE_SECONDS3 = 90 * 24 * 60 * 60;
1362
1621
  function nowSec2() {
@@ -1365,8 +1624,8 @@ function nowSec2() {
1365
1624
  var ProposalSubmissionStore = class {
1366
1625
  filePath;
1367
1626
  constructor(filePath) {
1368
- const baseDir = filePath && filePath.endsWith(".json") ? dirname7(filePath) : filePath ?? DEFAULT_DIR6;
1369
- this.filePath = join8(baseDir, FILE_NAME2);
1627
+ const baseDir = filePath && filePath.endsWith(".json") ? dirname8(filePath) : filePath ?? DEFAULT_DIR6;
1628
+ this.filePath = join9(baseDir, FILE_NAME2);
1370
1629
  }
1371
1630
  /** Record what was submitted, right after a successful submitProposal(). */
1372
1631
  record(entry) {
@@ -1389,9 +1648,9 @@ var ProposalSubmissionStore = class {
1389
1648
  return this.load().entries.find((e) => e.proposalId === proposalId);
1390
1649
  }
1391
1650
  load() {
1392
- if (!existsSync8(this.filePath)) return { version: 1, entries: [] };
1651
+ if (!existsSync9(this.filePath)) return { version: 1, entries: [] };
1393
1652
  try {
1394
- const parsed = JSON.parse(readFileSync8(this.filePath, "utf-8"));
1653
+ const parsed = JSON.parse(readFileSync10(this.filePath, "utf-8"));
1395
1654
  const cutoff = nowSec2() - MAX_AGE_SECONDS3;
1396
1655
  parsed.entries = (parsed.entries ?? []).filter((e) => e.submittedAt >= cutoff);
1397
1656
  return parsed;
@@ -1400,9 +1659,9 @@ var ProposalSubmissionStore = class {
1400
1659
  }
1401
1660
  }
1402
1661
  persist(file) {
1403
- mkdirSync7(dirname7(this.filePath), { recursive: true, mode: 448 });
1662
+ mkdirSync8(dirname8(this.filePath), { recursive: true, mode: 448 });
1404
1663
  const tmpPath = `${this.filePath}.tmp`;
1405
- writeFileSync7(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
1664
+ writeFileSync8(tmpPath, JSON.stringify(file, null, 2), { encoding: "utf-8", mode: 384 });
1406
1665
  renameSync2(tmpPath, this.filePath);
1407
1666
  }
1408
1667
  };
@@ -1514,12 +1773,13 @@ var SharedState = class {
1514
1773
  gatekeeper;
1515
1774
  /**
1516
1775
  * @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.
1776
+ * pinning (as-pairing.ts), and to the SPClient so it can enforce opt-in
1777
+ * TLS pinning (as-tls-pin.ts). Optional for backward compat with
1778
+ * existing constructions/tests that pass only a URL — those get
1779
+ * unpinned trust-on-first-use behavior, same as before pinning existed.
1520
1780
  */
1521
1781
  constructor(spUrl2, gateStorePath, dataDir2) {
1522
- this.spClient = new SPClient(spUrl2);
1782
+ this.spClient = new SPClient(spUrl2, void 0, dataDir2);
1523
1783
  this.cache = new AttestationCache(this.spClient, dataDir2);
1524
1784
  this.gateStore = new GateStore(gateStorePath);
1525
1785
  this.executionLog = new ExecutionLog(gateStorePath);
@@ -1679,17 +1939,17 @@ function formatConsumptionFull(entries) {
1679
1939
  }
1680
1940
 
1681
1941
  // src/lib/context-loader.ts
1682
- import { readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
1942
+ import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
1683
1943
  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");
1944
+ import { join as join10 } from "path";
1945
+ var DEFAULT_DIR7 = process.env.SUVEREN_DATA_DIR ?? join10(homedir7(), ".suveren");
1686
1946
  var BRIEF_MAX_CHARS = 16 * 1024;
1687
1947
  function readContextFile(dataDir2) {
1688
1948
  const dir = dataDir2 ?? DEFAULT_DIR7;
1689
- const filePath = join9(dir, "context.md");
1690
- if (!existsSync9(filePath)) return null;
1949
+ const filePath = join10(dir, "context.md");
1950
+ if (!existsSync10(filePath)) return null;
1691
1951
  try {
1692
- const content = readFileSync9(filePath, "utf-8").trim();
1952
+ const content = readFileSync11(filePath, "utf-8").trim();
1693
1953
  return content || null;
1694
1954
  } catch {
1695
1955
  return null;
@@ -1809,6 +2069,11 @@ function asKeyMismatchNotice(what) {
1809
2069
 
1810
2070
  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
2071
  }
2072
+ function asTlsMismatchNotice(what) {
2073
+ 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.
2074
+
2075
+ 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.`;
2076
+ }
1812
2077
  function asUrlChangedNotice(what) {
1813
2078
  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
2079
 
@@ -1821,6 +2086,8 @@ function lockedNotice(action, reason = "restart") {
1821
2086
  return expiredSessionNotice(what);
1822
2087
  case "as-key-mismatch":
1823
2088
  return asKeyMismatchNotice(what);
2089
+ case "as-tls-mismatch":
2090
+ return asTlsMismatchNotice(what);
1824
2091
  case "as-url-changed":
1825
2092
  return asUrlChangedNotice(what);
1826
2093
  default:
@@ -1989,6 +2256,11 @@ async function verifyTicket(cache, receipt, expected) {
1989
2256
  `Ticket proposalId "${String(receipt.proposalId)}" does not match the proposal being executed "${expected.proposalId}" \u2014 refusing to execute.`
1990
2257
  );
1991
2258
  }
2259
+ if (expected.idempotencyKey !== void 0 && receipt.idempotencyKey !== expected.idempotencyKey) {
2260
+ throw new TicketBindingMismatchError(
2261
+ `Ticket idempotencyKey "${String(receipt.idempotencyKey)}" does not match the one this call sent "${expected.idempotencyKey}" \u2014 refusing to execute.`
2262
+ );
2263
+ }
1992
2264
  if (expected.contentHash !== void 0) {
1993
2265
  if (receipt.contentHash !== expected.contentHash) {
1994
2266
  throw new TicketBindingMismatchError(
@@ -2766,6 +3038,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
2766
3038
  try {
2767
3039
  const actionType = declaredActionType;
2768
3040
  const binding = computeContentBinding(auth.profileId, tool, args, actionType);
3041
+ const idempotencyKey = randomUUID();
2769
3042
  const { receipt } = await state2.spClient.postReceipt({
2770
3043
  authorizationId: authzId,
2771
3044
  // Optional cross-check — the AS fails closed on a mismatch.
@@ -2775,7 +3048,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
2775
3048
  actionType,
2776
3049
  executionContext: { ...execution },
2777
3050
  amount: typeof execution.amount === "number" ? execution.amount : void 0,
2778
- idempotencyKey: randomUUID(),
3051
+ idempotencyKey,
2779
3052
  // Privacy: send the hash and how to reproduce it — never the
2780
3053
  // preimage. `binding` also carries `boundContent`, the plaintext
2781
3054
  // that was hashed (an email's to/cc/subject/body); it stays on
@@ -2789,6 +3062,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
2789
3062
  executionContext: { ...execution },
2790
3063
  authorizationId: authzId,
2791
3064
  profileId: auth.profileId,
3065
+ idempotencyKey,
2792
3066
  contentHash: binding?.contentHash,
2793
3067
  contentBinding: binding?.contentBinding
2794
3068
  });
@@ -3750,16 +4024,16 @@ function verifyGateContentHashes(content, auth) {
3750
4024
  }
3751
4025
 
3752
4026
  // src/lib/integration-registry.ts
3753
- import { mkdirSync as mkdirSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync10 } from "fs";
4027
+ import { mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9, existsSync as existsSync11 } from "fs";
3754
4028
  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");
4029
+ import { dirname as dirname9, join as join11 } from "path";
4030
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join11(homedir8(), ".suveren");
3757
4031
  var IntegrationRegistry = class {
3758
4032
  integrations = /* @__PURE__ */ new Map();
3759
4033
  filePath;
3760
4034
  constructor(dataDir2) {
3761
4035
  const dir = dataDir2 ?? DEFAULT_DATA_DIR;
3762
- this.filePath = join10(dir, "integrations.json");
4036
+ this.filePath = join11(dir, "integrations.json");
3763
4037
  this.load();
3764
4038
  }
3765
4039
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -3790,12 +4064,12 @@ var IntegrationRegistry = class {
3790
4064
  }
3791
4065
  // ─── Load / Save ────────────────────────────────────────────────────────
3792
4066
  load() {
3793
- if (!existsSync10(this.filePath)) {
3794
- mkdirSync8(dirname8(this.filePath), { recursive: true });
4067
+ if (!existsSync11(this.filePath)) {
4068
+ mkdirSync9(dirname9(this.filePath), { recursive: true });
3795
4069
  return;
3796
4070
  }
3797
4071
  try {
3798
- const raw = readFileSync10(this.filePath, "utf-8");
4072
+ const raw = readFileSync12(this.filePath, "utf-8");
3799
4073
  const data = JSON.parse(raw);
3800
4074
  const migrated = data.integrations.map((i) => {
3801
4075
  if ("toolGating" in i && !("profile" in i)) {
@@ -3824,45 +4098,45 @@ var IntegrationRegistry = class {
3824
4098
  version: 1,
3825
4099
  integrations: Array.from(this.integrations.values())
3826
4100
  };
3827
- mkdirSync8(dirname8(this.filePath), { recursive: true });
3828
- writeFileSync8(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
4101
+ mkdirSync9(dirname9(this.filePath), { recursive: true });
4102
+ writeFileSync9(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
3829
4103
  }
3830
4104
  };
3831
4105
 
3832
4106
  // src/lib/integration-manager.ts
3833
4107
  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";
4108
+ import { join as join13, delimiter } from "path";
4109
+ import { existsSync as existsSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync } from "fs";
3836
4110
  import { execFile } from "child_process";
3837
4111
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3838
4112
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3839
4113
  import { getProfile as getProfile5 } from "@hap/core";
3840
4114
 
3841
4115
  // src/lib/manifest-loader.ts
3842
- import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
3843
- import { join as join11, resolve } from "path";
4116
+ import { readFileSync as readFileSync13, existsSync as existsSync12 } from "fs";
4117
+ import { join as join12, resolve } from "path";
3844
4118
  var manifests = /* @__PURE__ */ new Map();
3845
4119
  function loadManifests(integrationsDir) {
3846
4120
  const dir = resolve(
3847
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join11(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
4121
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join12(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3848
4122
  );
3849
- const indexPath = join11(dir, "index.json");
3850
- if (!existsSync11(indexPath)) {
4123
+ const indexPath = join12(dir, "index.json");
4124
+ if (!existsSync12(indexPath)) {
3851
4125
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
3852
4126
  return 0;
3853
4127
  }
3854
4128
  let index;
3855
4129
  try {
3856
- index = JSON.parse(readFileSync11(indexPath, "utf-8"));
4130
+ index = JSON.parse(readFileSync13(indexPath, "utf-8"));
3857
4131
  } catch (err) {
3858
4132
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
3859
4133
  return 0;
3860
4134
  }
3861
4135
  let loaded = 0;
3862
4136
  for (const [id, relativePath] of Object.entries(index.integrations)) {
3863
- const manifestPath = join11(dir, relativePath);
4137
+ const manifestPath = join12(dir, relativePath);
3864
4138
  try {
3865
- const manifest = JSON.parse(readFileSync11(manifestPath, "utf-8"));
4139
+ const manifest = JSON.parse(readFileSync13(manifestPath, "utf-8"));
3866
4140
  manifests.set(id, manifest);
3867
4141
  loaded++;
3868
4142
  } catch (err) {
@@ -3950,26 +4224,26 @@ function safeHost(url) {
3950
4224
  }
3951
4225
 
3952
4226
  // 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");
4227
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join13(homedir9(), ".suveren");
4228
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join13(DEFAULT_DATA_DIR2, "integrations");
3955
4229
  var installLock = Promise.resolve();
3956
4230
  function withInstallLock(task) {
3957
4231
  const run = installLock.then(task, task);
3958
4232
  installLock = run.then(() => void 0, () => void 0);
3959
4233
  return run;
3960
4234
  }
3961
- var INTEGRATIONS_BIN = join12(INTEGRATIONS_DIR, "node_modules", ".bin");
4235
+ var INTEGRATIONS_BIN = join13(INTEGRATIONS_DIR, "node_modules", ".bin");
3962
4236
  function buildPath() {
3963
4237
  const base = process.env.PATH ?? "";
3964
4238
  return [INTEGRATIONS_BIN, base].join(delimiter);
3965
4239
  }
3966
4240
  function ensureIntegrationsDir() {
3967
- if (!existsSync12(INTEGRATIONS_DIR)) {
3968
- mkdirSync9(INTEGRATIONS_DIR, { recursive: true });
4241
+ if (!existsSync13(INTEGRATIONS_DIR)) {
4242
+ mkdirSync10(INTEGRATIONS_DIR, { recursive: true });
3969
4243
  }
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));
4244
+ const pkgPath = join13(INTEGRATIONS_DIR, "package.json");
4245
+ if (!existsSync13(pkgPath)) {
4246
+ writeFileSync10(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
3973
4247
  }
3974
4248
  }
3975
4249
  function readAgeOf(config) {
@@ -4035,19 +4309,19 @@ var IntegrationManager = class {
4035
4309
  * exist, and treat anything else as not-installed (and reinstallable).
4036
4310
  */
4037
4311
  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;
4312
+ const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4313
+ const pkgJson = join13(pkgDir, "package.json");
4314
+ if (!existsSync13(pkgDir) || !existsSync13(pkgJson)) return false;
4041
4315
  try {
4042
- const pkg = JSON.parse(readFileSync12(pkgJson, "utf8"));
4316
+ const pkg = JSON.parse(readFileSync14(pkgJson, "utf8"));
4043
4317
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
4044
4318
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
4045
4319
  const binEntries = Object.entries(binMap);
4046
4320
  for (const [binName, target] of binEntries) {
4047
- if (!existsSync12(join12(pkgDir, target))) return false;
4321
+ if (!existsSync13(join13(pkgDir, target))) return false;
4048
4322
  if (!this.binShimExists(binName)) return false;
4049
4323
  }
4050
- if (binEntries.length === 0 && pkg.main && !existsSync12(join12(pkgDir, pkg.main))) return false;
4324
+ if (binEntries.length === 0 && pkg.main && !existsSync13(join13(pkgDir, pkg.main))) return false;
4051
4325
  return true;
4052
4326
  } catch {
4053
4327
  return false;
@@ -4061,7 +4335,7 @@ var IntegrationManager = class {
4061
4335
  binShimExists(binName) {
4062
4336
  if (!binName) return false;
4063
4337
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
4064
- return candidates.some((c) => existsSync12(join12(INTEGRATIONS_BIN, c)));
4338
+ return candidates.some((c) => existsSync13(join13(INTEGRATIONS_BIN, c)));
4065
4339
  }
4066
4340
  /**
4067
4341
  * Install an npm package into the managed integrations directory if not
@@ -4085,8 +4359,8 @@ var IntegrationManager = class {
4085
4359
  }
4086
4360
  /** The actual install. Callers MUST hold the install lock. */
4087
4361
  async installNow(npmPackage) {
4088
- const pkgDir = join12(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4089
- if (existsSync12(pkgDir)) {
4362
+ const pkgDir = join13(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
4363
+ if (existsSync13(pkgDir)) {
4090
4364
  console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
4091
4365
  try {
4092
4366
  rmSync(pkgDir, { recursive: true, force: true });
@@ -4521,28 +4795,28 @@ var IntegrationManager = class {
4521
4795
  };
4522
4796
 
4523
4797
  // 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";
4798
+ import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
4799
+ import { join as join14, resolve as resolve2 } from "path";
4526
4800
  import { registerProfile } from "@hap/core";
4527
4801
  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)) {
4802
+ const dir = resolve2(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join14(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
4803
+ const indexPath = join14(dir, "index.json");
4804
+ if (!existsSync14(indexPath)) {
4531
4805
  warnNoProfiles(`No index.json at ${indexPath}`);
4532
4806
  return 0;
4533
4807
  }
4534
4808
  let index;
4535
4809
  try {
4536
- index = JSON.parse(readFileSync13(indexPath, "utf-8"));
4810
+ index = JSON.parse(readFileSync15(indexPath, "utf-8"));
4537
4811
  } catch (err) {
4538
4812
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
4539
4813
  return 0;
4540
4814
  }
4541
4815
  let loaded = 0;
4542
4816
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
4543
- const profilePath = join13(dir, relativePath);
4817
+ const profilePath = join14(dir, relativePath);
4544
4818
  try {
4545
- const profile = JSON.parse(readFileSync13(profilePath, "utf-8"));
4819
+ const profile = JSON.parse(readFileSync15(profilePath, "utf-8"));
4546
4820
  registerProfile(profileId, profile);
4547
4821
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
4548
4822
  if (shortName && shortName !== profileId) {
@@ -4578,8 +4852,8 @@ function warnNoProfiles(reason) {
4578
4852
  import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
4579
4853
 
4580
4854
  // 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";
4855
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync16, unlinkSync as unlinkSync6, writeFileSync as writeFileSync11 } from "fs";
4856
+ import { dirname as dirname10, join as join15 } from "path";
4583
4857
  var CommittedExecutor = class {
4584
4858
  constructor(run) {
4585
4859
  this.run = run;
@@ -4607,7 +4881,7 @@ var ExecutorLock = class {
4607
4881
  path;
4608
4882
  held = false;
4609
4883
  constructor(dataDir2) {
4610
- this.path = join14(dataDir2, "executor.lock");
4884
+ this.path = join15(dataDir2, "executor.lock");
4611
4885
  }
4612
4886
  /** Try to take the lock. Returns the holder's pid when someone else has it. */
4613
4887
  acquire() {
@@ -4615,9 +4889,9 @@ var ExecutorLock = class {
4615
4889
  if (current && current.pid !== process.pid && isAlive(current.pid)) {
4616
4890
  return { held: false, holderPid: current.pid };
4617
4891
  }
4618
- mkdirSync10(dirname9(this.path), { recursive: true, mode: 448 });
4892
+ mkdirSync11(dirname10(this.path), { recursive: true, mode: 448 });
4619
4893
  const lock = { pid: process.pid, startedAt: Math.floor(Date.now() / 1e3) };
4620
- writeFileSync10(this.path, JSON.stringify(lock), { mode: 384 });
4894
+ writeFileSync11(this.path, JSON.stringify(lock), { mode: 384 });
4621
4895
  this.held = true;
4622
4896
  return { held: true };
4623
4897
  }
@@ -4639,9 +4913,9 @@ var ExecutorLock = class {
4639
4913
  }
4640
4914
  }
4641
4915
  read() {
4642
- if (!existsSync14(this.path)) return null;
4916
+ if (!existsSync15(this.path)) return null;
4643
4917
  try {
4644
- const parsed = JSON.parse(readFileSync14(this.path, "utf8"));
4918
+ const parsed = JSON.parse(readFileSync16(this.path, "utf8"));
4645
4919
  return typeof parsed?.pid === "number" ? parsed : null;
4646
4920
  } catch {
4647
4921
  return null;
@@ -4661,76 +4935,8 @@ function isAlive(pid) {
4661
4935
  import { homedir as homedir10 } from "os";
4662
4936
  import { join as join17 } from "path";
4663
4937
 
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
4938
  // src/lib/mcp-as-tracker.ts
4733
- import { existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as readFileSync16, writeFileSync as writeFileSync12 } from "fs";
4939
+ import { existsSync as existsSync16, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "fs";
4734
4940
  import { dirname as dirname11, join as join16 } from "path";
4735
4941
  function trackerPath(dataDir2) {
4736
4942
  return join16(dataDir2, "mcp-as-pairing.json");
@@ -4739,7 +4945,7 @@ function readMcpPairedAsUrl(dataDir2) {
4739
4945
  const path = trackerPath(dataDir2);
4740
4946
  if (!existsSync16(path)) return null;
4741
4947
  try {
4742
- const data = JSON.parse(readFileSync16(path, "utf-8"));
4948
+ const data = JSON.parse(readFileSync17(path, "utf-8"));
4743
4949
  return typeof data.asUrl === "string" ? data.asUrl : null;
4744
4950
  } catch {
4745
4951
  return null;
@@ -4754,6 +4960,7 @@ function writeMcpPairedAsUrl(dataDir2, asUrl) {
4754
4960
  // bin/http.ts
4755
4961
  var dataDir = process.env.SUVEREN_DATA_DIR ?? join17(homedir10(), ".suveren");
4756
4962
  var spUrl = resolveAsUrl(dataDir);
4963
+ validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
4757
4964
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
4758
4965
  setAsBaseUrl(spUrl);
4759
4966
  var state = new SharedState(spUrl, void 0, dataDir);
@@ -4838,7 +5045,7 @@ app.post("/internal/configure", internalOnly, (req, res) => {
4838
5045
  });
4839
5046
  app.post("/internal/clear-session", internalOnly, (req, res) => {
4840
5047
  const reason = req.body?.reason;
4841
- const validReason = reason === "as-key-mismatch" || reason === "as-url-changed" ? reason : void 0;
5048
+ const validReason = reason === "as-key-mismatch" || reason === "as-tls-mismatch" || reason === "as-url-changed" ? reason : void 0;
4842
5049
  state.spClient.clearSession(validReason);
4843
5050
  console.error(`[Suveren MCP] Session cleared by control-plane (${validReason ?? "expired"})`);
4844
5051
  res.json({ ok: true });