@private.me/xbind 3.0.1 → 3.0.2

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 (210) hide show
  1. package/README.md +55 -14
  2. package/dist-standalone/_deps/mldsa-wasm/dist/mldsa.js +1920 -1
  3. package/dist-standalone/_deps/shared/cjs/errors.js +729 -1
  4. package/dist-standalone/_deps/shared/cjs/index.js +463 -1
  5. package/dist-standalone/_deps/shared/cjs/types.js +315 -1
  6. package/dist-standalone/_deps/shared/errors.js +244 -1
  7. package/dist-standalone/_deps/shared/index.js +72 -1
  8. package/dist-standalone/_deps/shared/types.js +86 -1
  9. package/dist-standalone/_deps/ux-helpers/cjs/errors.js +1 -1
  10. package/dist-standalone/_deps/ux-helpers/cjs/index.js +1 -1
  11. package/dist-standalone/_deps/ux-helpers/cjs/pagination.js +1 -1
  12. package/dist-standalone/_deps/ux-helpers/cjs/progress.js +1 -1
  13. package/dist-standalone/_deps/ux-helpers/cjs/search.js +1 -1
  14. package/dist-standalone/_deps/ux-helpers/cjs/types.js +1 -1
  15. package/dist-standalone/_deps/ux-helpers/errors.js +1 -1
  16. package/dist-standalone/_deps/ux-helpers/index.js +1 -1
  17. package/dist-standalone/_deps/ux-helpers/pagination.js +1 -1
  18. package/dist-standalone/_deps/ux-helpers/progress.js +1 -1
  19. package/dist-standalone/_deps/ux-helpers/search.js +1 -1
  20. package/dist-standalone/_deps/xchange/auto-accept.js +1 -1
  21. package/dist-standalone/_deps/xchange/cjs/auto-accept.js +1 -1
  22. package/dist-standalone/_deps/xchange/cjs/errors.js +1 -1
  23. package/dist-standalone/_deps/xchange/cjs/index.js +1 -1
  24. package/dist-standalone/_deps/xchange/cjs/invite-client.js +1 -1
  25. package/dist-standalone/_deps/xchange/cjs/lazy-init.js +1 -1
  26. package/dist-standalone/_deps/xchange/cjs/trust-integration.js +1 -1
  27. package/dist-standalone/_deps/xchange/cjs/xchange.js +1 -1
  28. package/dist-standalone/_deps/xchange/errors.js +1 -1
  29. package/dist-standalone/_deps/xchange/index.js +1 -1
  30. package/dist-standalone/_deps/xchange/invite-client.js +1 -1
  31. package/dist-standalone/_deps/xchange/lazy-init.js +1 -1
  32. package/dist-standalone/_deps/xchange/trust-integration.js +1 -1
  33. package/dist-standalone/_deps/xchange/xchange.js +1 -1
  34. package/dist-standalone/_deps/xregistry/cjs/discovery.js +1 -1
  35. package/dist-standalone/_deps/xregistry/cjs/errors.js +1 -1
  36. package/dist-standalone/_deps/xregistry/cjs/index.js +1 -1
  37. package/dist-standalone/_deps/xregistry/cjs/registry.js +1 -1
  38. package/dist-standalone/_deps/xregistry/cjs/schema.js +1 -1
  39. package/dist-standalone/_deps/xregistry/cjs/types.js +1 -1
  40. package/dist-standalone/_deps/xregistry/discovery.js +1 -1
  41. package/dist-standalone/_deps/xregistry/errors.js +1 -1
  42. package/dist-standalone/_deps/xregistry/index.js +1 -1
  43. package/dist-standalone/_deps/xregistry/registry.js +1 -1
  44. package/dist-standalone/_deps/xregistry/schema.js +1 -1
  45. package/dist-standalone/_deps/xregistry/types.js +1 -1
  46. package/dist-standalone/agent-call.js +659 -1
  47. package/dist-standalone/agent-sdk.js +328 -1
  48. package/dist-standalone/agent.js +1800 -1
  49. package/dist-standalone/approval.js +193 -1
  50. package/dist-standalone/async-iterators.js +382 -1
  51. package/dist-standalone/auth.js +219 -1
  52. package/dist-standalone/auto-accept.js +229 -1
  53. package/dist-standalone/backup-config.js +201 -1
  54. package/dist-standalone/backup.js +326 -1
  55. package/dist-standalone/batch-operations.js +388 -1
  56. package/dist-standalone/cancellation.js +477 -1
  57. package/dist-standalone/checkpoint.js +186 -1
  58. package/dist-standalone/circuit-breaker.js +468 -1
  59. package/dist-standalone/cjs/agent-call.js +701 -1
  60. package/dist-standalone/cjs/agent-sdk.js +332 -1
  61. package/dist-standalone/cjs/agent.js +1837 -1
  62. package/dist-standalone/cjs/approval.js +199 -1
  63. package/dist-standalone/cjs/async-iterators.js +392 -1
  64. package/dist-standalone/cjs/auth.js +225 -1
  65. package/dist-standalone/cjs/auto-accept.js +233 -1
  66. package/dist-standalone/cjs/backup-config.js +207 -1
  67. package/dist-standalone/cjs/backup.js +330 -1
  68. package/dist-standalone/cjs/batch-operations.js +397 -1
  69. package/dist-standalone/cjs/cancellation.js +490 -1
  70. package/dist-standalone/cjs/checkpoint.js +193 -1
  71. package/dist-standalone/cjs/circuit-breaker.js +476 -1
  72. package/dist-standalone/cjs/cli/init.js +492 -1
  73. package/dist-standalone/cjs/config-validation.js +522 -1
  74. package/dist-standalone/cjs/connect.js +312 -1
  75. package/dist-standalone/cjs/connection-pool.js +506 -1
  76. package/dist-standalone/cjs/correlation-id.js +339 -1
  77. package/dist-standalone/cjs/crypto-utils.js +176 -1
  78. package/dist-standalone/cjs/debug-mode.js +534 -1
  79. package/dist-standalone/cjs/did-document.js +101 -1
  80. package/dist-standalone/cjs/did-privateme.js +130 -1
  81. package/dist-standalone/cjs/did-web.js +201 -1
  82. package/dist-standalone/cjs/discovery.js +462 -1
  83. package/dist-standalone/cjs/dual-mode.js +251 -1
  84. package/dist-standalone/cjs/email-templates.js +313 -1
  85. package/dist-standalone/cjs/email-transport.js +239 -1
  86. package/dist-standalone/cjs/envelope.js +538 -1
  87. package/dist-standalone/cjs/errors.js +913 -1
  88. package/dist-standalone/cjs/event-emitter.js +461 -1
  89. package/dist-standalone/cjs/gateway-state.js +55 -1
  90. package/dist-standalone/cjs/gateway-transport.js +120 -1
  91. package/dist-standalone/cjs/graceful-degradation.js +403 -1
  92. package/dist-standalone/cjs/guardrails.js +223 -1
  93. package/dist-standalone/cjs/health-check.js +336 -1
  94. package/dist-standalone/cjs/http-compat.js +272 -1
  95. package/dist-standalone/cjs/http-status-map.js +571 -1
  96. package/dist-standalone/cjs/identity.js +645 -1
  97. package/dist-standalone/cjs/index.js +406 -1
  98. package/dist-standalone/cjs/invitation.js +421 -1
  99. package/dist-standalone/cjs/invite.js +328 -1
  100. package/dist-standalone/cjs/key-agreement.js +335 -1
  101. package/dist-standalone/cjs/lazy-init.js +300 -1
  102. package/dist-standalone/cjs/logger.js +291 -1
  103. package/dist-standalone/cjs/mdns-discovery.js +202 -1
  104. package/dist-standalone/cjs/nonce-store.js +80 -1
  105. package/dist-standalone/cjs/pairing-manager.js +223 -1
  106. package/dist-standalone/cjs/plugin-system.js +264 -1
  107. package/dist-standalone/cjs/plugins/logging.js +168 -1
  108. package/dist-standalone/cjs/plugins/metrics.js +181 -1
  109. package/dist-standalone/cjs/plugins/validation.js +302 -1
  110. package/dist-standalone/cjs/policy.js +320 -1
  111. package/dist-standalone/cjs/progress-callbacks.js +583 -1
  112. package/dist-standalone/cjs/redis-nonce-store.js +76 -1
  113. package/dist-standalone/cjs/registry-middleware.js +50 -1
  114. package/dist-standalone/cjs/retry-strategies.js +544 -1
  115. package/dist-standalone/cjs/retry-transport.js +102 -1
  116. package/dist-standalone/cjs/runtime/browser.js +533 -1
  117. package/dist-standalone/cjs/runtime/edge.js +526 -1
  118. package/dist-standalone/cjs/runtime/react-native.js +394 -1
  119. package/dist-standalone/cjs/security-policy.js +245 -1
  120. package/dist-standalone/cjs/serialization.js +1040 -1
  121. package/dist-standalone/cjs/split-channel.js +225 -1
  122. package/dist-standalone/cjs/subscription-proof.js +230 -1
  123. package/dist-standalone/cjs/succession.js +148 -1
  124. package/dist-standalone/cjs/timeouts.js +412 -1
  125. package/dist-standalone/cjs/trace-context.js +424 -1
  126. package/dist-standalone/cjs/trace-spans.js +495 -1
  127. package/dist-standalone/cjs/transport.js +63 -1
  128. package/dist-standalone/cjs/trust-registry.js +991 -1
  129. package/dist-standalone/cjs/types/error-response.js +56 -1
  130. package/dist-standalone/cjs/vault-auth.js +178 -1
  131. package/dist-standalone/cjs/vault-store-loader.js +194 -1
  132. package/dist-standalone/cjs/verify.js +25 -1
  133. package/dist-standalone/cjs/version-info.js +543 -1
  134. package/dist-standalone/cjs/xfetch.js +340 -1
  135. package/dist-standalone/cli/init.js +455 -1
  136. package/dist-standalone/cli/setup.js +514 -1
  137. package/dist-standalone/cli/types.js +27 -1
  138. package/dist-standalone/cli/xbind.js +148 -1
  139. package/dist-standalone/config-validation.js +513 -1
  140. package/dist-standalone/connect.js +274 -1
  141. package/dist-standalone/connection-pool.js +500 -1
  142. package/dist-standalone/correlation-id.js +326 -1
  143. package/dist-standalone/crypto-utils.js +157 -1
  144. package/dist-standalone/debug-mode.js +510 -1
  145. package/dist-standalone/did-document.js +96 -1
  146. package/dist-standalone/did-privateme.js +121 -1
  147. package/dist-standalone/did-web.js +196 -1
  148. package/dist-standalone/discovery.js +458 -1
  149. package/dist-standalone/dual-mode.js +247 -1
  150. package/dist-standalone/email-templates.js +309 -1
  151. package/dist-standalone/email-transport.js +232 -1
  152. package/dist-standalone/envelope.js +525 -1
  153. package/dist-standalone/errors.js +896 -1
  154. package/dist-standalone/event-emitter.js +456 -1
  155. package/dist-standalone/gateway-state.js +51 -1
  156. package/dist-standalone/gateway-transport.js +116 -1
  157. package/dist-standalone/graceful-degradation.js +396 -1
  158. package/dist-standalone/guardrails.js +216 -1
  159. package/dist-standalone/health-check.js +332 -1
  160. package/dist-standalone/http-compat.js +267 -1
  161. package/dist-standalone/http-status-map.js +561 -1
  162. package/dist-standalone/identity.js +619 -1
  163. package/dist-standalone/index.js +78 -1
  164. package/dist-standalone/invitation.js +415 -1
  165. package/dist-standalone/invite.js +324 -1
  166. package/dist-standalone/key-agreement.js +325 -1
  167. package/dist-standalone/lazy-init.js +295 -1
  168. package/dist-standalone/logger.js +285 -1
  169. package/dist-standalone/mdns-discovery.js +195 -1
  170. package/dist-standalone/nonce-store.js +76 -1
  171. package/dist-standalone/pairing-manager.js +219 -1
  172. package/dist-standalone/plugin-system.js +257 -1
  173. package/dist-standalone/plugins/logging.js +163 -1
  174. package/dist-standalone/plugins/metrics.js +176 -1
  175. package/dist-standalone/plugins/validation.js +297 -1
  176. package/dist-standalone/policy.js +315 -1
  177. package/dist-standalone/progress-callbacks.js +576 -1
  178. package/dist-standalone/redis-nonce-store.js +72 -1
  179. package/dist-standalone/registry-middleware.js +47 -1
  180. package/dist-standalone/retry-strategies.js +534 -1
  181. package/dist-standalone/retry-transport.js +98 -1
  182. package/dist-standalone/runtime/browser.js +516 -1
  183. package/dist-standalone/runtime/edge.js +511 -1
  184. package/dist-standalone/runtime/react-native.js +383 -1
  185. package/dist-standalone/security-policy.js +239 -1
  186. package/dist-standalone/serialization.js +1031 -1
  187. package/dist-standalone/split-channel.js +219 -1
  188. package/dist-standalone/subscription-proof.js +224 -1
  189. package/dist-standalone/succession.js +142 -1
  190. package/dist-standalone/timeouts.js +398 -1
  191. package/dist-standalone/trace-context.js +414 -1
  192. package/dist-standalone/trace-spans.js +488 -1
  193. package/dist-standalone/transport.js +59 -1
  194. package/dist-standalone/trust-registry.js +950 -1
  195. package/dist-standalone/types/error-response.js +52 -1
  196. package/dist-standalone/vault-auth.js +174 -1
  197. package/dist-standalone/vault-store-loader.js +187 -1
  198. package/dist-standalone/verify.js +16 -1
  199. package/dist-standalone/version-info.js +530 -1
  200. package/dist-standalone/xfetch.js +335 -1
  201. package/package.json +4 -13
  202. package/share1.dat +0 -0
  203. package/dist-standalone/_deps/mldsa-wasm/LICENSE +0 -24
  204. package/dist-standalone/_deps/mldsa-wasm/package.json +0 -46
  205. package/dist-standalone/_deps/shared/cjs/package.json +0 -1
  206. package/dist-standalone/_deps/ux-helpers/cjs/package.json +0 -1
  207. package/dist-standalone/_deps/xchange/cjs/package.json +0 -1
  208. package/dist-standalone/_deps/xregistry/cjs/package.json +0 -1
  209. package/dist-standalone/cjs/package.json +0 -3
  210. package/dist-standalone/package.json +0 -10
@@ -1 +1,950 @@
1
- import{ok,err}from"./_deps/shared/index.js";import*as fs from"node:fs/promises";import*as path from"node:path";function isExpired(e){return!!e.expiresAt&&Date.now()>e.expiresAt}export class RegistrationRateLimiter{perIPTimestamps=new Map;globalTimestamps=[];perIPLimit;globalLimit;windowMs;cleanupInterval=null;constructor(e=10,t=1e3,i=36e5){this.perIPLimit=e,this.globalLimit=t,this.windowMs=i,this.cleanupInterval=setInterval(()=>this.cleanup(),3e5)}checkLimit(e){const t=Date.now()-this.windowMs;if((this.perIPTimestamps.get(e)||[]).filter(e=>e>t).length>=this.perIPLimit)return!1;return!(this.globalTimestamps.filter(e=>e>t).length>=this.globalLimit)}recordRegistration(e){const t=Date.now(),i=this.perIPTimestamps.get(e)||[];i.push(t),this.perIPTimestamps.set(e,i),this.globalTimestamps.push(t)}getRemainingForIP(e){const t=Date.now()-this.windowMs,i=(this.perIPTimestamps.get(e)||[]).filter(e=>e>t);return Math.max(0,this.perIPLimit-i.length)}getRemainingGlobal(){const e=Date.now()-this.windowMs,t=this.globalTimestamps.filter(t=>t>e);return Math.max(0,this.globalLimit-t.length)}getResetTimeForIP(e){const t=this.perIPTimestamps.get(e)||[];if(0===t.length)return null;const i=t[0];return i?i+this.windowMs:null}cleanup(){const e=Date.now()-this.windowMs;for(const[t,i]of this.perIPTimestamps.entries()){const s=i.filter(t=>t>e);0===s.length?this.perIPTimestamps.delete(t):this.perIPTimestamps.set(t,s)}const t=this.globalTimestamps.filter(t=>t>e);this.globalTimestamps.length=0,this.globalTimestamps.push(...t)}destroy(){this.cleanupInterval&&(clearInterval(this.cleanupInterval),this.cleanupInterval=null)}reset(){this.perIPTimestamps.clear(),this.globalTimestamps.length=0}}export class MemoryTrustRegistry{entries=new Map;rateLimiter;constructor(e){e?.enableRateLimiting&&(this.rateLimiter=e.rateLimiter||new RegistrationRateLimiter)}async register(e,t,i,s,r,n,o,a,c,l,h,p,u,d){if(this.rateLimiter&&d&&!this.rateLimiter.checkLimit(d))return err("RATE_LIMIT_EXCEEDED");if(this.entries.has(e))return err("ALREADY_REGISTERED");const y=u?Date.now()+u:void 0;return this.entries.set(e,{did:e,publicKey:t,name:i,scopes:new Set(s??[]),receiveScopes:c?new Set(c):void 0,revoked:!1,rotation_sequence:1,x25519PublicKey:r,mlKemPublicKey:n,mlDsaPublicKey:o,xchange:a,sdkVersion:l,minEnvelopeVersion:h,maxEnvelopeVersion:p,expiresAt:y}),this.rateLimiter&&d&&this.rateLimiter.recordRegistration(d),ok(void 0)}async resolve(e){const t=this.entries.get(e);return t?isExpired(t)?err("EXPIRED"):t.revoked?err("REVOKED"):ok(t.publicKey):err("NOT_FOUND")}async hasScope(e,t){const i=this.entries.get(e);return!(!i||isExpired(i)||i.revoked)&&i.scopes.has(t)}async hasReceiveScope(e,t){const i=this.entries.get(e);return!(!i||isExpired(i)||i.revoked)&&(!i.receiveScopes||i.receiveScopes.has(t))}async revoke(e){const t=this.entries.get(e);return t?(this.entries.set(e,{...t,revoked:!0}),ok(void 0)):err("NOT_FOUND")}async getEntry(e){const t=this.entries.get(e);return t?isExpired(t)?err("EXPIRED"):ok(t):err("NOT_FOUND")}async updateScopes(e,t){const i=this.entries.get(e);return i?isExpired(i)?err("EXPIRED"):i.revoked?err("REVOKED"):(this.entries.set(e,{...i,scopes:new Set(t)}),ok(void 0)):err("NOT_FOUND")}async cleanup(){let e=0;const t=Date.now();for(const[i,s]of this.entries)s.expiresAt&&t>s.expiresAt&&(this.entries.delete(i),e++);return e}get size(){return this.entries.size}}export class HttpTrustRegistry{baseUrl;fetchFn;cacheTtlMs;cacheFailureMode;enablePush;bloomFilterSize;bloomFilterFpr;resolveCache=new Map;entryCache=new Map;constructor(e){this.baseUrl=e.baseUrl.replace(/\/$/,""),this.fetchFn=e.fetch??globalThis.fetch.bind(globalThis),this.cacheTtlMs=e.cacheTtlMs??3e4,this.cacheFailureMode=e.cacheFailureMode??"fail-secure",this.enablePush=e.enablePush??!1,this.bloomFilterSize=e.bloomFilterSize??1e4,this.bloomFilterFpr=e.bloomFilterFpr??.01}clearCache(){this.resolveCache.clear(),this.entryCache.clear()}async register(e,t,i,s,r,n,o,a,c,l,h,p,u,d){try{const d=await this.fetchFn(`${this.baseUrl}/registry/register`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({did:e,publicKey:Array.from(t),name:i,scopes:s??[],...c?{receiveScopes:c}:{},...r?{x25519PublicKey:Array.from(r)}:{},...n?{mlKemPublicKey:Array.from(n)}:{},...o?{mlDsaPublicKey:Array.from(o)}:{},...void 0!==a?{xchange:a}:{},...void 0!==l?{sdkVersion:l}:{},...void 0!==h?{minEnvelopeVersion:h}:{},...void 0!==p?{maxEnvelopeVersion:p}:{},...void 0!==u?{ttlMs:u}:{}})});return 409===d.status?err("ALREADY_REGISTERED"):429===d.status?err("RATE_LIMIT_EXCEEDED"):d.ok?ok(void 0):err("NETWORK_ERROR")}catch{return err("NETWORK_ERROR")}}async resolve(e){if(this.cacheTtlMs>0){const t=this.resolveCache.get(e);if(t&&t.expiry>Date.now())return t.value}let t;try{const i=await this.fetchFn(`${this.baseUrl}/registry/resolve/${encodeURIComponent(e)}`);if(404===i.status)t=err("NOT_FOUND");else if(408===i.status)t=err("EXPIRED");else if(410===i.status)t=err("REVOKED");else if(i.ok){const e=await i.json();t=ok(new Uint8Array(e.publicKey))}else t=err("NETWORK_ERROR")}catch{const i=this.resolveCache.get(e);if("fail-secure"===this.cacheFailureMode)t=err("NETWORK_ERROR");else{if(i)return i.value;t=err("NETWORK_ERROR")}}return this.cacheTtlMs>0&&this.resolveCache.set(e,{value:t,expiry:Date.now()+this.cacheTtlMs}),t}async hasScope(e,t){try{return(await this.fetchFn(`${this.baseUrl}/registry/scope/${encodeURIComponent(e)}/${encodeURIComponent(t)}`)).ok}catch{return!1}}async hasReceiveScope(e,t){try{return(await this.fetchFn(`${this.baseUrl}/registry/receive-scope/${encodeURIComponent(e)}/${encodeURIComponent(t)}`)).ok}catch{return!1}}async revoke(e){try{const t=await this.fetchFn(`${this.baseUrl}/registry/revoke/${encodeURIComponent(e)}`,{method:"POST"});return 404===t.status?err("NOT_FOUND"):t.ok?ok(void 0):err("NETWORK_ERROR")}catch{return err("NETWORK_ERROR")}}async getEntry(e){if(this.cacheTtlMs>0){const t=this.entryCache.get(e);if(t&&t.expiry>Date.now())return t.value}let t;try{const i=await this.fetchFn(`${this.baseUrl}/registry/entry/${encodeURIComponent(e)}`);if(404===i.status)t=err("NOT_FOUND");else if(408===i.status)t=err("EXPIRED");else if(i.ok){const e=await i.json();t=ok({did:e.did,publicKey:new Uint8Array(e.publicKey),name:e.name,scopes:new Set(e.scopes),receiveScopes:e.receiveScopes?new Set(e.receiveScopes):void 0,revoked:e.revoked,rotation_sequence:e.rotation_sequence??1,x25519PublicKey:e.x25519PublicKey?new Uint8Array(e.x25519PublicKey):void 0,mlKemPublicKey:e.mlKemPublicKey?new Uint8Array(e.mlKemPublicKey):void 0,mlDsaPublicKey:e.mlDsaPublicKey?new Uint8Array(e.mlDsaPublicKey):void 0,xchange:e.xchange,sdkVersion:e.sdkVersion,minEnvelopeVersion:e.minEnvelopeVersion,maxEnvelopeVersion:e.maxEnvelopeVersion,expiresAt:e.expiresAt})}else t=err("NETWORK_ERROR")}catch{t=err("NETWORK_ERROR")}return this.cacheTtlMs>0&&this.entryCache.set(e,{value:t,expiry:Date.now()+this.cacheTtlMs}),t}async rotate(e,t,i,s){const r=await this.fetchFn(`${this.baseUrl}/registry/rotate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({did:e,newPublicKey:Array.from(t),proof:Array.from(i),rotationSequence:s})});if(!r.ok)throw new Error(`Key rotation failed: ${r.status} ${r.statusText}`);this.resolveCache.delete(e),this.entryCache.delete(e)}async subscribe(e,t){if(!this.enablePush)throw new Error("Push notifications not enabled (set enablePush: true in HttpTrustRegistryOptions)");const i=new Set(e.map(e=>this.hashDid(e))),s=this.baseUrl.replace(/^http/,"ws")+"/trust/events",r=new globalThis.WebSocket(s);return r.addEventListener("open",()=>{r.send(JSON.stringify({type:"subscribe",dids:e,bloomSize:this.bloomFilterSize,bloomFpr:this.bloomFilterFpr}))}),r.addEventListener("message",e=>{try{const s=JSON.parse(e.data),r=this.hashDid(s.did);i.has(r)&&("revocation"!==s.type&&"succession"!==s.type||(this.resolveCache.delete(s.did),this.entryCache.delete(s.did)),t(s))}catch(e){console.warn("Failed to parse trust event:",e)}}),r.addEventListener("error",e=>{console.error("WebSocket error:",e)}),()=>{r.readyState===globalThis.WebSocket.OPEN&&r.close()}}hashDid(e){let t=0;for(let i=0;i<e.length;i++)t=(t<<5)-t+e.charCodeAt(i),t&=t;return t}async resumeSubscriptions(e){try{return(await this.fetchFn(`${this.baseUrl}/trust/resume-batch`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({proofs:e})})).ok?ok(void 0):err("NETWORK_ERROR")}catch{return err("NETWORK_ERROR")}}async fetchCheckpoint(e){try{const t=await this.fetchFn(`${this.baseUrl}/registry/checkpoint/${encodeURIComponent(e)}`);if(404===t.status)return err("NOT_FOUND");if(!t.ok)return err("NETWORK_ERROR");const i=await t.json();return ok(i)}catch{return err("NETWORK_ERROR")}}async updateScopes(e,t){try{const i=await this.fetchFn(`${this.baseUrl}/registry/${encodeURIComponent(e)}/scopes`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({scopes:t})});return 404===i.status?err("NOT_FOUND"):410===i.status?err("REVOKED"):i.ok?(this.resolveCache.delete(e),this.entryCache.delete(e),ok(void 0)):err("NETWORK_ERROR")}catch{return err("NETWORK_ERROR")}}}export class FileTrustRegistry{path;entries=new Map;initialized=!1;constructor(e){this.path=e.path}async init(){if(!this.initialized){try{await fs.mkdir(path.dirname(this.path),{recursive:!0});const e=(await fs.readFile(this.path,"utf-8").catch(()=>"")).split("\n").filter(e=>e.trim());for(const t of e){const e=JSON.parse(t);if("register"===e.type&&e.publicKey&&e.name)this.entries.set(e.did,{did:e.did,publicKey:new Uint8Array(e.publicKey),name:e.name,scopes:new Set(e.scopes??[]),receiveScopes:e.receiveScopes?new Set(e.receiveScopes):void 0,revoked:!1,rotation_sequence:e.rotation_sequence??1,x25519PublicKey:e.x25519PublicKey?new Uint8Array(e.x25519PublicKey):void 0,mlKemPublicKey:e.mlKemPublicKey?new Uint8Array(e.mlKemPublicKey):void 0,mlDsaPublicKey:e.mlDsaPublicKey?new Uint8Array(e.mlDsaPublicKey):void 0,xchange:e.xchange,sdkVersion:e.sdkVersion,minEnvelopeVersion:e.minEnvelopeVersion,maxEnvelopeVersion:e.maxEnvelopeVersion,expiresAt:e.expiresAt});else if("revoke"===e.type){const t=this.entries.get(e.did);t&&this.entries.set(e.did,{...t,revoked:!0})}else if("update-scopes"===e.type){const t=this.entries.get(e.did);t&&this.entries.set(e.did,{...t,scopes:new Set(e.scopes??[])})}else if("rotate"===e.type&&e.publicKey&&e.rotation_sequence){const t=this.entries.get(e.did);t&&e.rotation_sequence>t.rotation_sequence&&this.entries.set(e.did,{...t,publicKey:new Uint8Array(e.publicKey),rotation_sequence:e.rotation_sequence})}}}catch(e){}this.initialized=!0}}async append(e){await fs.appendFile(this.path,JSON.stringify(e)+"\n","utf-8")}async register(e,t,i,s,r,n,o,a,c,l,h,p,u,d){if(await this.init(),this.entries.has(e))return err("ALREADY_REGISTERED");const y=u?Date.now()+u:void 0,m={did:e,publicKey:t,name:i,scopes:new Set(s??[]),receiveScopes:c?new Set(c):void 0,revoked:!1,rotation_sequence:1,x25519PublicKey:r,mlKemPublicKey:n,mlDsaPublicKey:o,xchange:a,sdkVersion:l,minEnvelopeVersion:h,maxEnvelopeVersion:p,expiresAt:y};return this.entries.set(e,m),await this.append({type:"register",did:e,publicKey:Array.from(t),name:i,scopes:s??[],rotation_sequence:1,...c?{receiveScopes:c}:{},...r?{x25519PublicKey:Array.from(r)}:{},...n?{mlKemPublicKey:Array.from(n)}:{},...o?{mlDsaPublicKey:Array.from(o)}:{},...void 0!==a?{xchange:a}:{},...void 0!==l?{sdkVersion:l}:{},...void 0!==h?{minEnvelopeVersion:h}:{},...void 0!==p?{maxEnvelopeVersion:p}:{},...void 0!==y?{expiresAt:y}:{}}),ok(void 0)}async resolve(e){await this.init();const t=this.entries.get(e);return t?isExpired(t)?err("EXPIRED"):t.revoked?err("REVOKED"):ok(t.publicKey):err("NOT_FOUND")}async hasScope(e,t){await this.init();const i=this.entries.get(e);return!(!i||isExpired(i)||i.revoked)&&i.scopes.has(t)}async hasReceiveScope(e,t){await this.init();const i=this.entries.get(e);return!(!i||isExpired(i)||i.revoked)&&(!i.receiveScopes||i.receiveScopes.has(t))}async revoke(e){await this.init();const t=this.entries.get(e);return t?(this.entries.set(e,{...t,revoked:!0}),await this.append({type:"revoke",did:e}),ok(void 0)):err("NOT_FOUND")}async getEntry(e){await this.init();const t=this.entries.get(e);return t?isExpired(t)?err("EXPIRED"):ok(t):err("NOT_FOUND")}async updateScopes(e,t){await this.init();const i=this.entries.get(e);return i?isExpired(i)?err("EXPIRED"):i.revoked?err("REVOKED"):(this.entries.set(e,{...i,scopes:new Set(t)}),await this.append({type:"update-scopes",did:e,scopes:t}),ok(void 0)):err("NOT_FOUND")}async cleanup(){await this.init();let e=0;const t=Date.now();for(const[i,s]of this.entries)s.expiresAt&&t>s.expiresAt&&(this.entries.delete(i),e++);return e}async rotate(e,t,i,s){await this.init();const r=this.entries.get(e);if(!r)throw new Error(`DID not found: ${e}`);if(s<=r.rotation_sequence)throw new Error(`Rotation sequence ${s} must be > current ${r.rotation_sequence} (rollback attack prevented)`);await this.append({type:"rotate",did:e,publicKey:Array.from(t),proof:Array.from(i),rotation_sequence:s,timestamp:Date.now()}),this.entries.set(e,{...r,publicKey:t,rotation_sequence:s})}get size(){return this.entries.size}}export async function createEnterpriseTrustRegistry(e){let t;if("file"===e.storage){if(!e.path)throw new Error("FileTrustRegistry requires path option");t=new FileTrustRegistry({path:e.path})}else if("http"===e.storage){if(!e.baseUrl)throw new Error("HttpTrustRegistry requires baseUrl option");t=new HttpTrustRegistry({baseUrl:e.baseUrl})}else t=new MemoryTrustRegistry;if(e.preload)for(const i of e.preload)await t.register(i.did,i.publicKey,i.name,i.scopes,i.x25519PublicKey,i.mlKemPublicKey,i.mlDsaPublicKey,i.xchange,i.receiveScopes,i.sdkVersion,i.minEnvelopeVersion,i.maxEnvelopeVersion);return t}
1
+ import { ok, err } from"./_deps/shared/index.js";
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ /* ── Types ── */
5
+ /** Check if a registry entry has expired. */
6
+ function isExpired(entry) {
7
+ if (!entry.expiresAt)
8
+ return false;
9
+ return Date.now() > entry.expiresAt;
10
+ }
11
+ /* ── Rate Limiting ── */
12
+ /**
13
+ * Rate limiter for DID registration endpoints.
14
+ *
15
+ * Implements sliding window algorithm with:
16
+ * - Per-IP limit: 10 registrations/hour
17
+ * - Global limit: 1000 registrations/hour
18
+ *
19
+ * Prevents DID registration spam attacks (SCALE-1).
20
+ */
21
+ export class RegistrationRateLimiter {
22
+ perIPTimestamps = new Map();
23
+ globalTimestamps = [];
24
+ perIPLimit;
25
+ globalLimit;
26
+ windowMs;
27
+ cleanupInterval = null;
28
+ /**
29
+ * Create a new registration rate limiter.
30
+ *
31
+ * @param perIPLimit - Maximum registrations per IP in time window (default: 10)
32
+ * @param globalLimit - Maximum global registrations in time window (default: 1000)
33
+ * @param windowMs - Time window in milliseconds (default: 3600000 = 1 hour)
34
+ */
35
+ constructor(perIPLimit = 10, globalLimit = 1000, windowMs = 3600000) {
36
+ this.perIPLimit = perIPLimit;
37
+ this.globalLimit = globalLimit;
38
+ this.windowMs = windowMs;
39
+ // Cleanup old timestamps every 5 minutes
40
+ this.cleanupInterval = setInterval(() => this.cleanup(), 300000);
41
+ }
42
+ /**
43
+ * Check if registration is allowed for the given IP.
44
+ *
45
+ * @param ip - Client IP address
46
+ * @returns True if allowed, false if rate limited
47
+ */
48
+ checkLimit(ip) {
49
+ const now = Date.now();
50
+ const windowStart = now - this.windowMs;
51
+ // Check per-IP limit
52
+ const ipTimestamps = this.perIPTimestamps.get(ip) || [];
53
+ const recentIPRequests = ipTimestamps.filter((t) => t > windowStart);
54
+ if (recentIPRequests.length >= this.perIPLimit) {
55
+ return false;
56
+ }
57
+ // Check global limit
58
+ const recentGlobalRequests = this.globalTimestamps.filter((t) => t > windowStart);
59
+ if (recentGlobalRequests.length >= this.globalLimit) {
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+ /**
65
+ * Record a registration attempt.
66
+ *
67
+ * Call this AFTER successful registration to avoid counting failures.
68
+ *
69
+ * @param ip - Client IP address
70
+ */
71
+ recordRegistration(ip) {
72
+ const now = Date.now();
73
+ // Record per-IP
74
+ const ipTimestamps = this.perIPTimestamps.get(ip) || [];
75
+ ipTimestamps.push(now);
76
+ this.perIPTimestamps.set(ip, ipTimestamps);
77
+ // Record global
78
+ this.globalTimestamps.push(now);
79
+ }
80
+ /**
81
+ * Get remaining registrations for an IP.
82
+ *
83
+ * @param ip - Client IP address
84
+ * @returns Remaining registrations allowed
85
+ */
86
+ getRemainingForIP(ip) {
87
+ const now = Date.now();
88
+ const windowStart = now - this.windowMs;
89
+ const ipTimestamps = this.perIPTimestamps.get(ip) || [];
90
+ const recentIPRequests = ipTimestamps.filter((t) => t > windowStart);
91
+ return Math.max(0, this.perIPLimit - recentIPRequests.length);
92
+ }
93
+ /**
94
+ * Get remaining global registrations.
95
+ *
96
+ * @returns Remaining global registrations allowed
97
+ */
98
+ getRemainingGlobal() {
99
+ const now = Date.now();
100
+ const windowStart = now - this.windowMs;
101
+ const recentGlobalRequests = this.globalTimestamps.filter((t) => t > windowStart);
102
+ return Math.max(0, this.globalLimit - recentGlobalRequests.length);
103
+ }
104
+ /**
105
+ * Get reset time for an IP's rate limit.
106
+ *
107
+ * @param ip - Client IP address
108
+ * @returns Unix timestamp (ms) when limit resets, or null if not rate limited
109
+ */
110
+ getResetTimeForIP(ip) {
111
+ const ipTimestamps = this.perIPTimestamps.get(ip) || [];
112
+ if (ipTimestamps.length === 0)
113
+ return null;
114
+ const oldestTimestamp = ipTimestamps[0];
115
+ if (!oldestTimestamp)
116
+ return null;
117
+ return oldestTimestamp + this.windowMs;
118
+ }
119
+ /**
120
+ * Cleanup old timestamps to prevent memory leaks.
121
+ *
122
+ * Removes entries with no requests in the last window period.
123
+ */
124
+ cleanup() {
125
+ const now = Date.now();
126
+ const windowStart = now - this.windowMs;
127
+ // Cleanup per-IP timestamps
128
+ for (const [ip, timestamps] of this.perIPTimestamps.entries()) {
129
+ const recentRequests = timestamps.filter((t) => t > windowStart);
130
+ if (recentRequests.length === 0) {
131
+ this.perIPTimestamps.delete(ip);
132
+ }
133
+ else {
134
+ this.perIPTimestamps.set(ip, recentRequests);
135
+ }
136
+ }
137
+ // Cleanup global timestamps
138
+ const recentGlobalRequests = this.globalTimestamps.filter((t) => t > windowStart);
139
+ this.globalTimestamps.length = 0;
140
+ this.globalTimestamps.push(...recentGlobalRequests);
141
+ }
142
+ /**
143
+ * Stop cleanup interval (for testing or shutdown).
144
+ */
145
+ destroy() {
146
+ if (this.cleanupInterval) {
147
+ clearInterval(this.cleanupInterval);
148
+ this.cleanupInterval = null;
149
+ }
150
+ }
151
+ /**
152
+ * Reset all rate limits (for testing).
153
+ */
154
+ reset() {
155
+ this.perIPTimestamps.clear();
156
+ this.globalTimestamps.length = 0;
157
+ }
158
+ }
159
+ /* ── Memory Implementation ── */
160
+ /**
161
+ * In-memory trust registry for development and testing.
162
+ */
163
+ export class MemoryTrustRegistry {
164
+ entries = new Map();
165
+ rateLimiter;
166
+ constructor(opts) {
167
+ if (opts?.enableRateLimiting) {
168
+ this.rateLimiter = opts.rateLimiter || new RegistrationRateLimiter();
169
+ }
170
+ }
171
+ async register(did, publicKey, name, scopes, x25519PublicKey, mlKemPublicKey, mlDsaPublicKey, xchange, receiveScopes, sdkVersion, minEnvelopeVersion, maxEnvelopeVersion, ttlMs, clientIP) {
172
+ // Check rate limit if enabled
173
+ if (this.rateLimiter && clientIP) {
174
+ if (!this.rateLimiter.checkLimit(clientIP)) {
175
+ return err('RATE_LIMIT_EXCEEDED');
176
+ }
177
+ }
178
+ if (this.entries.has(did))
179
+ return err('ALREADY_REGISTERED');
180
+ const expiresAt = ttlMs ? Date.now() + ttlMs : undefined;
181
+ this.entries.set(did, {
182
+ did,
183
+ publicKey,
184
+ name,
185
+ scopes: new Set(scopes ?? []),
186
+ receiveScopes: receiveScopes ? new Set(receiveScopes) : undefined,
187
+ revoked: false,
188
+ rotation_sequence: 1, // Initial sequence starts at 1
189
+ x25519PublicKey,
190
+ mlKemPublicKey,
191
+ mlDsaPublicKey,
192
+ xchange,
193
+ sdkVersion,
194
+ minEnvelopeVersion,
195
+ maxEnvelopeVersion,
196
+ expiresAt,
197
+ });
198
+ // Record registration after success
199
+ if (this.rateLimiter && clientIP) {
200
+ this.rateLimiter.recordRegistration(clientIP);
201
+ }
202
+ return ok(undefined);
203
+ }
204
+ async resolve(did) {
205
+ const entry = this.entries.get(did);
206
+ if (!entry)
207
+ return err('NOT_FOUND');
208
+ if (isExpired(entry))
209
+ return err('EXPIRED');
210
+ if (entry.revoked)
211
+ return err('REVOKED');
212
+ return ok(entry.publicKey);
213
+ }
214
+ async hasScope(did, scope) {
215
+ const entry = this.entries.get(did);
216
+ if (!entry || isExpired(entry) || entry.revoked)
217
+ return false;
218
+ return entry.scopes.has(scope);
219
+ }
220
+ async hasReceiveScope(did, scope) {
221
+ const entry = this.entries.get(did);
222
+ if (!entry || isExpired(entry) || entry.revoked)
223
+ return false;
224
+ // Undefined = accept all scopes (backward compatibility)
225
+ if (!entry.receiveScopes)
226
+ return true;
227
+ return entry.receiveScopes.has(scope);
228
+ }
229
+ async revoke(did) {
230
+ const entry = this.entries.get(did);
231
+ if (!entry)
232
+ return err('NOT_FOUND');
233
+ this.entries.set(did, { ...entry, revoked: true });
234
+ return ok(undefined);
235
+ }
236
+ async getEntry(did) {
237
+ const entry = this.entries.get(did);
238
+ if (!entry)
239
+ return err('NOT_FOUND');
240
+ if (isExpired(entry))
241
+ return err('EXPIRED');
242
+ return ok(entry);
243
+ }
244
+ async updateScopes(did, scopes) {
245
+ const entry = this.entries.get(did);
246
+ if (!entry)
247
+ return err('NOT_FOUND');
248
+ if (isExpired(entry))
249
+ return err('EXPIRED');
250
+ if (entry.revoked)
251
+ return err('REVOKED');
252
+ this.entries.set(did, { ...entry, scopes: new Set(scopes) });
253
+ return ok(undefined);
254
+ }
255
+ /** Remove all expired entries from the registry. */
256
+ async cleanup() {
257
+ let removed = 0;
258
+ const now = Date.now();
259
+ for (const [did, entry] of this.entries) {
260
+ if (entry.expiresAt && now > entry.expiresAt) {
261
+ this.entries.delete(did);
262
+ removed++;
263
+ }
264
+ }
265
+ return removed;
266
+ }
267
+ /** Number of entries (for testing). */
268
+ get size() {
269
+ return this.entries.size;
270
+ }
271
+ }
272
+ /**
273
+ * HTTP-backed trust registry for production use.
274
+ * Delegates to a remote atelier.xail.io service.
275
+ */
276
+ export class HttpTrustRegistry {
277
+ baseUrl;
278
+ fetchFn;
279
+ cacheTtlMs;
280
+ cacheFailureMode;
281
+ enablePush;
282
+ bloomFilterSize;
283
+ bloomFilterFpr;
284
+ resolveCache = new Map();
285
+ entryCache = new Map();
286
+ constructor(opts) {
287
+ this.baseUrl = opts.baseUrl.replace(/\/$/, '');
288
+ this.fetchFn = opts.fetch ?? globalThis.fetch.bind(globalThis);
289
+ this.cacheTtlMs = opts.cacheTtlMs ?? 30_000;
290
+ this.cacheFailureMode = opts.cacheFailureMode ?? 'fail-secure';
291
+ this.enablePush = opts.enablePush ?? false;
292
+ this.bloomFilterSize = opts.bloomFilterSize ?? 10_000;
293
+ this.bloomFilterFpr = opts.bloomFilterFpr ?? 0.01;
294
+ }
295
+ /** Clear all cached entries. Call after registration or revocation. */
296
+ clearCache() {
297
+ this.resolveCache.clear();
298
+ this.entryCache.clear();
299
+ }
300
+ async register(did, publicKey, name, scopes, x25519PublicKey, mlKemPublicKey, mlDsaPublicKey, xchange, receiveScopes, sdkVersion, minEnvelopeVersion, maxEnvelopeVersion, ttlMs, clientIP) {
301
+ try {
302
+ const res = await this.fetchFn(`${this.baseUrl}/registry/register`, {
303
+ method: 'POST',
304
+ headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify({
306
+ did,
307
+ publicKey: Array.from(publicKey),
308
+ name,
309
+ scopes: scopes ?? [],
310
+ ...(receiveScopes
311
+ ? { receiveScopes }
312
+ : {}),
313
+ ...(x25519PublicKey
314
+ ? { x25519PublicKey: Array.from(x25519PublicKey) }
315
+ : {}),
316
+ ...(mlKemPublicKey
317
+ ? { mlKemPublicKey: Array.from(mlKemPublicKey) }
318
+ : {}),
319
+ ...(mlDsaPublicKey
320
+ ? { mlDsaPublicKey: Array.from(mlDsaPublicKey) }
321
+ : {}),
322
+ ...(xchange !== undefined
323
+ ? { xchange }
324
+ : {}),
325
+ ...(sdkVersion !== undefined
326
+ ? { sdkVersion }
327
+ : {}),
328
+ ...(minEnvelopeVersion !== undefined
329
+ ? { minEnvelopeVersion }
330
+ : {}),
331
+ ...(maxEnvelopeVersion !== undefined
332
+ ? { maxEnvelopeVersion }
333
+ : {}),
334
+ ...(ttlMs !== undefined
335
+ ? { ttlMs }
336
+ : {}),
337
+ }),
338
+ });
339
+ if (res.status === 409)
340
+ return err('ALREADY_REGISTERED');
341
+ if (res.status === 429)
342
+ return err('RATE_LIMIT_EXCEEDED');
343
+ if (!res.ok)
344
+ return err('NETWORK_ERROR');
345
+ return ok(undefined);
346
+ }
347
+ catch {
348
+ return err('NETWORK_ERROR');
349
+ }
350
+ }
351
+ async resolve(did) {
352
+ // Check cache first
353
+ if (this.cacheTtlMs > 0) {
354
+ const cached = this.resolveCache.get(did);
355
+ if (cached && cached.expiry > Date.now())
356
+ return cached.value;
357
+ }
358
+ let result;
359
+ try {
360
+ const res = await this.fetchFn(`${this.baseUrl}/registry/resolve/${encodeURIComponent(did)}`);
361
+ if (res.status === 404)
362
+ result = err('NOT_FOUND');
363
+ else if (res.status === 408)
364
+ result = err('EXPIRED');
365
+ else if (res.status === 410)
366
+ result = err('REVOKED');
367
+ else if (!res.ok)
368
+ result = err('NETWORK_ERROR');
369
+ else {
370
+ const data = (await res.json());
371
+ result = ok(new Uint8Array(data.publicKey));
372
+ }
373
+ }
374
+ catch {
375
+ // Network failure - apply cache failure mode
376
+ const staleCache = this.resolveCache.get(did);
377
+ if (this.cacheFailureMode === 'fail-secure') {
378
+ // Fail-secure: reject on cache refresh failure (default)
379
+ result = err('NETWORK_ERROR');
380
+ }
381
+ else {
382
+ // Fail-open: accept stale cache if available
383
+ if (staleCache) {
384
+ // Return stale cached value
385
+ return staleCache.value;
386
+ }
387
+ result = err('NETWORK_ERROR');
388
+ }
389
+ }
390
+ // Update cache with fresh result (only if cache enabled)
391
+ if (this.cacheTtlMs > 0) {
392
+ this.resolveCache.set(did, { value: result, expiry: Date.now() + this.cacheTtlMs });
393
+ }
394
+ return result;
395
+ }
396
+ async hasScope(did, scope) {
397
+ try {
398
+ const res = await this.fetchFn(`${this.baseUrl}/registry/scope/${encodeURIComponent(did)}/${encodeURIComponent(scope)}`);
399
+ return res.ok;
400
+ }
401
+ catch {
402
+ return false;
403
+ }
404
+ }
405
+ async hasReceiveScope(did, scope) {
406
+ try {
407
+ const res = await this.fetchFn(`${this.baseUrl}/registry/receive-scope/${encodeURIComponent(did)}/${encodeURIComponent(scope)}`);
408
+ return res.ok;
409
+ }
410
+ catch {
411
+ return false;
412
+ }
413
+ }
414
+ async revoke(did) {
415
+ try {
416
+ const res = await this.fetchFn(`${this.baseUrl}/registry/revoke/${encodeURIComponent(did)}`, { method: 'POST' });
417
+ if (res.status === 404)
418
+ return err('NOT_FOUND');
419
+ if (!res.ok)
420
+ return err('NETWORK_ERROR');
421
+ return ok(undefined);
422
+ }
423
+ catch {
424
+ return err('NETWORK_ERROR');
425
+ }
426
+ }
427
+ async getEntry(did) {
428
+ if (this.cacheTtlMs > 0) {
429
+ const cached = this.entryCache.get(did);
430
+ if (cached && cached.expiry > Date.now())
431
+ return cached.value;
432
+ }
433
+ let result;
434
+ try {
435
+ const res = await this.fetchFn(`${this.baseUrl}/registry/entry/${encodeURIComponent(did)}`);
436
+ if (res.status === 404)
437
+ result = err('NOT_FOUND');
438
+ else if (res.status === 408)
439
+ result = err('EXPIRED');
440
+ else if (!res.ok)
441
+ result = err('NETWORK_ERROR');
442
+ else {
443
+ const data = (await res.json());
444
+ result = ok({
445
+ did: data.did,
446
+ publicKey: new Uint8Array(data.publicKey),
447
+ name: data.name,
448
+ scopes: new Set(data.scopes),
449
+ receiveScopes: data.receiveScopes ? new Set(data.receiveScopes) : undefined,
450
+ revoked: data.revoked,
451
+ rotation_sequence: data.rotation_sequence ?? 1,
452
+ x25519PublicKey: data.x25519PublicKey
453
+ ? new Uint8Array(data.x25519PublicKey)
454
+ : undefined,
455
+ mlKemPublicKey: data.mlKemPublicKey
456
+ ? new Uint8Array(data.mlKemPublicKey)
457
+ : undefined,
458
+ mlDsaPublicKey: data.mlDsaPublicKey
459
+ ? new Uint8Array(data.mlDsaPublicKey)
460
+ : undefined,
461
+ xchange: data.xchange,
462
+ sdkVersion: data.sdkVersion,
463
+ minEnvelopeVersion: data.minEnvelopeVersion,
464
+ maxEnvelopeVersion: data.maxEnvelopeVersion,
465
+ expiresAt: data.expiresAt,
466
+ });
467
+ }
468
+ }
469
+ catch {
470
+ result = err('NETWORK_ERROR');
471
+ }
472
+ if (this.cacheTtlMs > 0) {
473
+ this.entryCache.set(did, { value: result, expiry: Date.now() + this.cacheTtlMs });
474
+ }
475
+ return result;
476
+ }
477
+ /**
478
+ * Rotate a DID to a new public key with cryptographic proof.
479
+ *
480
+ * Sends rotation request to gateway and invalidates local cache.
481
+ *
482
+ * @param did - DID being rotated (old key)
483
+ * @param newPublicKey - New public key bytes
484
+ * @param proof - Succession announcement (dual signatures from old and new keys)
485
+ * @param rotationSequence - Monotonically increasing sequence number (prevents rollback)
486
+ */
487
+ async rotate(did, newPublicKey, proof, rotationSequence) {
488
+ const res = await this.fetchFn(`${this.baseUrl}/registry/rotate`, {
489
+ method: 'POST',
490
+ headers: { 'Content-Type': 'application/json' },
491
+ body: JSON.stringify({
492
+ did,
493
+ newPublicKey: Array.from(newPublicKey),
494
+ proof: Array.from(proof),
495
+ rotationSequence,
496
+ }),
497
+ });
498
+ if (!res.ok) {
499
+ throw new Error(`Key rotation failed: ${res.status} ${res.statusText}`);
500
+ }
501
+ // Invalidate cache for this DID
502
+ this.resolveCache.delete(did);
503
+ this.entryCache.delete(did);
504
+ }
505
+ /**
506
+ * Subscribe to real-time trust events (revocation, key rotation).
507
+ *
508
+ * Creates a bloom filter from DIDs and connects WebSocket to gateway.
509
+ * Events matching subscribed DIDs trigger the callback and invalidate cache.
510
+ *
511
+ * @param dids - Array of DIDs to monitor
512
+ * @param callback - Function called when trust events occur
513
+ * @returns Unsubscribe function to stop watching
514
+ *
515
+ * @throws Error if push notifications not enabled (enablePush: true)
516
+ */
517
+ async subscribe(dids, callback) {
518
+ if (!this.enablePush) {
519
+ throw new Error('Push notifications not enabled (set enablePush: true in HttpTrustRegistryOptions)');
520
+ }
521
+ // Simple bloom filter: hash each DID to track subscriptions
522
+ // Production implementation would use full bloom filter library
523
+ const bloomSet = new Set(dids.map(did => this.hashDid(did)));
524
+ // Convert HTTP URL to WebSocket URL
525
+ const wsUrl = this.baseUrl.replace(/^http/, 'ws') + '/trust/events';
526
+ // SAFETY: WebSocket is a standard browser/Node.js API
527
+ const ws = new globalThis.WebSocket(wsUrl);
528
+ ws.addEventListener('open', () => {
529
+ // Send subscription with bloom filter
530
+ ws.send(JSON.stringify({
531
+ type: 'subscribe',
532
+ dids: dids, // Simple implementation sends full DID list
533
+ bloomSize: this.bloomFilterSize,
534
+ bloomFpr: this.bloomFilterFpr,
535
+ }));
536
+ });
537
+ ws.addEventListener('message', (event) => {
538
+ try {
539
+ const trustEvent = JSON.parse(event.data);
540
+ // Check if event DID matches our subscription
541
+ const eventHash = this.hashDid(trustEvent.did);
542
+ if (bloomSet.has(eventHash)) {
543
+ // Invalidate cache for this DID
544
+ if (trustEvent.type === 'revocation' || trustEvent.type === 'succession') {
545
+ this.resolveCache.delete(trustEvent.did);
546
+ this.entryCache.delete(trustEvent.did);
547
+ }
548
+ // Notify callback
549
+ callback(trustEvent);
550
+ }
551
+ }
552
+ catch (error) {
553
+ // Ignore malformed events
554
+ console.warn('Failed to parse trust event:', error);
555
+ }
556
+ });
557
+ ws.addEventListener('error', (error) => {
558
+ console.error('WebSocket error:', error);
559
+ });
560
+ // Return unsubscribe function
561
+ return () => {
562
+ if (ws.readyState === globalThis.WebSocket.OPEN) {
563
+ ws.close();
564
+ }
565
+ };
566
+ }
567
+ /**
568
+ * Simple hash function for bloom filter (DID → number).
569
+ * Production implementation would use proper bloom filter hashing.
570
+ */
571
+ hashDid(did) {
572
+ let hash = 0;
573
+ for (let i = 0; i < did.length; i++) {
574
+ hash = ((hash << 5) - hash) + did.charCodeAt(i);
575
+ hash = hash & hash; // Convert to 32-bit integer
576
+ }
577
+ return hash;
578
+ }
579
+ /**
580
+ * Resume subscriptions on this gateway using proofs from another gateway.
581
+ *
582
+ * Allows clients to migrate between gateways without re-subscribing.
583
+ * Validates proofs and restores subscription state.
584
+ *
585
+ * @param proofs - Array of subscription proofs from previous gateway.
586
+ * @returns Success or error.
587
+ *
588
+ * @example
589
+ * ```typescript
590
+ * const registry = new HttpTrustRegistry({ baseUrl: 'https://atelier2.xail.io' });
591
+ * const result = await registry.resumeSubscriptions([proof1, proof2]);
592
+ * ```
593
+ */
594
+ async resumeSubscriptions(proofs) {
595
+ try {
596
+ const res = await this.fetchFn(`${this.baseUrl}/trust/resume-batch`, {
597
+ method: 'POST',
598
+ headers: { 'Content-Type': 'application/json' },
599
+ body: JSON.stringify({ proofs }),
600
+ });
601
+ if (!res.ok)
602
+ return err('NETWORK_ERROR');
603
+ return ok(undefined);
604
+ }
605
+ catch {
606
+ return err('NETWORK_ERROR');
607
+ }
608
+ }
609
+ /**
610
+ * Fetch signed checkpoint for a DID (freshness primitive).
611
+ *
612
+ * Checkpoints provide cryptographic proof of DID state at a specific timestamp.
613
+ * Clients verify checkpoint signature and compare rotation_sequence to detect staleness.
614
+ *
615
+ * @param did - DID to fetch checkpoint for
616
+ * @returns Signed checkpoint or error
617
+ *
618
+ * @example
619
+ * ```typescript
620
+ * const checkpoint = await registry.fetchCheckpoint('did:key:z6Mk...');
621
+ * if (checkpoint.ok) {
622
+ * const verified = await verifyCheckpoint(checkpoint.value, gatewayPubKey);
623
+ * if (verified.ok && verified.value) {
624
+ * // Use checkpoint for staleness detection
625
+ * if (isCacheStale(localCache, checkpoint.value)) {
626
+ * // Refresh cache
627
+ * }
628
+ * }
629
+ * }
630
+ * ```
631
+ */
632
+ async fetchCheckpoint(did) {
633
+ try {
634
+ const res = await this.fetchFn(`${this.baseUrl}/registry/checkpoint/${encodeURIComponent(did)}`);
635
+ if (res.status === 404)
636
+ return err('NOT_FOUND');
637
+ if (!res.ok)
638
+ return err('NETWORK_ERROR');
639
+ const checkpoint = (await res.json());
640
+ return ok(checkpoint);
641
+ }
642
+ catch {
643
+ return err('NETWORK_ERROR');
644
+ }
645
+ }
646
+ async updateScopes(did, scopes) {
647
+ try {
648
+ const res = await this.fetchFn(`${this.baseUrl}/registry/${encodeURIComponent(did)}/scopes`, {
649
+ method: 'POST',
650
+ headers: { 'Content-Type': 'application/json' },
651
+ body: JSON.stringify({ scopes }),
652
+ });
653
+ if (res.status === 404)
654
+ return err('NOT_FOUND');
655
+ if (res.status === 410)
656
+ return err('REVOKED');
657
+ if (!res.ok)
658
+ return err('NETWORK_ERROR');
659
+ // Clear cache for this DID
660
+ this.resolveCache.delete(did);
661
+ this.entryCache.delete(did);
662
+ return ok(undefined);
663
+ }
664
+ catch {
665
+ return err('NETWORK_ERROR');
666
+ }
667
+ }
668
+ }
669
+ /**
670
+ * File-based trust registry using JSONL append-only log.
671
+ * Replays all entries on initialization, keeps in-memory Map for fast access.
672
+ * Suitable for production deployments with local persistence.
673
+ */
674
+ export class FileTrustRegistry {
675
+ path;
676
+ entries = new Map();
677
+ initialized = false;
678
+ constructor(opts) {
679
+ this.path = opts.path;
680
+ }
681
+ /** Initialize by replaying JSONL log. Called automatically on first operation. */
682
+ async init() {
683
+ if (this.initialized)
684
+ return;
685
+ try {
686
+ // Ensure directory exists
687
+ await fs.mkdir(path.dirname(this.path), { recursive: true });
688
+ // Read and replay JSONL file
689
+ const content = await fs.readFile(this.path, 'utf-8').catch(() => '');
690
+ const lines = content.split('\n').filter((line) => line.trim());
691
+ for (const line of lines) {
692
+ const record = JSON.parse(line);
693
+ if (record.type === 'register' && record.publicKey && record.name) {
694
+ this.entries.set(record.did, {
695
+ did: record.did,
696
+ publicKey: new Uint8Array(record.publicKey),
697
+ name: record.name,
698
+ scopes: new Set(record.scopes ?? []),
699
+ receiveScopes: record.receiveScopes ? new Set(record.receiveScopes) : undefined,
700
+ revoked: false,
701
+ rotation_sequence: record.rotation_sequence ?? 1,
702
+ x25519PublicKey: record.x25519PublicKey
703
+ ? new Uint8Array(record.x25519PublicKey)
704
+ : undefined,
705
+ mlKemPublicKey: record.mlKemPublicKey
706
+ ? new Uint8Array(record.mlKemPublicKey)
707
+ : undefined,
708
+ mlDsaPublicKey: record.mlDsaPublicKey
709
+ ? new Uint8Array(record.mlDsaPublicKey)
710
+ : undefined,
711
+ xchange: record.xchange,
712
+ sdkVersion: record.sdkVersion,
713
+ minEnvelopeVersion: record.minEnvelopeVersion,
714
+ maxEnvelopeVersion: record.maxEnvelopeVersion,
715
+ expiresAt: record.expiresAt,
716
+ });
717
+ }
718
+ else if (record.type === 'revoke') {
719
+ const entry = this.entries.get(record.did);
720
+ if (entry) {
721
+ this.entries.set(record.did, { ...entry, revoked: true });
722
+ }
723
+ }
724
+ else if (record.type === 'update-scopes') {
725
+ const entry = this.entries.get(record.did);
726
+ if (entry) {
727
+ this.entries.set(record.did, { ...entry, scopes: new Set(record.scopes ?? []) });
728
+ }
729
+ }
730
+ else if (record.type === 'rotate' && record.publicKey && record.rotation_sequence) {
731
+ const entry = this.entries.get(record.did);
732
+ if (entry) {
733
+ // Only apply rotation if sequence is greater (prevents replaying stale rotations)
734
+ if (record.rotation_sequence > entry.rotation_sequence) {
735
+ this.entries.set(record.did, {
736
+ ...entry,
737
+ publicKey: new Uint8Array(record.publicKey),
738
+ rotation_sequence: record.rotation_sequence,
739
+ });
740
+ }
741
+ }
742
+ }
743
+ }
744
+ }
745
+ catch (error) {
746
+ // If file doesn't exist yet, that's OK - it will be created on first write
747
+ }
748
+ this.initialized = true;
749
+ }
750
+ /** Append record to JSONL file. */
751
+ async append(record) {
752
+ await fs.appendFile(this.path, JSON.stringify(record) + '\n', 'utf-8');
753
+ }
754
+ async register(did, publicKey, name, scopes, x25519PublicKey, mlKemPublicKey, mlDsaPublicKey, xchange, receiveScopes, sdkVersion, minEnvelopeVersion, maxEnvelopeVersion, ttlMs, clientIP) {
755
+ await this.init();
756
+ if (this.entries.has(did))
757
+ return err('ALREADY_REGISTERED');
758
+ const expiresAt = ttlMs ? Date.now() + ttlMs : undefined;
759
+ const entry = {
760
+ did,
761
+ publicKey,
762
+ name,
763
+ scopes: new Set(scopes ?? []),
764
+ receiveScopes: receiveScopes ? new Set(receiveScopes) : undefined,
765
+ revoked: false,
766
+ rotation_sequence: 1, // Initial sequence starts at 1
767
+ x25519PublicKey,
768
+ mlKemPublicKey,
769
+ mlDsaPublicKey,
770
+ xchange,
771
+ sdkVersion,
772
+ minEnvelopeVersion,
773
+ maxEnvelopeVersion,
774
+ expiresAt,
775
+ };
776
+ this.entries.set(did, entry);
777
+ // Append to JSONL
778
+ await this.append({
779
+ type: 'register',
780
+ did,
781
+ publicKey: Array.from(publicKey),
782
+ name,
783
+ scopes: scopes ?? [],
784
+ rotation_sequence: 1,
785
+ ...(receiveScopes ? { receiveScopes } : {}),
786
+ ...(x25519PublicKey ? { x25519PublicKey: Array.from(x25519PublicKey) } : {}),
787
+ ...(mlKemPublicKey ? { mlKemPublicKey: Array.from(mlKemPublicKey) } : {}),
788
+ ...(mlDsaPublicKey ? { mlDsaPublicKey: Array.from(mlDsaPublicKey) } : {}),
789
+ ...(xchange !== undefined ? { xchange } : {}),
790
+ ...(sdkVersion !== undefined ? { sdkVersion } : {}),
791
+ ...(minEnvelopeVersion !== undefined ? { minEnvelopeVersion } : {}),
792
+ ...(maxEnvelopeVersion !== undefined ? { maxEnvelopeVersion } : {}),
793
+ ...(expiresAt !== undefined ? { expiresAt } : {}),
794
+ });
795
+ return ok(undefined);
796
+ }
797
+ async resolve(did) {
798
+ await this.init();
799
+ const entry = this.entries.get(did);
800
+ if (!entry)
801
+ return err('NOT_FOUND');
802
+ if (isExpired(entry))
803
+ return err('EXPIRED');
804
+ if (entry.revoked)
805
+ return err('REVOKED');
806
+ return ok(entry.publicKey);
807
+ }
808
+ async hasScope(did, scope) {
809
+ await this.init();
810
+ const entry = this.entries.get(did);
811
+ if (!entry || isExpired(entry) || entry.revoked)
812
+ return false;
813
+ return entry.scopes.has(scope);
814
+ }
815
+ async hasReceiveScope(did, scope) {
816
+ await this.init();
817
+ const entry = this.entries.get(did);
818
+ if (!entry || isExpired(entry) || entry.revoked)
819
+ return false;
820
+ // Undefined = accept all scopes (backward compatibility)
821
+ if (!entry.receiveScopes)
822
+ return true;
823
+ return entry.receiveScopes.has(scope);
824
+ }
825
+ async revoke(did) {
826
+ await this.init();
827
+ const entry = this.entries.get(did);
828
+ if (!entry)
829
+ return err('NOT_FOUND');
830
+ this.entries.set(did, { ...entry, revoked: true });
831
+ await this.append({ type: 'revoke', did });
832
+ return ok(undefined);
833
+ }
834
+ async getEntry(did) {
835
+ await this.init();
836
+ const entry = this.entries.get(did);
837
+ if (!entry)
838
+ return err('NOT_FOUND');
839
+ if (isExpired(entry))
840
+ return err('EXPIRED');
841
+ return ok(entry);
842
+ }
843
+ async updateScopes(did, scopes) {
844
+ await this.init();
845
+ const entry = this.entries.get(did);
846
+ if (!entry)
847
+ return err('NOT_FOUND');
848
+ if (isExpired(entry))
849
+ return err('EXPIRED');
850
+ if (entry.revoked)
851
+ return err('REVOKED');
852
+ this.entries.set(did, { ...entry, scopes: new Set(scopes) });
853
+ await this.append({ type: 'update-scopes', did, scopes });
854
+ return ok(undefined);
855
+ }
856
+ /** Remove all expired entries from the registry. */
857
+ async cleanup() {
858
+ await this.init();
859
+ let removed = 0;
860
+ const now = Date.now();
861
+ for (const [did, entry] of this.entries) {
862
+ if (entry.expiresAt && now > entry.expiresAt) {
863
+ this.entries.delete(did);
864
+ removed++;
865
+ }
866
+ }
867
+ return removed;
868
+ }
869
+ /**
870
+ * Rotate a DID to a new public key with rollback protection.
871
+ *
872
+ * Validates that rotationSequence is greater than current sequence to prevent
873
+ * rollback attacks. Appends rotation event to JSONL and updates in-memory state.
874
+ *
875
+ * @param did - DID being rotated
876
+ * @param newPublicKey - New public key bytes
877
+ * @param proof - Cryptographic proof (e.g., signature from old key)
878
+ * @param rotationSequence - Monotonically increasing sequence number
879
+ * @throws Error if DID not found or sequence validation fails
880
+ */
881
+ async rotate(did, newPublicKey, proof, rotationSequence) {
882
+ await this.init();
883
+ const entry = this.entries.get(did);
884
+ if (!entry) {
885
+ throw new Error(`DID not found: ${did}`);
886
+ }
887
+ // Rollback protection: new sequence must be greater than current
888
+ if (rotationSequence <= entry.rotation_sequence) {
889
+ throw new Error(`Rotation sequence ${rotationSequence} must be > current ${entry.rotation_sequence} (rollback attack prevented)`);
890
+ }
891
+ // Append rotation event to JSONL
892
+ await this.append({
893
+ type: 'rotate',
894
+ did,
895
+ publicKey: Array.from(newPublicKey),
896
+ proof: Array.from(proof),
897
+ rotation_sequence: rotationSequence,
898
+ timestamp: Date.now(),
899
+ });
900
+ // Update in-memory entry
901
+ this.entries.set(did, {
902
+ ...entry,
903
+ publicKey: newPublicKey,
904
+ rotation_sequence: rotationSequence,
905
+ });
906
+ }
907
+ /** Number of entries (for testing). */
908
+ get size() {
909
+ return this.entries.size;
910
+ }
911
+ }
912
+ /* ── Enterprise Factory ── */
913
+ /**
914
+ * Create an enterprise trust registry with optional pre-population.
915
+ * Suitable for corporate deployments with centralized trust management.
916
+ *
917
+ * @example
918
+ * ```typescript
919
+ * const registry = await TrustRegistry.enterprise({
920
+ * storage: 'file',
921
+ * path: '/opt/corp/trust.jsonl',
922
+ * preload: [
923
+ * { did: 'did:web:corp.example.com', publicKey: ..., name: 'Corporate Gateway' }
924
+ * ]
925
+ * });
926
+ * ```
927
+ */
928
+ export async function createEnterpriseTrustRegistry(opts) {
929
+ let registry;
930
+ if (opts.storage === 'file') {
931
+ if (!opts.path)
932
+ throw new Error('FileTrustRegistry requires path option');
933
+ registry = new FileTrustRegistry({ path: opts.path });
934
+ }
935
+ else if (opts.storage === 'http') {
936
+ if (!opts.baseUrl)
937
+ throw new Error('HttpTrustRegistry requires baseUrl option');
938
+ registry = new HttpTrustRegistry({ baseUrl: opts.baseUrl });
939
+ }
940
+ else {
941
+ registry = new MemoryTrustRegistry();
942
+ }
943
+ // Pre-populate if requested
944
+ if (opts.preload) {
945
+ for (const entry of opts.preload) {
946
+ await registry.register(entry.did, entry.publicKey, entry.name, entry.scopes, entry.x25519PublicKey, entry.mlKemPublicKey, entry.mlDsaPublicKey, entry.xchange, entry.receiveScopes, entry.sdkVersion, entry.minEnvelopeVersion, entry.maxEnvelopeVersion);
947
+ }
948
+ }
949
+ return registry;
950
+ }