@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
package/README.md CHANGED
@@ -106,7 +106,7 @@ Open `http://localhost:7400`. The MCP server is at `http://localhost:7430`.
106
106
 
107
107
  ### Option B — npm
108
108
 
109
- Requires [Node.js 20+](https://nodejs.org/).
109
+ Requires [Node.js 20.18.1+](https://nodejs.org/).
110
110
 
111
111
  ```bash
112
112
  npm install -g @suveren/gateway
@@ -153,6 +153,38 @@ See [`docs/development.md`](docs/development.md) for environment variables, test
153
153
 
154
154
  ---
155
155
 
156
+ ## Pinning the Authority Server's TLS certificate
157
+
158
+ Opt-in, for self-hosted Authority Servers with a stable signing key — not needed against the hosted `suveren.ai`, and off by default.
159
+
160
+ The gateway already verifies the Authority Server can *sign* under the right key before it ever sends an API key or session cookie. What that alone does not cover: something sitting on the network path between the gateway and the Authority Server, presenting its own TLS certificate, that a locally-trusted CA (e.g. a custom `--ca-file`) would otherwise accept. TLS certificate pinning closes that gap by pinning the Authority Server's certificate public key (SPKI, SHA-256) — every connection after that must present the same key, or the gateway refuses it and locks.
161
+
162
+ **Pinning only protects from the moment the fingerprint has actually been checked over a second channel** — a phone call, a video call, a separate trusted connection. That is why enabling it requires `--expect-fingerprint`: not an optional confirmation step, but the check itself. Recommended: enable it once, right after pairing, from a network you already trust.
163
+
164
+ ```bash
165
+ # Get the Authority Server's own fingerprint independently — ask the operator,
166
+ # or run this yourself on a trusted network:
167
+ openssl s_client -connect your-as-host:443 </dev/null 2>/dev/null \
168
+ | openssl x509 -pubkey -noout \
169
+ | openssl pkey -pubin -outform der \
170
+ | openssl dgst -sha256
171
+
172
+ # Compare it against what you were told out-of-band, THEN enable:
173
+ suveren-gateway config set pin-tls on --expect-fingerprint <the-sha256-you-just-confirmed>
174
+ suveren-gateway restart
175
+ ```
176
+
177
+ A few things worth knowing before turning it on:
178
+
179
+ - **Certificate renewal with the SAME key keeps the pin working** — nothing to do. Renew with the same key where your tooling supports it (e.g. `certbot renew --reuse-key`).
180
+ - **A renewal under a NEW key locks the gateway** (`as-tls-mismatch`) until an operator re-pairs — this is deliberate fail-closed behavior, not a bug. Re-pairing means clearing `<dataDir>/as-pairing.json` and signing in again.
181
+ - Pinned connections trust Node's own bundled root certificates plus `--ca-file` / `NODE_EXTRA_CA_CERTS` — **not** your operating system's trust store.
182
+ - `suveren-gateway config set pin-tls off` disables enforcement at any time; the stored fingerprint stays on file, so turning it back on later with the SAME `--expect-fingerprint` value succeeds immediately (it is still required every time — stating it again is cheap, and the command refuses outright if it doesn't match what's on file).
183
+
184
+ See `suveren-gateway config help` for the full command reference.
185
+
186
+ ---
187
+
156
188
  ## Technical Documentation
157
189
 
158
190
  | Document | Contents |
@@ -17,7 +17,8 @@ import { homedir, platform, userInfo } from 'node:os';
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { buildLaunchAgentPlist, buildMacLauncher, buildSystemdUnit, buildWindowsTaskXml } from '../lib/autostart-templates.mjs';
20
- import { DEFAULT_AS_URL, readConfig, writeConfig, validateAsUrl, validateCaFile, resolveAsUrl } from '../lib/config.mjs';
20
+ import { DEFAULT_AS_URL, readConfig, writeConfig, validateAsUrl, validateCaFile, validatePinTls, resolveAsUrl, resolvePinTls, resolvePinTlsExpectedFingerprint } from '../lib/config.mjs';
21
+ import { readPairing as readAsPairing, recordTlsPin, formatFingerprint, normalizeFingerprint } from '../lib/as-pairing.mjs';
21
22
 
22
23
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
24
  const PKG_ROOT = resolve(__dirname, '..');
@@ -61,6 +62,101 @@ function flagValue(args, name) {
61
62
  return args[i + 1];
62
63
  }
63
64
 
65
+ /**
66
+ * The TLS SPKI fingerprint currently on file for `asUrl`, if any — either a
67
+ * real, verified pin (as-pairing.json, captured at a past sign-in) or a
68
+ * staged-but-not-yet-verified one (config.json's `pinTlsExpectedFingerprint`
69
+ * — set by a PRIOR `--expect-fingerprint` run before any pairing existed).
70
+ * The pairing value always wins when both are present.
71
+ */
72
+ function currentTlsFingerprintHex(dataDir, asUrl) {
73
+ const pairing = readAsPairing(dataDir);
74
+ if (pairing && pairing.asUrl === asUrl && pairing.tlsSpkiPinHex) return pairing.tlsSpkiPinHex;
75
+ return resolvePinTlsExpectedFingerprint(dataDir);
76
+ }
77
+
78
+ function printFingerprintHowTo(asUrl) {
79
+ let host = asUrl;
80
+ try { host = new URL(asUrl).host; } catch { /* keep the raw string */ }
81
+ console.error(`Obtain the Authority Server's OWN fingerprint independently — e.g. ask the operator over`);
82
+ console.error(`the phone, or run this yourself on a network you already trust:`);
83
+ console.error(``);
84
+ console.error(` openssl s_client -connect ${host} </dev/null 2>/dev/null \\`);
85
+ console.error(` | openssl x509 -pubkey -noout \\`);
86
+ console.error(` | openssl pkey -pubin -outform der \\`);
87
+ console.error(` | openssl dgst -sha256`);
88
+ console.error(``);
89
+ console.error(`Compare it against the value shown above (if any) BEFORE re-running this with --expect-fingerprint.`);
90
+ }
91
+
92
+ /**
93
+ * Mandatory out-of-band check for enabling pin-tls (opt-in TLS certificate
94
+ * pinning) — shared by `start --pin-tls` and `config set pin-tls on`.
95
+ * Without this, turning pin-tls on would either silently trust whatever
96
+ * certificate the Authority Server happens to present at the next sign-in
97
+ * (defeating the whole point of pinning) or require a SEPARATE manual
98
+ * verification step a script could skip. `--expect-fingerprint` makes the
99
+ * operator state, on the command line, the value they already checked
100
+ * out-of-band — refusing outright when it's missing or doesn't match
101
+ * whatever is already on file.
102
+ *
103
+ * Returns the normalized (lowercase, no separators) hex fingerprint to
104
+ * commit, or exits the process (code 1) having already printed why.
105
+ */
106
+ function requireConfirmedFingerprint(args, asUrl) {
107
+ const flag = flagValue(args, '--expect-fingerprint');
108
+ const existingHex = currentTlsFingerprintHex(DATA_DIR, asUrl);
109
+
110
+ if (!flag) {
111
+ console.error(`--expect-fingerprint is required to enable pin-tls — this is the Authority Server's TLS`);
112
+ console.error(`certificate public-key fingerprint (SHA-256 of the leaf SPKI), checked over a second`);
113
+ console.error(`channel BEFORE trusting it, not just whatever this gateway happens to see on the wire.`);
114
+ console.error(``);
115
+ if (existingHex) {
116
+ console.error(`Currently on file for ${asUrl}:`);
117
+ console.error(` ${formatFingerprint(existingHex)}`);
118
+ console.error(``);
119
+ }
120
+ printFingerprintHowTo(asUrl);
121
+ process.exit(1);
122
+ }
123
+
124
+ const norm = normalizeFingerprint(flag);
125
+ if (!norm.ok) {
126
+ console.error(`Invalid --expect-fingerprint: ${norm.error}`);
127
+ process.exit(1);
128
+ }
129
+
130
+ if (existingHex && existingHex !== norm.hex) {
131
+ console.error(`--expect-fingerprint does not match the fingerprint already on file for ${asUrl}:`);
132
+ console.error(` on file: ${formatFingerprint(existingHex)}`);
133
+ console.error(` provided: ${formatFingerprint(norm.hex)}`);
134
+ console.error(``);
135
+ console.error(`If the Authority Server's certificate changed INTENTIONALLY (a new key, not just a`);
136
+ console.error(`renewal), clear the pairing (${join(DATA_DIR, 'as-pairing.json')}) and sign in again to`);
137
+ console.error(`re-pin it, then re-run this command with the new fingerprint.`);
138
+ process.exit(1);
139
+ }
140
+
141
+ return norm.hex;
142
+ }
143
+
144
+ /**
145
+ * Commit a confirmed fingerprint: into the existing pairing record
146
+ * (as-pairing.json) when one exists for this URL, so it is enforced from
147
+ * the very next connection on — or, when no pairing exists yet (nothing to
148
+ * attach a TLS pin to), staged in config.json's `pinTlsExpectedFingerprint`
149
+ * for the control plane to enforce at the NEXT sign-in's challenge (see
150
+ * checkAsKeyBeforeLogin in apps/control-plane/src/routes/auth.ts), which
151
+ * moves it into as-pairing.json and clears the staging field once it
152
+ * verifies.
153
+ */
154
+ function commitConfirmedFingerprint(dataDir, asUrl, hex) {
155
+ if (recordTlsPin(dataDir, asUrl, hex)) return 'pinned';
156
+ writeConfig(dataDir, { pinTlsExpectedFingerprint: hex });
157
+ return 'staged';
158
+ }
159
+
64
160
  async function start(args) {
65
161
  const detach = args.includes('--detach') || args.includes('-d');
66
162
 
@@ -93,6 +189,27 @@ async function start(args) {
93
189
  caFileToSave = v.path;
94
190
  }
95
191
 
192
+ // --pin-tls: a boolean flag (turns TLS certificate pinning ON — there is
193
+ // no `start --pin-tls off`; use `config set pin-tls off` for that).
194
+ // Validated against the EFFECTIVE as-url for this very start (the flag
195
+ // above, if given, wins over whatever is saved) — enabling it for an
196
+ // http:// Authority Server has nothing to pin. Also requires
197
+ // --expect-fingerprint (the mandatory out-of-band check — see
198
+ // requireConfirmedFingerprint) before it can be turned on at all.
199
+ const pinTlsFlag = args.includes('--pin-tls');
200
+ let pinTlsToSave = null;
201
+ let confirmedFingerprintHex = null;
202
+ if (pinTlsFlag) {
203
+ const effectiveAsUrl = asUrlToSave ?? resolveAsUrl(DATA_DIR);
204
+ const v = validatePinTls(effectiveAsUrl);
205
+ if (!v.ok) {
206
+ console.error(`Invalid --pin-tls: ${v.error}`);
207
+ process.exit(1);
208
+ }
209
+ confirmedFingerprintHex = requireConfirmedFingerprint(args, effectiveAsUrl);
210
+ pinTlsToSave = true;
211
+ }
212
+
96
213
  if (await isAlreadyRunning()) {
97
214
  console.error(`suveren-gateway is already running (pid ${readPid()}). Use \`suveren-gateway stop\` first or \`suveren-gateway restart\`.`);
98
215
  process.exit(1);
@@ -141,6 +258,17 @@ async function start(args) {
141
258
  writeConfig(DATA_DIR, { caFile: caFileToSave });
142
259
  console.log(`CA file: ${caFileToSave} (saved — applied to every process this gateway starts)`);
143
260
  }
261
+ if (pinTlsToSave) {
262
+ const effectiveAsUrl = asUrlToSave ?? resolveAsUrl(DATA_DIR);
263
+ const committed = commitConfirmedFingerprint(DATA_DIR, effectiveAsUrl, confirmedFingerprintHex);
264
+ writeConfig(DATA_DIR, { pinTls: true });
265
+ console.log(`TLS pinning: ON (fingerprint ${formatFingerprint(confirmedFingerprintHex)} confirmed)`);
266
+ console.log(
267
+ committed === 'pinned'
268
+ ? ` Enforced from your next connection on.`
269
+ : ` Staged — the first sign-in's challenge must match it (refused otherwise); it then becomes the permanent pin.`,
270
+ );
271
+ }
144
272
 
145
273
  if (detach) {
146
274
  const out = openSync(LOG_FILE, 'a');
@@ -368,14 +496,46 @@ function printConfigHelp() {
368
496
  console.log(`suveren-gateway config — read or save gateway settings
369
497
 
370
498
  Usage:
371
- suveren-gateway config get [as-url|ca-file] Print the resolved value(s)
372
- suveren-gateway config set as-url <url> Save the Authority Server URL
373
- suveren-gateway config set ca-file <path> Save a CA bundle for internal TLS
499
+ suveren-gateway config get [as-url|ca-file|pin-tls] Print the resolved value(s)
500
+ suveren-gateway config set as-url <url> Save the Authority Server URL
501
+ suveren-gateway config set ca-file <path> Save a CA bundle for internal TLS
502
+ suveren-gateway config set pin-tls on --expect-fingerprint <sha256-hex>
503
+ Pin the Authority Server's TLS certificate
504
+ suveren-gateway config set pin-tls off Stop enforcing the TLS certificate pin
374
505
 
375
506
  Saved in ${join(DATA_DIR, 'config.json')}.
376
507
  Precedence at start: --as-url flag > env SUVEREN_AS_URL > saved as-url >
377
508
  default (${DEFAULT_AS_URL}).
378
509
 
510
+ pin-tls (default off, self-hosted Authority Servers with a stable signing
511
+ key) requires https:// and pins the Authority Server's TLS certificate
512
+ public key; every connection after it's established must present that same
513
+ key, or the gateway refuses it and locks (reason: as-tls-mismatch) until an
514
+ operator resolves it. It protects against a party on the network between
515
+ this gateway and the Authority Server presenting its OWN certificate — but
516
+ ONLY from the moment the fingerprint has actually been checked over a
517
+ second channel (phone, a separate trusted connection), which is why
518
+ --expect-fingerprint is mandatory: it is not optional confirmation, it IS
519
+ the check. Recommended: enable it once, right after pairing, from a network
520
+ you already trust.
521
+
522
+ --expect-fingerprint <sha256-hex> The Authority Server's own TLS leaf
523
+ certificate public-key fingerprint (SHA-256 of the SPKI — colons/spacing
524
+ accepted, case-insensitive), confirmed over a second channel BEFORE
525
+ running this command. Without it, this command refuses and prints how to
526
+ obtain the fingerprint yourself.
527
+ - If a pin is already on file and it DIFFERS, this refuses outright —
528
+ clear the pairing first if the certificate changed intentionally (a
529
+ NEW key; a renewal with the SAME key never needs this).
530
+ - If none is on file yet, the value is trusted immediately if a
531
+ signing-key pairing already exists, or staged for the very next
532
+ sign-in's challenge to match (refused otherwise) if it doesn't.
533
+
534
+ Renewing the certificate under the SAME key (e.g. \`certbot renew
535
+ --reuse-key\`) keeps the pin working with no action needed; a renewal under
536
+ a NEW key locks the gateway until an operator re-pairs (clears
537
+ <dataDir>/as-pairing.json and signs in again).
538
+
379
539
  Changes take effect on the next \`suveren-gateway start\` / \`restart\` — a
380
540
  running gateway keeps using what it already resolved at its own startup.
381
541
  `);
@@ -390,10 +550,12 @@ async function config(args) {
390
550
  if (!key) {
391
551
  console.log(`as-url: ${resolveAsUrl(DATA_DIR)}`);
392
552
  console.log(`ca-file: ${saved.caFile ?? '(not set)'}`);
553
+ console.log(`pin-tls: ${resolvePinTls(DATA_DIR) ? 'on' : 'off'}`);
393
554
  return;
394
555
  }
395
556
  if (key === 'as-url') { console.log(resolveAsUrl(DATA_DIR)); return; }
396
557
  if (key === 'ca-file') { console.log(saved.caFile ?? ''); return; }
558
+ if (key === 'pin-tls') { console.log(resolvePinTls(DATA_DIR) ? 'on' : 'off'); return; }
397
559
  console.error(`Unknown config key: ${key}\n`);
398
560
  printConfigHelp();
399
561
  process.exit(2);
@@ -432,6 +594,37 @@ async function config(args) {
432
594
  console.log('Restart to pick it up: suveren-gateway restart');
433
595
  return;
434
596
  }
597
+ if (key === 'pin-tls') {
598
+ if (value !== 'on' && value !== 'off') {
599
+ console.error('Usage: suveren-gateway config set pin-tls on|off [--expect-fingerprint <sha256-hex>]');
600
+ process.exit(2);
601
+ }
602
+ if (value === 'off') {
603
+ writeConfig(DATA_DIR, { pinTls: false });
604
+ console.log('Saved pin-tls: off');
605
+ console.log('Restart to pick it up: suveren-gateway restart');
606
+ return;
607
+ }
608
+ // value === 'on': requires the out-of-band fingerprint check (see
609
+ // requireConfirmedFingerprint) — never trusts whatever the AS
610
+ // happens to present at the next sign-in without one.
611
+ const effectiveAsUrl = resolveAsUrl(DATA_DIR);
612
+ const v = validatePinTls(effectiveAsUrl);
613
+ if (!v.ok) {
614
+ console.error(`Invalid pin-tls: ${v.error}`);
615
+ process.exit(1);
616
+ }
617
+ const hex = requireConfirmedFingerprint(args, effectiveAsUrl);
618
+ const committed = commitConfirmedFingerprint(DATA_DIR, effectiveAsUrl, hex);
619
+ writeConfig(DATA_DIR, { pinTls: true });
620
+ console.log(`Saved pin-tls: on (fingerprint ${formatFingerprint(hex)} confirmed)`);
621
+ console.log(
622
+ committed === 'pinned'
623
+ ? 'Restart to pick it up: suveren-gateway restart'
624
+ : 'Restart and sign in again — the first challenge must match this fingerprint: suveren-gateway restart',
625
+ );
626
+ return;
627
+ }
435
628
  console.error(`Unknown config key: ${key}\n`);
436
629
  printConfigHelp();
437
630
  process.exit(2);
@@ -945,15 +1138,20 @@ Usage:
945
1138
  suveren-gateway start [--detach] Run the gateway (foreground by default)
946
1139
  [--as-url <url>] Authority Server URL (saved for next time)
947
1140
  [--ca-file <path>] Internal CA bundle (saved for next time)
1141
+ [--pin-tls --expect-fingerprint <hex>] Pin the AS TLS certificate (saved; https:// only — see below)
948
1142
  suveren-gateway stop Stop a detached gateway
949
1143
  suveren-gateway restart Stop, then start --detach
950
1144
  suveren-gateway status Show running state + health
951
1145
  suveren-gateway logs [--tail] Print or tail ~/.suveren/gateway.log
952
1146
  suveren-gateway service <cmd> Run as a login service that survives reboot
953
1147
  (install | uninstall | status)
954
- suveren-gateway config get [as-url|ca-file] Print the resolved value(s)
1148
+ suveren-gateway config get [as-url|ca-file|pin-tls] Print the resolved value(s)
955
1149
  suveren-gateway config set as-url <url> Save the Authority Server URL
956
1150
  suveren-gateway config set ca-file <path> Save a CA bundle for internal TLS
1151
+ suveren-gateway config set pin-tls on --expect-fingerprint <hex>
1152
+ Pin the Authority Server's TLS certificate
1153
+ (see \`suveren-gateway config help\` for the fingerprint check)
1154
+ suveren-gateway config set pin-tls off Stop enforcing the TLS certificate pin
957
1155
  suveren-gateway help Print this help
958
1156
 
959
1157
  Environment:
@@ -0,0 +1,184 @@
1
+ {
2
+ "id": "erp",
3
+ "name": "ERP",
4
+ "version": "1.0.0",
5
+ "description": "Quote-to-order — items, customers, quotes, orders",
6
+ "icon": "briefcase",
7
+ "profile": "sales",
8
+ "npmPackage": "@humanagencyp/erp-mcp",
9
+ "mcp": {
10
+ "command": "erp-mcp",
11
+ "args": []
12
+ },
13
+ "credentials": {
14
+ "fields": [
15
+ {
16
+ "key": "databaseUrl",
17
+ "label": "Database URL (optional)",
18
+ "type": "text",
19
+ "placeholder": "Leave empty for local SQLite",
20
+ "optional": true
21
+ }
22
+ ],
23
+ "envMapping": {
24
+ "DATABASE_URL": "databaseUrl"
25
+ }
26
+ },
27
+ "personalDefault": true,
28
+ "oauth": null,
29
+ "toolGating": {
30
+ "default": {
31
+ "executionMapping": {},
32
+ "staticExecution": {}
33
+ },
34
+ "overrides": {
35
+ "list_items": {
36
+ "category": "read",
37
+ "boundField": "read_access",
38
+ "requiredValue": "unlimited"
39
+ },
40
+ "get_item": {
41
+ "category": "read",
42
+ "boundField": "read_access",
43
+ "requiredValue": "unlimited"
44
+ },
45
+ "find_customers": {
46
+ "category": "read",
47
+ "boundField": "read_access",
48
+ "requiredValue": "unlimited"
49
+ },
50
+ "get_customer": {
51
+ "category": "read",
52
+ "boundField": "read_access",
53
+ "requiredValue": "unlimited"
54
+ },
55
+ "list_quotes": {
56
+ "category": "read",
57
+ "boundField": "read_access",
58
+ "requiredValue": "unlimited"
59
+ },
60
+ "get_quote": {
61
+ "category": "read",
62
+ "boundField": "read_access",
63
+ "requiredValue": "unlimited"
64
+ },
65
+ "list_orders": {
66
+ "category": "read",
67
+ "boundField": "read_access",
68
+ "requiredValue": "unlimited"
69
+ },
70
+ "get_order": {
71
+ "category": "read",
72
+ "boundField": "read_access",
73
+ "requiredValue": "unlimited"
74
+ },
75
+ "create_quote": {
76
+ "executionMapping": {
77
+ "value": "value",
78
+ "discount_pct": "discount_pct",
79
+ "currency": "currency"
80
+ },
81
+ "staticExecution": {
82
+ "action_type": "quote"
83
+ },
84
+ "actionLabel": "Quote drafted"
85
+ },
86
+ "update_quote": {
87
+ "executionMapping": {
88
+ "value": "value",
89
+ "discount_pct": "discount_pct",
90
+ "currency": "currency"
91
+ },
92
+ "staticExecution": {
93
+ "action_type": "quote"
94
+ },
95
+ "actionLabel": "Quote updated"
96
+ },
97
+ "send_quote": {
98
+ "executionMapping": {
99
+ "value": "value",
100
+ "discount_pct": "discount_pct",
101
+ "currency": "currency"
102
+ },
103
+ "staticExecution": {
104
+ "action_type": "send"
105
+ },
106
+ "actionLabel": "Quote sent"
107
+ },
108
+ "convert_quote_to_order": {
109
+ "executionMapping": {
110
+ "value": "value",
111
+ "discount_pct": "discount_pct",
112
+ "currency": "currency"
113
+ },
114
+ "staticExecution": {
115
+ "action_type": "order"
116
+ },
117
+ "actionLabel": "Quote converted to order"
118
+ }
119
+ }
120
+ },
121
+ "templates": [
122
+ {
123
+ "name": "Draft quotes",
124
+ "description": "Look up items and customers, create and edit draft quotes. Nothing is sent to a customer or converted to an order.",
125
+ "risk": "low",
126
+ "mode": "automatic",
127
+ "bounds": {
128
+ "read_access": "unlimited",
129
+ "value_max": "5000",
130
+ "discount_max": "15",
131
+ "order_value_daily_max": "0",
132
+ "quote_daily_max": "20",
133
+ "send_daily_max": "0",
134
+ "order_daily_max": "0"
135
+ },
136
+ "context": {
137
+ "currency": "EUR"
138
+ },
139
+ "intent": "Allow the agent to browse items and customers and prepare draft quotes for my review. It cannot send a quote to a customer or convert one into an order.",
140
+ "ttl": 2592000,
141
+ "tags": [
142
+ "drafts only",
143
+ "no sends",
144
+ "no orders"
145
+ ]
146
+ },
147
+ {
148
+ "name": "Send and confirm with review above limit",
149
+ "description": "Draft, send, and convert quotes into orders. Every send and every order requires your approval.",
150
+ "risk": "high",
151
+ "mode": "review",
152
+ "bounds": {
153
+ "read_access": "unlimited",
154
+ "value_max": "20000",
155
+ "discount_max": "20",
156
+ "order_value_daily_max": "50000",
157
+ "quote_daily_max": "50",
158
+ "send_daily_max": "20",
159
+ "order_daily_max": "10"
160
+ },
161
+ "context": {
162
+ "currency": "EUR"
163
+ },
164
+ "intent": "Allow the agent to draft, send, and convert quotes into orders. Every quote sent to a customer and every order requires my review before it happens.",
165
+ "ttl": 2592000,
166
+ "tags": [
167
+ "20k EUR/document",
168
+ "review mode",
169
+ "10 orders/day"
170
+ ]
171
+ }
172
+ ],
173
+ "setupHint": "No configuration needed — works out of the box.",
174
+ "setupGuide": [
175
+ {
176
+ "title": "Ready to use",
177
+ "description": "The ERP connector stores data locally on your machine, seeded with a small demo catalog and customer list. No external account or API key needed."
178
+ },
179
+ {
180
+ "title": "Optional: use a shared database",
181
+ "description": "For team use, provide a PostgreSQL connection string in the Database URL field. Otherwise, leave it empty to use local SQLite storage."
182
+ }
183
+ ]
184
+ }
@@ -6,6 +6,7 @@
6
6
  "records": "records.json",
7
7
  "linkedin": "linkedin.json",
8
8
  "calendar": "calendar.json",
9
- "deploy-github": "deploy-github.json"
9
+ "deploy-github": "deploy-github.json",
10
+ "erp": "erp.json"
10
11
  }
11
12
  }