@suveren/gateway 0.7.3 → 0.7.6

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 (206) hide show
  1. package/content/integrations/gmail.json +3 -6
  2. package/dist/control-plane/index.mjs +294 -62
  3. package/dist/mcp-server/http.mjs +467 -73
  4. package/dist/ui/assets/{index-CkWaRck1.css → index-BYVb_0oG.css} +1 -1
  5. package/dist/ui/assets/index-CsrM6RQr.js +110 -0
  6. package/dist/ui/index.html +2 -2
  7. package/node_modules/@hap/core/dist/index.d.mts +258 -9
  8. package/node_modules/@hap/core/dist/index.d.ts +258 -9
  9. package/node_modules/@hap/core/dist/index.js +227 -5
  10. package/node_modules/@hap/core/dist/index.mjs +213 -4
  11. package/node_modules/@hap/core/package.json +3 -2
  12. package/node_modules/@hap/core/src/did-key.ts +126 -0
  13. package/node_modules/@hap/core/src/identity.ts +21 -5
  14. package/node_modules/@hap/core/src/index.ts +3 -0
  15. package/node_modules/@hap/core/src/mandate.ts +207 -0
  16. package/node_modules/@hap/core/src/receipt.ts +74 -0
  17. package/node_modules/@hap/core/src/types.ts +81 -5
  18. package/node_modules/@hono/node-server/dist/index.cjs +151 -9
  19. package/node_modules/@hono/node-server/dist/index.mjs +151 -9
  20. package/node_modules/@hono/node-server/package.json +1 -1
  21. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/README.md +2 -0
  22. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.d.ts +20 -0
  23. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.js +20 -14
  24. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/dist/index.js.map +1 -1
  25. package/node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type/package.json +1 -1
  26. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/accept.js +54 -0
  27. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/charset.js +10 -34
  28. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/encoding.js +9 -30
  29. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/language.js +15 -36
  30. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/lib/mediaType.js +16 -134
  31. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/LICENSE +22 -0
  32. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/README.md +71 -0
  33. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.d.ts +46 -0
  34. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.js +176 -0
  35. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/dist/index.js.map +1 -0
  36. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/node_modules/content-type/package.json +52 -0
  37. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/package.json +16 -10
  38. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/README.md +2 -0
  39. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.d.ts +20 -0
  40. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.js +20 -14
  41. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/dist/index.js.map +1 -1
  42. package/node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type/package.json +1 -1
  43. package/node_modules/@types/node/README.md +1 -1
  44. package/node_modules/@types/node/buffer.buffer.d.ts +5 -5
  45. package/node_modules/@types/node/crypto.d.ts +2 -2
  46. package/node_modules/@types/node/http.d.ts +16 -0
  47. package/node_modules/@types/node/http2.d.ts +10 -2
  48. package/node_modules/@types/node/package.json +2 -2
  49. package/node_modules/@types/node/process.d.ts +52 -0
  50. package/node_modules/@types/node/quic.d.ts +210 -28
  51. package/node_modules/@types/node/sqlite.d.ts +6 -2
  52. package/node_modules/@types/node/test.d.ts +36 -0
  53. package/node_modules/eventsource-parser/dist/index.cjs +2 -2
  54. package/node_modules/eventsource-parser/dist/index.cjs.map +1 -1
  55. package/node_modules/eventsource-parser/dist/index.js +2 -2
  56. package/node_modules/eventsource-parser/dist/index.js.map +1 -1
  57. package/node_modules/eventsource-parser/package.json +1 -1
  58. package/node_modules/eventsource-parser/src/parse.ts +4 -2
  59. package/node_modules/fast-uri/index.js +201 -48
  60. package/node_modules/fast-uri/lib/schemes.js +9 -4
  61. package/node_modules/fast-uri/lib/utils.js +388 -91
  62. package/node_modules/fast-uri/package.json +1 -1
  63. package/node_modules/fast-uri/test/component-safe-serialization.test.js +105 -0
  64. package/node_modules/fast-uri/test/equal.test.js +31 -3
  65. package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
  66. package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
  67. package/node_modules/fast-uri/test/ipv6-validation.test.js +90 -0
  68. package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
  69. package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
  70. package/node_modules/fast-uri/test/parse.test.js +7 -3
  71. package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
  72. package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
  73. package/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
  74. package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
  75. package/node_modules/fast-uri/test/security.test.js +75 -3
  76. package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
  77. package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
  78. package/node_modules/hono/dist/cjs/client/client.js +27 -13
  79. package/node_modules/hono/dist/cjs/client/utils.js +4 -1
  80. package/node_modules/hono/dist/cjs/context.js +4 -0
  81. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +36 -2
  82. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +1 -1
  83. package/node_modules/hono/dist/cjs/helper/ssg/utils.js +30 -10
  84. package/node_modules/hono/dist/cjs/jsx/dom/render.js +2 -0
  85. package/node_modules/hono/dist/cjs/jsx/intrinsic-element/components.js +1 -1
  86. package/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
  87. package/node_modules/hono/dist/cjs/middleware/cors/index.js +16 -13
  88. package/node_modules/hono/dist/cjs/middleware/csrf/index.js +1 -1
  89. package/node_modules/hono/dist/cjs/middleware/etag/digest.js +47 -1
  90. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -3
  91. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +3 -1
  92. package/node_modules/hono/dist/cjs/middleware/secure-headers/secure-headers.js +6 -3
  93. package/node_modules/hono/dist/cjs/request.js +8 -4
  94. package/node_modules/hono/dist/cjs/router/linear-router/router.js +7 -2
  95. package/node_modules/hono/dist/cjs/router/pattern-router/router.js +3 -9
  96. package/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +10 -3
  97. package/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +47 -83
  98. package/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +2 -1
  99. package/node_modules/hono/dist/cjs/router/trie-router/node.js +46 -69
  100. package/node_modules/hono/dist/cjs/router/trie-router/router.js +3 -11
  101. package/node_modules/hono/dist/cjs/router/utils.js +27 -0
  102. package/node_modules/hono/dist/cjs/utils/body.js +15 -3
  103. package/node_modules/hono/dist/cjs/utils/cookie.js +1 -1
  104. package/node_modules/hono/dist/cjs/utils/ipaddr.js +5 -3
  105. package/node_modules/hono/dist/cjs/utils/stream.js +7 -1
  106. package/node_modules/hono/dist/cjs/utils/url.js +11 -3
  107. package/node_modules/hono/dist/client/client.js +27 -13
  108. package/node_modules/hono/dist/client/utils.js +4 -1
  109. package/node_modules/hono/dist/context.js +4 -0
  110. package/node_modules/hono/dist/helper/accepts/accepts.js +36 -2
  111. package/node_modules/hono/dist/helper/ssg/ssg.js +1 -1
  112. package/node_modules/hono/dist/helper/ssg/utils.js +30 -10
  113. package/node_modules/hono/dist/jsx/dom/render.js +2 -0
  114. package/node_modules/hono/dist/jsx/intrinsic-element/components.js +1 -1
  115. package/node_modules/hono/dist/middleware/cache/index.js +1 -1
  116. package/node_modules/hono/dist/middleware/cors/index.js +16 -13
  117. package/node_modules/hono/dist/middleware/csrf/index.js +1 -1
  118. package/node_modules/hono/dist/middleware/etag/digest.js +47 -1
  119. package/node_modules/hono/dist/middleware/etag/index.js +3 -3
  120. package/node_modules/hono/dist/middleware/pretty-json/index.js +3 -1
  121. package/node_modules/hono/dist/middleware/secure-headers/secure-headers.js +6 -3
  122. package/node_modules/hono/dist/request.js +8 -4
  123. package/node_modules/hono/dist/router/linear-router/router.js +7 -2
  124. package/node_modules/hono/dist/router/pattern-router/router.js +3 -9
  125. package/node_modules/hono/dist/router/reg-exp-router/node.js +6 -2
  126. package/node_modules/hono/dist/router/reg-exp-router/router.js +53 -84
  127. package/node_modules/hono/dist/router/reg-exp-router/trie.js +2 -1
  128. package/node_modules/hono/dist/router/trie-router/node.js +46 -69
  129. package/node_modules/hono/dist/router/trie-router/router.js +3 -11
  130. package/node_modules/hono/dist/router/utils.js +5 -0
  131. package/node_modules/hono/dist/types/context.d.ts +4 -0
  132. package/node_modules/hono/dist/types/middleware/secure-headers/permissions-policy.d.ts +3 -3
  133. package/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +3 -0
  134. package/node_modules/hono/dist/types/router/trie-router/node.d.ts +1 -2
  135. package/node_modules/hono/dist/types/router/trie-router/router.d.ts +0 -1
  136. package/node_modules/hono/dist/types/router/utils.d.ts +1 -0
  137. package/node_modules/hono/dist/types/utils/url.d.ts +4 -0
  138. package/node_modules/hono/dist/utils/body.js +15 -3
  139. package/node_modules/hono/dist/utils/cookie.js +1 -1
  140. package/node_modules/hono/dist/utils/ipaddr.js +5 -3
  141. package/node_modules/hono/dist/utils/stream.js +7 -1
  142. package/node_modules/hono/dist/utils/url.js +11 -3
  143. package/node_modules/hono/package.json +1 -1
  144. package/node_modules/ip-address/README.md +134 -134
  145. package/node_modules/ip-address/dist/address-error.d.ts +11 -0
  146. package/node_modules/ip-address/dist/address-error.js.map +1 -1
  147. package/node_modules/ip-address/dist/ipv4.d.ts +22 -6
  148. package/node_modules/ip-address/dist/ipv4.js +22 -6
  149. package/node_modules/ip-address/dist/ipv4.js.map +1 -1
  150. package/node_modules/ip-address/dist/ipv6.d.ts +10 -3
  151. package/node_modules/ip-address/dist/ipv6.js +26 -17
  152. package/node_modules/ip-address/dist/ipv6.js.map +1 -1
  153. package/node_modules/ip-address/package.json +1 -1
  154. package/node_modules/is-plain-object/README.md +39 -95
  155. package/node_modules/is-plain-object/dist/is-plain-object.js +0 -2
  156. package/node_modules/is-plain-object/is-plain-object.d.ts +6 -1
  157. package/node_modules/is-plain-object/package.json +8 -34
  158. package/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts +4 -3
  159. package/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts +4 -3
  160. package/node_modules/jose/dist/types/jwe/general/encrypt.d.ts +4 -3
  161. package/node_modules/jose/dist/types/jwks/remote.d.ts +4 -4
  162. package/node_modules/jose/dist/types/jws/general/verify.d.ts +3 -3
  163. package/node_modules/jose/dist/types/jwt/encrypt.d.ts +12 -17
  164. package/node_modules/jose/dist/types/jwt/sign.d.ts +8 -14
  165. package/node_modules/jose/dist/types/jwt/unsecured.d.ts +9 -15
  166. package/node_modules/jose/dist/types/types.d.ts +4 -2
  167. package/node_modules/jose/dist/webapi/jwe/compact/encrypt.js +30 -8
  168. package/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js +8 -4
  169. package/node_modules/jose/dist/webapi/jwe/flattened/encrypt.js +6 -13
  170. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +19 -19
  171. package/node_modules/jose/dist/webapi/jwe/general/encrypt.js +32 -21
  172. package/node_modules/jose/dist/webapi/jwk/embedded.js +15 -1
  173. package/node_modules/jose/dist/webapi/jwk/thumbprint.js +11 -5
  174. package/node_modules/jose/dist/webapi/jwks/local.js +45 -53
  175. package/node_modules/jose/dist/webapi/jwks/remote.js +82 -103
  176. package/node_modules/jose/dist/webapi/jws/compact/sign.js +10 -9
  177. package/node_modules/jose/dist/webapi/jws/flattened/sign.js +2 -1
  178. package/node_modules/jose/dist/webapi/jws/flattened/verify.js +8 -7
  179. package/node_modules/jose/dist/webapi/jws/general/sign.js +5 -3
  180. package/node_modules/jose/dist/webapi/jws/general/verify.js +51 -21
  181. package/node_modules/jose/dist/webapi/jwt/decrypt.js +7 -9
  182. package/node_modules/jose/dist/webapi/jwt/encrypt.js +21 -50
  183. package/node_modules/jose/dist/webapi/jwt/sign.js +8 -41
  184. package/node_modules/jose/dist/webapi/jwt/unsecured.js +21 -42
  185. package/node_modules/jose/dist/webapi/key/generate_key_pair.js +6 -2
  186. package/node_modules/jose/dist/webapi/key/generate_secret.js +7 -6
  187. package/node_modules/jose/dist/webapi/key/import.js +16 -12
  188. package/node_modules/jose/dist/webapi/lib/asn1.js +7 -2
  189. package/node_modules/jose/dist/webapi/lib/content_encryption.js +7 -15
  190. package/node_modules/jose/dist/webapi/lib/deflate.js +8 -0
  191. package/node_modules/jose/dist/webapi/lib/helpers.js +1 -1
  192. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +85 -28
  193. package/node_modules/jose/dist/webapi/lib/jwe_encrypt.js +45 -13
  194. package/node_modules/jose/dist/webapi/lib/jwk_metadata.js +20 -0
  195. package/node_modules/jose/dist/webapi/lib/jws_sign.js +47 -36
  196. package/node_modules/jose/dist/webapi/lib/jws_verify.js +77 -45
  197. package/node_modules/jose/dist/webapi/lib/jwt_claims_set.js +82 -47
  198. package/node_modules/jose/dist/webapi/lib/key.js +24 -9
  199. package/node_modules/jose/dist/webapi/lib/key_management.js +10 -4
  200. package/node_modules/jose/dist/webapi/lib/key_options.js +6 -0
  201. package/node_modules/jose/dist/webapi/lib/options.js +26 -0
  202. package/node_modules/jose/dist/webapi/lib/type_checks.js +11 -13
  203. package/node_modules/jose/package.json +1 -1
  204. package/package.json +2 -2
  205. package/dist/ui/assets/index-DdJeSfV9.js +0 -110
  206. package/node_modules/@modelcontextprotocol/sdk/node_modules/negotiator/HISTORY.md +0 -114
@@ -55,6 +55,10 @@ var SPClient = class {
55
55
  sessionCookie = "";
56
56
  apiKey = "";
57
57
  receiptRetry;
58
+ /** The AS base URL this client talks to (for provenance records). */
59
+ get url() {
60
+ return this.baseUrl;
61
+ }
58
62
  setApiKey(key) {
59
63
  this.apiKey = key;
60
64
  }
@@ -137,7 +141,7 @@ var SPClient = class {
137
141
  */
138
142
  async postReceipt(data) {
139
143
  const body = JSON.stringify(data);
140
- const maxAttempts = data.idempotencyKey ? this.receiptRetry.maxAttempts : 1;
144
+ const maxAttempts = data.idempotencyKey || data.proposalId ? this.receiptRetry.maxAttempts : 1;
141
145
  let lastError;
142
146
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
143
147
  const isLast = attempt === maxAttempts;
@@ -825,6 +829,229 @@ var DenialLog = class {
825
829
  }
826
830
  };
827
831
 
832
+ // src/lib/receipt-archive.ts
833
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4, unlinkSync as unlinkSync4 } from "fs";
834
+ import { homedir as homedir4 } from "os";
835
+ import { dirname as dirname4, join as join4 } from "path";
836
+ import { createCipheriv as createCipheriv4, createDecipheriv as createDecipheriv4, randomBytes as randomBytes4 } from "crypto";
837
+ var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join4(homedir4(), ".suveren");
838
+ function stripPreviews(proposal) {
839
+ const args = proposal.toolArgs;
840
+ if (!args || typeof args !== "object") return proposal;
841
+ const { _imagePreview, ...rest } = args;
842
+ if (_imagePreview === void 0) return proposal;
843
+ return { ...proposal, toolArgs: rest };
844
+ }
845
+ var ReceiptArchive = class {
846
+ data = { version: 1, authorizations: {}, receipts: [] };
847
+ baseDir;
848
+ vaultKey = null;
849
+ constructor(filePath) {
850
+ if (filePath && filePath.endsWith(".json")) {
851
+ this.baseDir = dirname4(filePath);
852
+ } else {
853
+ this.baseDir = filePath ?? DEFAULT_DIR4;
854
+ }
855
+ this.loadPlaintext();
856
+ }
857
+ /**
858
+ * Set the vault key for encryption. Triggers migration from plaintext if needed.
859
+ */
860
+ setVaultKey(key) {
861
+ this.vaultKey = key;
862
+ if (existsSync4(this.encryptedFilePath)) {
863
+ this.loadEncrypted();
864
+ } else if (this.data.receipts.length > 0 || Object.keys(this.data.authorizations).length > 0) {
865
+ this.persistEncrypted();
866
+ if (existsSync4(this.plaintextFilePath)) {
867
+ try {
868
+ unlinkSync4(this.plaintextFilePath);
869
+ } catch {
870
+ }
871
+ }
872
+ }
873
+ }
874
+ /**
875
+ * True when encrypted evidence exists on disk that this process cannot read
876
+ * yet (no vault key). Callers MUST NOT present the archive as "empty" in
877
+ * this state — that would report a hollow success over unreadable evidence.
878
+ */
879
+ isLocked() {
880
+ return this.vaultKey === null && existsSync4(this.encryptedFilePath);
881
+ }
882
+ /**
883
+ * Archive a signed receipt (and the grant it was issued under).
884
+ * Idempotent on receipt id — a replayed receipt is recorded once.
885
+ */
886
+ record(entry, now = Math.floor(Date.now() / 1e3)) {
887
+ let changed = false;
888
+ const receiptId = typeof entry.receipt?.id === "string" ? entry.receipt.id : void 0;
889
+ const alreadyArchived = receiptId !== void 0 && this.data.receipts.some((r) => r.receipt.id === receiptId);
890
+ if (!alreadyArchived) {
891
+ this.data.receipts.push({
892
+ archivedAt: now,
893
+ authorizationId: entry.authorizationId,
894
+ asUrl: entry.asUrl,
895
+ asPublicKey: entry.asPublicKey,
896
+ receipt: entry.receipt,
897
+ ...entry.proposal ? { proposal: stripPreviews(entry.proposal) } : {},
898
+ ...entry.boundContent !== void 0 ? { boundContent: entry.boundContent } : {}
899
+ });
900
+ changed = true;
901
+ }
902
+ if (entry.authorization) {
903
+ const existing = this.data.authorizations[entry.authorizationId];
904
+ if (!existing) {
905
+ this.data.authorizations[entry.authorizationId] = {
906
+ authorizationId: entry.authorizationId,
907
+ profileId: entry.authorization.profileId,
908
+ boundsHash: entry.authorization.boundsHash,
909
+ contextHash: entry.authorization.contextHash,
910
+ bounds: entry.authorization.bounds,
911
+ context: entry.authorization.context,
912
+ intent: entry.authorization.intent,
913
+ attestations: [...entry.authorization.attestations],
914
+ archivedAt: now
915
+ };
916
+ changed = true;
917
+ } else {
918
+ for (const att of entry.authorization.attestations) {
919
+ const seen = existing.attestations.some(
920
+ (a) => a.domain === att.domain && a.blob === att.blob
921
+ );
922
+ if (!seen) {
923
+ existing.attestations.push(att);
924
+ changed = true;
925
+ }
926
+ }
927
+ if (!existing.bounds && entry.authorization.bounds) {
928
+ existing.bounds = entry.authorization.bounds;
929
+ changed = true;
930
+ }
931
+ if (!existing.context && entry.authorization.context) {
932
+ existing.context = entry.authorization.context;
933
+ changed = true;
934
+ }
935
+ if (!existing.intent && entry.authorization.intent) {
936
+ existing.intent = entry.authorization.intent;
937
+ changed = true;
938
+ }
939
+ }
940
+ }
941
+ if (changed) this.persist();
942
+ }
943
+ getReceipts() {
944
+ return [...this.data.receipts];
945
+ }
946
+ getAuthorizations() {
947
+ return Object.values(this.data.authorizations);
948
+ }
949
+ get size() {
950
+ return this.data.receipts.length;
951
+ }
952
+ // ─── Encryption helpers ─────────────────────────────────────────────────
953
+ encrypt(plaintext) {
954
+ if (!this.vaultKey) throw new Error("No vault key");
955
+ const iv = randomBytes4(12);
956
+ const cipher = createCipheriv4("aes-256-gcm", this.vaultKey, iv);
957
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
958
+ const tag = cipher.getAuthTag();
959
+ return {
960
+ iv: iv.toString("hex"),
961
+ ciphertext: encrypted.toString("hex"),
962
+ tag: tag.toString("hex")
963
+ };
964
+ }
965
+ decrypt(blob) {
966
+ if (!this.vaultKey) throw new Error("No vault key");
967
+ const decipher = createDecipheriv4(
968
+ "aes-256-gcm",
969
+ this.vaultKey,
970
+ Buffer.from(blob.iv, "hex")
971
+ );
972
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
973
+ const decrypted = Buffer.concat([
974
+ decipher.update(Buffer.from(blob.ciphertext, "hex")),
975
+ decipher.final()
976
+ ]);
977
+ return decrypted.toString("utf8");
978
+ }
979
+ // ─── File paths ─────────────────────────────────────────────────────────
980
+ get plaintextFilePath() {
981
+ return join4(this.baseDir, "receipt-archive.json");
982
+ }
983
+ get encryptedFilePath() {
984
+ return join4(this.baseDir, "receipt-archive.enc.json");
985
+ }
986
+ // ─── Load / Persist ─────────────────────────────────────────────────────
987
+ loadPlaintext() {
988
+ if (!existsSync4(this.plaintextFilePath)) {
989
+ mkdirSync4(this.baseDir, { recursive: true });
990
+ return;
991
+ }
992
+ try {
993
+ const raw = readFileSync4(this.plaintextFilePath, "utf-8");
994
+ const parsed = JSON.parse(raw);
995
+ this.data = {
996
+ version: 1,
997
+ authorizations: parsed.authorizations ?? {},
998
+ receipts: parsed.receipts ?? []
999
+ };
1000
+ } catch {
1001
+ console.error(`[ReceiptArchive] Could not parse ${this.plaintextFilePath} \u2014 preserving as .corrupt`);
1002
+ try {
1003
+ writeFileSync4(
1004
+ `${this.plaintextFilePath}.corrupt-${Date.now()}`,
1005
+ readFileSync4(this.plaintextFilePath)
1006
+ );
1007
+ } catch {
1008
+ }
1009
+ }
1010
+ }
1011
+ loadEncrypted() {
1012
+ if (!existsSync4(this.encryptedFilePath)) return;
1013
+ try {
1014
+ const raw = readFileSync4(this.encryptedFilePath, "utf-8");
1015
+ const data = JSON.parse(raw);
1016
+ const decrypted = this.decrypt(data.blob);
1017
+ const parsed = JSON.parse(decrypted);
1018
+ this.data = {
1019
+ version: 1,
1020
+ authorizations: parsed.authorizations ?? {},
1021
+ receipts: parsed.receipts ?? []
1022
+ };
1023
+ } catch (err) {
1024
+ console.error(`[ReceiptArchive] Could not decrypt ${this.encryptedFilePath} \u2014 preserving as .corrupt:`, err);
1025
+ try {
1026
+ writeFileSync4(
1027
+ `${this.encryptedFilePath}.corrupt-${Date.now()}`,
1028
+ readFileSync4(this.encryptedFilePath)
1029
+ );
1030
+ } catch {
1031
+ }
1032
+ }
1033
+ }
1034
+ persist() {
1035
+ if (this.vaultKey) {
1036
+ this.persistEncrypted();
1037
+ } else {
1038
+ this.persistPlaintext();
1039
+ }
1040
+ }
1041
+ persistPlaintext() {
1042
+ mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
1043
+ writeFileSync4(this.plaintextFilePath, JSON.stringify(this.data, null, 2), { encoding: "utf-8", mode: 384 });
1044
+ }
1045
+ persistEncrypted() {
1046
+ const data = {
1047
+ version: 1,
1048
+ blob: this.encrypt(JSON.stringify(this.data))
1049
+ };
1050
+ mkdirSync4(this.baseDir, { recursive: true, mode: 448 });
1051
+ writeFileSync4(this.encryptedFilePath, JSON.stringify(data, null, 2), { encoding: "utf-8", mode: 384 });
1052
+ }
1053
+ };
1054
+
828
1055
  // src/lib/gatekeeper.ts
829
1056
  import { verify } from "@hap/core";
830
1057
  var MCPGatekeeper = class {
@@ -898,6 +1125,7 @@ var SharedState = class {
898
1125
  gateStore;
899
1126
  executionLog;
900
1127
  denialLog;
1128
+ receiptArchive;
901
1129
  gatekeeper;
902
1130
  constructor(spUrl2, gateStorePath) {
903
1131
  this.spClient = new SPClient(spUrl2);
@@ -905,8 +1133,50 @@ var SharedState = class {
905
1133
  this.gateStore = new GateStore(gateStorePath);
906
1134
  this.executionLog = new ExecutionLog(gateStorePath);
907
1135
  this.denialLog = new DenialLog(gateStorePath);
1136
+ this.receiptArchive = new ReceiptArchive(gateStorePath);
908
1137
  this.gatekeeper = new MCPGatekeeper(this.cache, this.executionLog);
909
1138
  }
1139
+ /**
1140
+ * Archive a signed receipt into the local receipt archive — the subject's
1141
+ * own durable copy of the evidence (see receipt-archive.ts).
1142
+ *
1143
+ * Best-effort by contract: the AS has already issued the receipt and holds
1144
+ * the authoritative copy, so an archive failure logs loudly but MUST NOT
1145
+ * block the execution it documents.
1146
+ */
1147
+ async archiveReceipt(receipt, opts) {
1148
+ try {
1149
+ let asPublicKey;
1150
+ try {
1151
+ asPublicKey = await this.cache.getPublicKey();
1152
+ } catch {
1153
+ }
1154
+ this.receiptArchive.record({
1155
+ receipt,
1156
+ authorizationId: opts.authorizationId,
1157
+ asUrl: this.spClient.url,
1158
+ asPublicKey,
1159
+ proposal: opts.proposal,
1160
+ boundContent: opts.boundContent,
1161
+ authorization: opts.profileId ? {
1162
+ profileId: opts.profileId,
1163
+ boundsHash: opts.boundsHash,
1164
+ contextHash: opts.contextHash ?? this.gateStore.get(opts.authorizationId)?.contextHash,
1165
+ bounds: opts.bounds,
1166
+ context: opts.context,
1167
+ // Fall back to the gate store — the review path's cached auth
1168
+ // carries no gate content.
1169
+ intent: opts.intent ?? this.gateStore.get(opts.authorizationId)?.gateContent?.intent,
1170
+ attestations: opts.attestations ?? []
1171
+ } : void 0
1172
+ });
1173
+ } catch (err) {
1174
+ console.error(
1175
+ "[Suveren MCP] Receipt archive write failed (execution proceeds \u2014 AS retains authoritative copy):",
1176
+ err
1177
+ );
1178
+ }
1179
+ }
910
1180
  setGateContent(path, authorizationId, profileId, content, opts) {
911
1181
  this.gateStore.set(authorizationId, {
912
1182
  authorizationId,
@@ -1012,17 +1282,17 @@ function formatConsumptionFull(entries) {
1012
1282
  }
1013
1283
 
1014
1284
  // src/lib/context-loader.ts
1015
- import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
1016
- import { homedir as homedir4 } from "os";
1017
- import { join as join4 } from "path";
1018
- var DEFAULT_DIR4 = process.env.SUVEREN_DATA_DIR ?? join4(homedir4(), ".suveren");
1285
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
1286
+ import { homedir as homedir5 } from "os";
1287
+ import { join as join5 } from "path";
1288
+ var DEFAULT_DIR5 = process.env.SUVEREN_DATA_DIR ?? join5(homedir5(), ".suveren");
1019
1289
  var BRIEF_MAX_CHARS = 16 * 1024;
1020
1290
  function readContextFile(dataDir) {
1021
- const dir = dataDir ?? DEFAULT_DIR4;
1022
- const filePath = join4(dir, "context.md");
1023
- if (!existsSync4(filePath)) return null;
1291
+ const dir = dataDir ?? DEFAULT_DIR5;
1292
+ const filePath = join5(dir, "context.md");
1293
+ if (!existsSync5(filePath)) return null;
1024
1294
  try {
1025
- const content = readFileSync4(filePath, "utf-8").trim();
1295
+ const content = readFileSync5(filePath, "utf-8").trim();
1026
1296
  return content || null;
1027
1297
  } catch {
1028
1298
  return null;
@@ -1192,7 +1462,7 @@ function appendVerificationFooter(tool, args, receiptId, subject) {
1192
1462
  // src/lib/content-binding.ts
1193
1463
  import {
1194
1464
  computeContentHash,
1195
- computeFieldsContentHash,
1465
+ selectBoundFields,
1196
1466
  isFieldBinding,
1197
1467
  bindingAppliesTo,
1198
1468
  getProfile as getProfile2
@@ -1212,14 +1482,18 @@ function computeContentBinding(profileId, tool, toolArgs, actionType) {
1212
1482
  }
1213
1483
  return void 0;
1214
1484
  }
1485
+ const bound = selectBoundFields(binding, toolArgs);
1215
1486
  return {
1216
- contentHash: computeFieldsContentHash(binding, toolArgs),
1217
- contentBinding: { version: binding.version, kind: binding.kind, fields: binding.fields }
1487
+ contentHash: computeContentHash(binding, bound),
1488
+ contentBinding: { version: binding.version, kind: binding.kind, fields: binding.fields },
1489
+ boundContent: bound
1218
1490
  };
1219
1491
  }
1220
1492
  let contentHash;
1493
+ let boundContent;
1221
1494
  if (binding.kind === "jcs") {
1222
1495
  contentHash = computeContentHash(binding, toolArgs);
1496
+ boundContent = toolArgs;
1223
1497
  } else {
1224
1498
  const declared = tool?.gating?.contentField;
1225
1499
  const field = declared ?? (tool ? detectContentField(tool) : null);
@@ -1227,10 +1501,12 @@ function computeContentBinding(profileId, tool, toolArgs, actionType) {
1227
1501
  const raw = typeof toolArgs[field] === "string" ? toolArgs[field] : "";
1228
1502
  if (raw === "") return void 0;
1229
1503
  contentHash = computeContentHash(binding, raw);
1504
+ boundContent = raw;
1230
1505
  }
1231
1506
  return {
1232
1507
  contentHash,
1233
- contentBinding: { version: binding.version, kind: binding.kind }
1508
+ contentBinding: { version: binding.version, kind: binding.kind },
1509
+ boundContent
1234
1510
  };
1235
1511
  }
1236
1512
  function attachReceiptId(tool, args, receiptId) {
@@ -1849,6 +2125,29 @@ function createGatedToolHandlerInner(tool, integrationManager2, state2) {
1849
2125
  }
1850
2126
  }
1851
2127
  }
2128
+ const declaredActionType = typeof execution.action_type === "string" && execution.action_type.length > 0 ? execution.action_type : void 0;
2129
+ if (!declaredActionType) {
2130
+ return {
2131
+ content: [{
2132
+ type: "text",
2133
+ text: `Blocked by Gatekeeper: tool ${tool.namespacedName} declares no action_type in its manifest's staticExecution. A write cannot be bounds-checked without its action type \u2014 fix the integration manifest.`
2134
+ }],
2135
+ isError: true
2136
+ };
2137
+ }
2138
+ {
2139
+ const profileDef = getProfile3(profile);
2140
+ const registry = profileDef?.boundsSchema?.actionTypes;
2141
+ if (Array.isArray(registry) && registry.length > 0 && !registry.includes(declaredActionType)) {
2142
+ return {
2143
+ content: [{
2144
+ type: "text",
2145
+ text: `Blocked by Gatekeeper: action_type "${declaredActionType}" is not in profile ${profile}'s actionTypes registry (${registry.join(", ")}). Fix the integration manifest.`
2146
+ }],
2147
+ isError: true
2148
+ };
2149
+ }
2150
+ }
1852
2151
  const auths = state2.getEnrichedAuthorizations();
1853
2152
  const matchingAuths = auths.filter(
1854
2153
  (a) => a.complete && profileMatches(a.profileId, profile)
@@ -1946,12 +2245,7 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
1946
2245
  }
1947
2246
  let receiptId;
1948
2247
  try {
1949
- const actionType = typeof execution.action_type === "string" ? execution.action_type : void 0;
1950
- if (!actionType) {
1951
- console.error(
1952
- `[Suveren MCP] Warning: tool ${tool.namespacedName} has no action_type in staticExecution. Bounds check may be skipped. Fix the integration manifest.`
1953
- );
1954
- }
2248
+ const actionType = declaredActionType;
1955
2249
  const binding = computeContentBinding(auth.profileId, tool, args, actionType);
1956
2250
  const { receipt } = await state2.spClient.postReceipt({
1957
2251
  authorizationId: authzId,
@@ -1966,6 +2260,19 @@ Proposal ID: ${proposal.id}. Check status with check-pending-commitments(proposa
1966
2260
  ...binding ?? {}
1967
2261
  });
1968
2262
  receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
2263
+ await state2.archiveReceipt(receipt, {
2264
+ authorizationId: authzId,
2265
+ profileId: auth.profileId,
2266
+ boundsHash: auth.boundsHash,
2267
+ contextHash: auth.contextHash,
2268
+ bounds: auth.bounds ?? auth.frame,
2269
+ context: auth.context,
2270
+ intent: auth.gateContent?.intent,
2271
+ attestations: auth.attestations,
2272
+ // Automatic path has no proposal, so without this the archive
2273
+ // would hold a hash of content it cannot reproduce.
2274
+ boundContent: binding?.boundContent
2275
+ });
1969
2276
  } catch (err) {
1970
2277
  if (err instanceof ContentBindingError) {
1971
2278
  return {
@@ -2403,6 +2710,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2403
2710
  `[Suveren MCP] Warning: proposal ${proposal.id} has no action_type in executionContext. Bounds check may be skipped. Fix the integration manifest for ${proposal.tool}.`
2404
2711
  );
2405
2712
  }
2713
+ const cachedAuth = state2.cache.getAllAuthorizations().find((a) => a.authorizationId === proposal.authorizationId);
2406
2714
  let receiptId;
2407
2715
  try {
2408
2716
  const binding = computeContentBinding(
@@ -2413,6 +2721,9 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2413
2721
  );
2414
2722
  const { receipt } = await state2.spClient.postReceipt({
2415
2723
  authorizationId: proposal.authorizationId,
2724
+ // Optional cross-check — the AS fails closed on a mismatch. Parity
2725
+ // with the automatic path (tool-proxy.ts).
2726
+ boundsHash: cachedAuth?.boundsHash,
2416
2727
  profileId: proposal.profileId,
2417
2728
  action: proposal.tool,
2418
2729
  actionType: proposalActionType,
@@ -2420,9 +2731,29 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2420
2731
  amount: typeof proposal.executionContext.amount === "number" ? proposal.executionContext.amount : void 0,
2421
2732
  proposalId: proposal.id,
2422
2733
  toolArgs: proposal.toolArgs,
2734
+ // Deliberately NO idempotencyKey: the spec has review-mode commits carry
2735
+ // a proposalId INSTEAD of a key — the proposal's committed→executed
2736
+ // transition is their replay protection, and the AS replays the ORIGINAL
2737
+ // receipt to the same caller if a retry arrives after it committed.
2738
+ // SPClient therefore treats proposalId as retry-safe on its own; a
2739
+ // PROPOSAL_ALREADY_EXECUTED answer is still a definitive rejection
2740
+ // (never retried) and means a *different* caller consumed the proposal.
2423
2741
  ...binding ?? {}
2424
2742
  });
2425
2743
  receiptId = typeof receipt?.id === "string" ? receipt.id : void 0;
2744
+ await state2.archiveReceipt(receipt, {
2745
+ authorizationId: proposal.authorizationId,
2746
+ profileId: proposal.profileId,
2747
+ boundsHash: cachedAuth?.boundsHash,
2748
+ contextHash: cachedAuth?.contextHash,
2749
+ bounds: cachedAuth?.bounds ?? cachedAuth?.frame,
2750
+ context: cachedAuth?.context,
2751
+ attestations: cachedAuth?.attestations,
2752
+ // What the human actually approved, kept next to the receipt that cites
2753
+ // it — otherwise `proposalId` is a pointer to a server we may not have.
2754
+ proposal,
2755
+ boundContent: binding?.boundContent
2756
+ });
2426
2757
  } catch (err) {
2427
2758
  if (err instanceof ContentBindingError2) {
2428
2759
  return {
@@ -2451,7 +2782,7 @@ async function executeCommitted(proposal, state2, integrationManager2) {
2451
2782
  let outgoingArgs = proposal.toolArgs;
2452
2783
  if (discovered && receiptId) {
2453
2784
  if (shouldAttachFooter()) {
2454
- outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId);
2785
+ outgoingArgs = appendVerificationFooter(discovered, outgoingArgs, receiptId, cachedAuth?.subjects?.[0]);
2455
2786
  }
2456
2787
  outgoingArgs = attachReceiptId(discovered, outgoingArgs, receiptId);
2457
2788
  outgoingArgs = encodeOutgoingArgs(discovered, outgoingArgs);
@@ -2672,7 +3003,12 @@ function createMcpServer(state2, integrationManager2) {
2672
3003
  }
2673
3004
  }
2674
3005
  for (const tool of allTools) {
2675
- if (proxiedTools.has(tool.namespacedName)) continue;
3006
+ const existing = proxiedTools.get(tool.namespacedName);
3007
+ if (existing) {
3008
+ if (existing.tool === tool) continue;
3009
+ existing.registered.remove();
3010
+ proxiedTools.delete(tool.namespacedName);
3011
+ }
2676
3012
  const handler = createGatedToolHandler(tool, integrationManager2, state2);
2677
3013
  const zodShape = jsonSchemaToZodShape(tool.inputSchema);
2678
3014
  const description = buildProxiedToolDescription(tool, state2);
@@ -2740,16 +3076,16 @@ function verifyGateContentHashes(content, auth) {
2740
3076
  }
2741
3077
 
2742
3078
  // src/lib/integration-registry.ts
2743
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
2744
- import { homedir as homedir5 } from "os";
2745
- import { dirname as dirname4, join as join5 } from "path";
2746
- var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join5(homedir5(), ".suveren");
3079
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6 } from "fs";
3080
+ import { homedir as homedir6 } from "os";
3081
+ import { dirname as dirname5, join as join6 } from "path";
3082
+ var DEFAULT_DATA_DIR = process.env.SUVEREN_DATA_DIR ?? join6(homedir6(), ".suveren");
2747
3083
  var IntegrationRegistry = class {
2748
3084
  integrations = /* @__PURE__ */ new Map();
2749
3085
  filePath;
2750
3086
  constructor(dataDir) {
2751
3087
  const dir = dataDir ?? DEFAULT_DATA_DIR;
2752
- this.filePath = join5(dir, "integrations.json");
3088
+ this.filePath = join6(dir, "integrations.json");
2753
3089
  this.load();
2754
3090
  }
2755
3091
  // ─── CRUD ───────────────────────────────────────────────────────────────
@@ -2780,12 +3116,12 @@ var IntegrationRegistry = class {
2780
3116
  }
2781
3117
  // ─── Load / Save ────────────────────────────────────────────────────────
2782
3118
  load() {
2783
- if (!existsSync5(this.filePath)) {
2784
- mkdirSync4(dirname4(this.filePath), { recursive: true });
3119
+ if (!existsSync6(this.filePath)) {
3120
+ mkdirSync5(dirname5(this.filePath), { recursive: true });
2785
3121
  return;
2786
3122
  }
2787
3123
  try {
2788
- const raw = readFileSync5(this.filePath, "utf-8");
3124
+ const raw = readFileSync6(this.filePath, "utf-8");
2789
3125
  const data = JSON.parse(raw);
2790
3126
  const migrated = data.integrations.map((i) => {
2791
3127
  if ("toolGating" in i && !("profile" in i)) {
@@ -2814,33 +3150,33 @@ var IntegrationRegistry = class {
2814
3150
  version: 1,
2815
3151
  integrations: Array.from(this.integrations.values())
2816
3152
  };
2817
- mkdirSync4(dirname4(this.filePath), { recursive: true });
2818
- writeFileSync4(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
3153
+ mkdirSync5(dirname5(this.filePath), { recursive: true });
3154
+ writeFileSync5(this.filePath, JSON.stringify(data, null, 2), { encoding: "utf-8" });
2819
3155
  }
2820
3156
  };
2821
3157
 
2822
3158
  // src/lib/integration-manager.ts
2823
- import { homedir as homedir6 } from "os";
2824
- import { join as join6, delimiter } from "path";
2825
- import { existsSync as existsSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync } from "fs";
3159
+ import { homedir as homedir7 } from "os";
3160
+ import { join as join7, delimiter } from "path";
3161
+ import { existsSync as existsSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync } from "fs";
2826
3162
  import { execFile } from "child_process";
2827
3163
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2828
3164
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2829
3165
  import { getProfile as getProfile5 } from "@hap/core";
2830
- var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join6(homedir6(), ".suveren");
2831
- var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join6(DEFAULT_DATA_DIR2, "integrations");
2832
- var INTEGRATIONS_BIN = join6(INTEGRATIONS_DIR, "node_modules", ".bin");
3166
+ var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join7(homedir7(), ".suveren");
3167
+ var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join7(DEFAULT_DATA_DIR2, "integrations");
3168
+ var INTEGRATIONS_BIN = join7(INTEGRATIONS_DIR, "node_modules", ".bin");
2833
3169
  function buildPath() {
2834
3170
  const base = process.env.PATH ?? "";
2835
3171
  return [INTEGRATIONS_BIN, base].join(delimiter);
2836
3172
  }
2837
3173
  function ensureIntegrationsDir() {
2838
- if (!existsSync6(INTEGRATIONS_DIR)) {
2839
- mkdirSync5(INTEGRATIONS_DIR, { recursive: true });
3174
+ if (!existsSync7(INTEGRATIONS_DIR)) {
3175
+ mkdirSync6(INTEGRATIONS_DIR, { recursive: true });
2840
3176
  }
2841
- const pkgPath = join6(INTEGRATIONS_DIR, "package.json");
2842
- if (!existsSync6(pkgPath)) {
2843
- writeFileSync5(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
3177
+ const pkgPath = join7(INTEGRATIONS_DIR, "package.json");
3178
+ if (!existsSync7(pkgPath)) {
3179
+ writeFileSync6(pkgPath, JSON.stringify({ name: "suveren-integrations", version: "1.0.0", private: true }, null, 2));
2844
3180
  }
2845
3181
  }
2846
3182
  function readAgeOf(config) {
@@ -2869,6 +3205,24 @@ var IntegrationManager = class {
2869
3205
  }
2870
3206
  running = /* @__PURE__ */ new Map();
2871
3207
  onToolsChanged = null;
3208
+ /**
3209
+ * Per-integration operation queue. Start/stop for one id are serialized
3210
+ * through here; without it, a second start issued while the first is still
3211
+ * installing/handshaking sees `running.has(id) === false`, skips the stop,
3212
+ * and both children come up — last `running.set` wins, the loser's process
3213
+ * leaks, and the surviving GATING is whichever start happened to finish
3214
+ * last. That last-writer-wins gating is a fail-open: a permissive manifest
3215
+ * config can silently replace a stricter explicit one (observed as the
3216
+ * read-gate value-mismatch flake).
3217
+ */
3218
+ opQueues = /* @__PURE__ */ new Map();
3219
+ /** Run `task` after every previously queued operation for `id` has settled. */
3220
+ runExclusive(id, task) {
3221
+ const prev = this.opQueues.get(id) ?? Promise.resolve();
3222
+ const run = prev.then(task, task);
3223
+ this.opQueues.set(id, run.then(() => void 0, () => void 0));
3224
+ return run;
3225
+ }
2872
3226
  /**
2873
3227
  * Register a callback invoked when the tool set changes
2874
3228
  * (integration started, stopped, or crashed).
@@ -2888,19 +3242,19 @@ var IntegrationManager = class {
2888
3242
  * exist, and treat anything else as not-installed (and reinstallable).
2889
3243
  */
2890
3244
  isUsableInstall(npmPackage) {
2891
- const pkgDir = join6(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
2892
- const pkgJson = join6(pkgDir, "package.json");
2893
- if (!existsSync6(pkgDir) || !existsSync6(pkgJson)) return false;
3245
+ const pkgDir = join7(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3246
+ const pkgJson = join7(pkgDir, "package.json");
3247
+ if (!existsSync7(pkgDir) || !existsSync7(pkgJson)) return false;
2894
3248
  try {
2895
- const pkg = JSON.parse(readFileSync6(pkgJson, "utf8"));
3249
+ const pkg = JSON.parse(readFileSync7(pkgJson, "utf8"));
2896
3250
  const defaultBinName = (pkg.name ?? npmPackage).split("/").pop() ?? "";
2897
3251
  const binMap = typeof pkg.bin === "string" ? { [defaultBinName]: pkg.bin } : pkg.bin ?? {};
2898
3252
  const binEntries = Object.entries(binMap);
2899
3253
  for (const [binName, target] of binEntries) {
2900
- if (!existsSync6(join6(pkgDir, target))) return false;
3254
+ if (!existsSync7(join7(pkgDir, target))) return false;
2901
3255
  if (!this.binShimExists(binName)) return false;
2902
3256
  }
2903
- if (binEntries.length === 0 && pkg.main && !existsSync6(join6(pkgDir, pkg.main))) return false;
3257
+ if (binEntries.length === 0 && pkg.main && !existsSync7(join7(pkgDir, pkg.main))) return false;
2904
3258
  return true;
2905
3259
  } catch {
2906
3260
  return false;
@@ -2914,7 +3268,7 @@ var IntegrationManager = class {
2914
3268
  binShimExists(binName) {
2915
3269
  if (!binName) return false;
2916
3270
  const candidates = process.platform === "win32" ? [binName, `${binName}.cmd`, `${binName}.CMD`, `${binName}.ps1`] : [binName];
2917
- return candidates.some((c) => existsSync6(join6(INTEGRATIONS_BIN, c)));
3271
+ return candidates.some((c) => existsSync7(join7(INTEGRATIONS_BIN, c)));
2918
3272
  }
2919
3273
  /**
2920
3274
  * Install an npm package into the managed integrations directory if not
@@ -2931,8 +3285,8 @@ var IntegrationManager = class {
2931
3285
  async ensureInstalled(npmPackage) {
2932
3286
  ensureIntegrationsDir();
2933
3287
  if (this.isUsableInstall(npmPackage)) return;
2934
- const pkgDir = join6(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
2935
- if (existsSync6(pkgDir)) {
3288
+ const pkgDir = join7(INTEGRATIONS_DIR, "node_modules", ...npmPackage.split("/"));
3289
+ if (existsSync7(pkgDir)) {
2936
3290
  console.error(`[IntegrationManager] Removing unusable install of ${npmPackage}`);
2937
3291
  try {
2938
3292
  rmSync(pkgDir, { recursive: true, force: true });
@@ -3018,10 +3372,19 @@ var IntegrationManager = class {
3018
3372
  * Start a downstream MCP server integration.
3019
3373
  * Installs npm package on-demand if needed, resolves envKeys,
3020
3374
  * spawns the process, connects as MCP client, and discovers tools.
3375
+ *
3376
+ * Serialized per id: a start issued while another start/stop for the same
3377
+ * id is in flight waits for it, then applies its own semantics (restart, or
3378
+ * keep-if-running with `skipIfRunning`). See `opQueues`.
3021
3379
  */
3022
- async startIntegration(config) {
3023
- if (this.running.has(config.id)) {
3024
- await this.stopIntegration(config.id);
3380
+ async startIntegration(config, opts = {}) {
3381
+ return this.runExclusive(config.id, () => this.startIntegrationLocked(config, opts));
3382
+ }
3383
+ async startIntegrationLocked(config, opts) {
3384
+ const existing = this.running.get(config.id);
3385
+ if (existing) {
3386
+ if (opts.skipIfRunning) return existing.tools;
3387
+ await this.stopIntegrationLocked(config.id);
3025
3388
  }
3026
3389
  if (config.npmPackage) {
3027
3390
  await this.ensureInstalled(config.npmPackage);
@@ -3076,6 +3439,7 @@ var IntegrationManager = class {
3076
3439
  };
3077
3440
  this.running.set(config.id, entry);
3078
3441
  transport.onclose = () => {
3442
+ if (this.running.get(config.id)?.transport !== transport) return;
3079
3443
  console.error(`[IntegrationManager] Transport closed for ${config.id}`);
3080
3444
  this.handleCrash(config.id);
3081
3445
  };
@@ -3084,8 +3448,15 @@ var IntegrationManager = class {
3084
3448
  }
3085
3449
  /**
3086
3450
  * Stop a running integration, closing its transport and removing its tools.
3451
+ *
3452
+ * Serialized per id: a stop issued while a start is in flight waits and
3453
+ * then stops the just-started instance, instead of silently doing nothing
3454
+ * because the entry wasn't in `running` yet.
3087
3455
  */
3088
3456
  async stopIntegration(id) {
3457
+ return this.runExclusive(id, () => this.stopIntegrationLocked(id));
3458
+ }
3459
+ async stopIntegrationLocked(id) {
3089
3460
  const entry = this.running.get(id);
3090
3461
  if (!entry) return;
3091
3462
  entry.transport.onclose = void 0;
@@ -3308,7 +3679,7 @@ var IntegrationManager = class {
3308
3679
  console.error(`[IntegrationManager] ${id} crashed, respawning in ${delay}ms (attempt ${attempts + 1}/${MAX_RESPAWN_ATTEMPTS})`);
3309
3680
  setTimeout(async () => {
3310
3681
  try {
3311
- await this.startIntegration(entry.config);
3682
+ await this.startIntegration(entry.config, { skipIfRunning: true });
3312
3683
  const newEntry = this.running.get(id);
3313
3684
  if (newEntry) {
3314
3685
  newEntry.respawnAttempts = attempts + 1;
@@ -3330,28 +3701,28 @@ var IntegrationManager = class {
3330
3701
  };
3331
3702
 
3332
3703
  // src/lib/profile-loader.ts
3333
- import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
3334
- import { join as join7, resolve } from "path";
3704
+ import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
3705
+ import { join as join8, resolve } from "path";
3335
3706
  import { registerProfile } from "@hap/core";
3336
3707
  function loadProfiles(profilesDir) {
3337
- const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join7(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
3338
- const indexPath = join7(dir, "index.json");
3339
- if (!existsSync7(indexPath)) {
3708
+ const dir = resolve(profilesDir ?? process.env.SUVEREN_PROFILES_DIR ?? join8(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "..", "hap-profiles"));
3709
+ const indexPath = join8(dir, "index.json");
3710
+ if (!existsSync8(indexPath)) {
3340
3711
  warnNoProfiles(`No index.json at ${indexPath}`);
3341
3712
  return 0;
3342
3713
  }
3343
3714
  let index;
3344
3715
  try {
3345
- index = JSON.parse(readFileSync7(indexPath, "utf-8"));
3716
+ index = JSON.parse(readFileSync8(indexPath, "utf-8"));
3346
3717
  } catch (err) {
3347
3718
  console.error(`[ProfileLoader] Failed to parse ${indexPath}:`, err);
3348
3719
  return 0;
3349
3720
  }
3350
3721
  let loaded = 0;
3351
3722
  for (const [profileId, relativePath] of Object.entries(index.profiles)) {
3352
- const profilePath = join7(dir, relativePath);
3723
+ const profilePath = join8(dir, relativePath);
3353
3724
  try {
3354
- const profile = JSON.parse(readFileSync7(profilePath, "utf-8"));
3725
+ const profile = JSON.parse(readFileSync8(profilePath, "utf-8"));
3355
3726
  registerProfile(profileId, profile);
3356
3727
  const shortName = profileId.split("/").pop()?.replace(/@.*$/, "");
3357
3728
  if (shortName && shortName !== profileId) {
@@ -3384,30 +3755,30 @@ function warnNoProfiles(reason) {
3384
3755
  }
3385
3756
 
3386
3757
  // src/lib/manifest-loader.ts
3387
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
3388
- import { join as join8, resolve as resolve2 } from "path";
3758
+ import { readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
3759
+ import { join as join9, resolve as resolve2 } from "path";
3389
3760
  var manifests = /* @__PURE__ */ new Map();
3390
3761
  function loadManifests(integrationsDir) {
3391
3762
  const dir = resolve2(
3392
- integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join8(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3763
+ integrationsDir ?? process.env.SUVEREN_MANIFESTS_DIR ?? join9(import.meta.dirname ?? __dirname, "..", "..", "..", "..", "content", "integrations")
3393
3764
  );
3394
- const indexPath = join8(dir, "index.json");
3395
- if (!existsSync8(indexPath)) {
3765
+ const indexPath = join9(dir, "index.json");
3766
+ if (!existsSync9(indexPath)) {
3396
3767
  console.error(`[ManifestLoader] No index.json found at ${indexPath}, skipping manifest loading`);
3397
3768
  return 0;
3398
3769
  }
3399
3770
  let index;
3400
3771
  try {
3401
- index = JSON.parse(readFileSync8(indexPath, "utf-8"));
3772
+ index = JSON.parse(readFileSync9(indexPath, "utf-8"));
3402
3773
  } catch (err) {
3403
3774
  console.error(`[ManifestLoader] Failed to parse ${indexPath}:`, err);
3404
3775
  return 0;
3405
3776
  }
3406
3777
  let loaded = 0;
3407
3778
  for (const [id, relativePath] of Object.entries(index.integrations)) {
3408
- const manifestPath = join8(dir, relativePath);
3779
+ const manifestPath = join9(dir, relativePath);
3409
3780
  try {
3410
- const manifest = JSON.parse(readFileSync8(manifestPath, "utf-8"));
3781
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf-8"));
3411
3782
  manifests.set(id, manifest);
3412
3783
  loaded++;
3413
3784
  } catch (err) {
@@ -3425,6 +3796,7 @@ function getAllManifests() {
3425
3796
  }
3426
3797
 
3427
3798
  // bin/http.ts
3799
+ import { decodeAttestationBlob as decodeAttestationBlob3 } from "@hap/core";
3428
3800
  var spUrl = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
3429
3801
  var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
3430
3802
  var state = new SharedState(spUrl);
@@ -3488,7 +3860,8 @@ app.post("/internal/configure", internalOnly, (req, res) => {
3488
3860
  const key = Buffer.from(vaultKeyHex, "hex");
3489
3861
  state.gateStore.setVaultKey(key);
3490
3862
  state.denialLog.setVaultKey(key);
3491
- console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log encryption active");
3863
+ state.receiptArchive.setVaultKey(key);
3864
+ console.error("[Suveren MCP] Vault key configured \u2014 gate store + denial log + receipt archive encryption active");
3492
3865
  }
3493
3866
  if (apiKey) {
3494
3867
  state.spClient.setApiKey(apiKey);
@@ -3785,6 +4158,27 @@ app.get("/internal/authorizations", internalOnly, (_req, res) => {
3785
4158
  app.get("/internal/manifests", internalOnly, (_req, res) => {
3786
4159
  res.json({ manifests: getAllManifests() });
3787
4160
  });
4161
+ app.get("/internal/evidence", internalOnly, (_req, res) => {
4162
+ if (state.receiptArchive.isLocked()) {
4163
+ res.status(503).json({ error: "Vault locked \u2014 the evidence archive is encrypted until sign-in." });
4164
+ return;
4165
+ }
4166
+ const authorizations = state.receiptArchive.getAuthorizations().map((a) => ({
4167
+ ...a,
4168
+ attestations: a.attestations.map((att) => {
4169
+ try {
4170
+ return { ...att, decoded: decodeAttestationBlob3(att.blob) };
4171
+ } catch {
4172
+ return att;
4173
+ }
4174
+ })
4175
+ }));
4176
+ res.json({
4177
+ receipts: state.receiptArchive.getReceipts(),
4178
+ authorizations,
4179
+ gates: state.gateStore.getAll()
4180
+ });
4181
+ });
3788
4182
  app.get("/internal/brief", internalOnly, (_req, res) => {
3789
4183
  try {
3790
4184
  const enriched = state.getEnrichedAuthorizations();
@@ -3833,7 +4227,7 @@ async function startOneIntegration(config) {
3833
4227
  const manifest = getManifest(config.id);
3834
4228
  const effectiveConfig = manifest ? { ...config, toolGating: manifest.toolGating, npmPackage: manifest.npmPackage ?? config.npmPackage } : config;
3835
4229
  try {
3836
- await integrationManager.startIntegration(effectiveConfig);
4230
+ await integrationManager.startIntegration(effectiveConfig, { skipIfRunning: true });
3837
4231
  } catch (err) {
3838
4232
  console.error(`[Suveren MCP] Failed to start integration ${config.id}:`, err);
3839
4233
  }