@jaw.id/cli 0.0.2 → 0.0.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 (42) hide show
  1. package/dist/base-command.js +1 -2
  2. package/dist/base-command.js.map +1 -1
  3. package/dist/commands/config/set.js +19 -3
  4. package/dist/commands/config/set.js.map +1 -1
  5. package/dist/commands/config/show.js +1 -2
  6. package/dist/commands/config/show.js.map +1 -1
  7. package/dist/commands/disconnect.js +439 -36
  8. package/dist/commands/disconnect.js.map +1 -1
  9. package/dist/commands/mcp/index.js +472 -218
  10. package/dist/commands/mcp/index.js.map +1 -1
  11. package/dist/commands/rpc/call.js +429 -204
  12. package/dist/commands/rpc/call.js.map +1 -1
  13. package/dist/commands/version.js +134 -0
  14. package/dist/commands/version.js.map +1 -0
  15. package/dist/index.js +455 -203
  16. package/dist/index.js.map +1 -1
  17. package/dist/lib/bridge-singleton.js +457 -205
  18. package/dist/lib/bridge-singleton.js.map +1 -1
  19. package/dist/lib/config.js +17 -2
  20. package/dist/lib/config.js.map +1 -1
  21. package/dist/lib/crypto.js +74 -0
  22. package/dist/lib/crypto.js.map +1 -0
  23. package/dist/lib/paths.js +1 -2
  24. package/dist/lib/paths.js.map +1 -1
  25. package/dist/lib/session-store.js +1 -2
  26. package/dist/lib/session-store.js.map +1 -1
  27. package/dist/lib/validation.js +12 -1
  28. package/dist/lib/validation.js.map +1 -1
  29. package/dist/lib/ws-bridge.js +321 -59
  30. package/dist/lib/ws-bridge.js.map +1 -1
  31. package/dist/mcp/handlers/config.js +17 -2
  32. package/dist/mcp/handlers/config.js.map +1 -1
  33. package/dist/mcp/handlers/daemon.js +446 -46
  34. package/dist/mcp/handlers/daemon.js.map +1 -1
  35. package/dist/mcp/handlers/rpc.js +429 -204
  36. package/dist/mcp/handlers/rpc.js.map +1 -1
  37. package/dist/mcp/server.js +472 -218
  38. package/dist/mcp/server.js.map +1 -1
  39. package/oclif.manifest.json +70 -2
  40. package/package.json +5 -1
  41. package/dist/lib/ws-daemon.js +0 -344
  42. package/dist/lib/ws-daemon.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/validation.ts","../../src/lib/config.ts","../../src/lib/ws-bridge.ts","../../src/lib/bridge-singleton.ts"],"names":["fs","path2"],"mappings":";;;;;;;;;AAGA,IAAM,OAAA,GAAe,IAAA,CAAA,IAAA,CAAQ,EAAA,CAAA,OAAA,EAAQ,EAAG,MAAM,CAAA;AAEvC,IAAM,KAAA,GAAQ;AAAA,EACnB,IAAA,EAAM,OAAA;AAAA,EACN,MAAA,EAAa,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,aAAa,CAAA;AAAA,EACxC,OAAA,EAAc,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,cAAc,CAAA;AAAA,EAC1C,MAAA,EAAa,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,aAAa,CAAA;AAAA,EACxC,SAAA,EAAgB,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,YAAY;AAC5C,CAAA;;;ACyBO,SAAS,eAAe,GAAA,EAAsB;AACnD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,CAAS,SAAS,CAAA,IAClC,MAAA,CAAO,QAAA,KAAa,QAAA,IACpB,MAAA,CAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,QAAA,GACJ,OAAO,QAAA,KAAa,QAAA,IACpB,OAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,OAAO,aAAA,IAAiB,QAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;;;AC/CO,SAAS,UAAU,GAAA,EAAmB;AAC3C,EAAGA,cAAU,GAAA,EAAK,EAAE,WAAW,IAAA,EAAM,IAAA,EAAM,KAAO,CAAA;AAClD,EAAGA,GAAA,CAAA,SAAA,CAAU,KAAK,GAAK,CAAA;AACzB;AAEO,SAAS,UAAA,GAAwB;AACtC,EAAA,IAAI,CAAIA,GAAA,CAAA,UAAA,CAAW,KAAA,CAAM,MAAM,CAAA,EAAG;AAChC,IAAA,OAAO,EAAC;AAAA,EACV;AACA,EAAA,MAAM,GAAA,GAASA,GAAA,CAAA,YAAA,CAAa,KAAA,CAAM,MAAA,EAAQ,OAAO,CAAA;AACjD,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EACvB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,eAAA,EAAkB,MAAM,MAAM,CAAA,oEAAA;AAAA,KAChC;AAAA,EACF;AACF;ACLA,IAAM,kBAAA,GAAqB,IAAA;AAEpB,IAAM,WAAN,MAAe;AAAA,EACH,IAAA;AAAA,EACA,KAAA;AAAA,EACA,OAAA;AAAA,EACT,EAAA,GAAuB,IAAA;AAAA,EAE/B,YAAY,OAAA,EAA0B;AACpC,IAAA,IAAA,CAAK,OAAO,OAAA,CAAQ,IAAA;AACpB,IAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,KAAA;AACrB,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,OAAA,IAAW,kBAAA;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAA,GAAyB;AAC7B,IAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,MAAA,MAAM,GAAA,GAAM,kBAAkB,IAAA,CAAK,IAAI,UAAU,kBAAA,CAAmB,IAAA,CAAK,KAAK,CAAC,CAAA,SAAA,CAAA;AAC/E,MAAA,MAAM,EAAA,GAAK,IAAI,SAAA,CAAU,GAAG,CAAA;AAE5B,MAAA,MAAM,KAAA,GAAQ,WAAW,MAAM;AAC7B,QAAA,EAAA,CAAG,KAAA,EAAM;AACT,QAAA,MAAA;AAAA,UACE,IAAI,KAAA;AAAA,YACF;AAAA;AAEF,SACF;AAAA,MACF,GAAG,GAAM,CAAA;AAET,MAAA,EAAA,CAAG,EAAA,CAAG,QAAQ,MAAM;AAClB,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,IAAA,CAAK,EAAA,GAAK,EAAA;AAAA,MACZ,CAAC,CAAA;AAED,MAAA,EAAA,CAAG,EAAA,CAAG,SAAA,EAAW,CAAC,IAAA,KAAS;AACzB,QAAA,IAAI,GAAA;AACJ,QAAA,IAAI;AACF,UAAA,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,QAAA,EAAU,CAAA;AAAA,QAClC,CAAA,CAAA,MAAQ;AACN,UAAA;AAAA,QACF;AAKA,QAAA,IAAI,GAAA,CAAI,IAAA,KAAS,QAAA,IAAY,GAAA,CAAI,gBAAA,EAAkB;AACjD,UAAA,YAAA,CAAa,KAAK,CAAA;AAClB,UAAA,OAAA,EAAQ;AAAA,QACV,CAAA,MAAA,IAAW,GAAA,CAAI,IAAA,KAAS,mBAAA,EAAqB;AAC3C,UAAA,YAAA,CAAa,KAAK,CAAA;AAClB,UAAA,OAAA,EAAQ;AAAA,QACV;AAAA,MACF,CAAC,CAAA;AAED,MAAA,EAAA,CAAG,EAAA,CAAG,OAAA,EAAS,CAAC,GAAA,KAAQ;AACtB,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,MAAA,CAAO,GAAG,CAAA;AAAA,MACZ,CAAC,CAAA;AAED,MAAA,EAAA,CAAG,EAAA,CAAG,SAAS,MAAM;AACnB,QAAA,YAAA,CAAa,KAAK,CAAA;AAAA,MACpB,CAAC,CAAA;AAAA,IACH,CAAC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAA,CAAQ,MAAA,EAAgB,MAAA,EAAoC;AAChE,IAAA,MAAM,KAAK,IAAA,CAAK,EAAA;AAChB,IAAA,IAAI,CAAC,EAAA,IAAM,EAAA,CAAG,UAAA,KAAe,UAAU,IAAA,EAAM;AAC3C,MAAA,MAAM,IAAI,MAAM,gCAAgC,CAAA;AAAA,IAClD;AAEA,IAAA,MAAM,KAAY,MAAA,CAAA,UAAA,EAAW;AAE7B,IAAA,OAAO,IAAI,OAAA,CAAiB,CAAC,OAAA,EAAS,MAAA,KAAW;AAC/C,MAAA,MAAM,KAAA,GAAQ,WAAW,MAAM;AAC7B,QAAA,MAAA;AAAA,UACE,IAAI,KAAA;AAAA,YACF,CAAA,wBAAA,EAA2B,IAAA,CAAK,OAAA,GAAU,GAAI,CAAA,8CAAA;AAAA;AAEhD,SACF;AACA,QAAA,IAAA,CAAK,KAAA,EAAM;AAAA,MACb,CAAA,EAAG,KAAK,OAAO,CAAA;AAEf,MAAA,MAAM,SAAA,GAAY,CAAC,IAAA,KAAyB;AAC1C,QAAA,IAAI,GAAA;AACJ,QAAA,IAAI;AACF,UAAA,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,QAAA,EAAU,CAAA;AAAA,QAClC,CAAA,CAAA,MAAQ;AACN,UAAA;AAAA,QACF;AAEA,QAAA,IAAI,GAAA,CAAI,IAAA,KAAS,cAAA,IAAkB,GAAA,CAAI,OAAO,EAAA,EAAI;AAChD,UAAA,YAAA,CAAa,KAAK,CAAA;AAClB,UAAA,EAAA,CAAG,GAAA,CAAI,WAAW,SAAS,CAAA;AAE3B,UAAA,IAAI,IAAI,OAAA,EAAS;AACf,YAAA,OAAA,CAAQ,IAAI,IAAI,CAAA;AAAA,UAClB,CAAA,MAAO;AACL,YAAA,MAAM,MAAM,GAAA,CAAI,KAAA;AAGhB,YAAA,MAAA;AAAA,cACE,IAAI,KAAA;AAAA,gBACF,MAAM,CAAA,CAAA,EAAI,GAAA,CAAI,IAAI,CAAA,EAAA,EAAK,GAAA,CAAI,OAAO,CAAA,CAAA,GAAK;AAAA;AACzC,aACF;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAA;AAEA,MAAA,EAAA,CAAG,EAAA,CAAG,WAAW,SAAS,CAAA;AAE1B,MAAA,EAAA,CAAG,IAAA;AAAA,QACD,KAAK,SAAA,CAAU;AAAA,UACb,EAAA;AAAA,UACA,IAAA,EAAM,aAAA;AAAA,UACN,MAAA;AAAA,UACA;AAAA,SACD;AAAA,OACH;AAAA,IACF,CAAC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAA,GAAkB;AAChB,IAAA,OAAO,IAAA,CAAK,EAAA,EAAI,UAAA,KAAe,SAAA,CAAU,IAAA;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA,EAKA,QAAA,GAAiB;AACf,IAAA,IAAI,IAAA,CAAK,EAAA,EAAI,UAAA,KAAe,SAAA,CAAU,IAAA,EAAM;AAC1C,MAAA,IAAA,CAAK,EAAA,CAAG,KAAK,IAAA,CAAK,SAAA,CAAU,EAAE,IAAA,EAAM,UAAA,EAAY,CAAC,CAAA;AAAA,IACnD;AACA,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb;AAAA;AAAA;AAAA;AAAA,EAKA,KAAA,GAAc;AACZ,IAAA,IAAI,KAAK,EAAA,EAAI;AACX,MAAA,IAAI;AACF,QAAA,IAAA,CAAK,GAAG,KAAA,EAAM;AAAA,MAChB,CAAA,CAAA,MAAQ;AAAA,MAER;AACA,MAAA,IAAA,CAAK,EAAA,GAAK,IAAA;AAAA,IACZ;AAAA,EACF;AACF,CAAA;;;ACzJA,SAAS,WAAA,GAAsB;AAC7B,EAAA,IAAI,GAAA,GAAWC,IAAA,CAAA,OAAA,CAAQ,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AAErD,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,EAAA,EAAI,CAAA,EAAA,EAAK;AAC3B,IAAA,MAAM,SAAA,GAAiBA,IAAA,CAAA,IAAA,CAAK,GAAA,EAAK,KAAA,EAAO,cAAc,CAAA;AACtD,IAAA,IAAO,GAAA,CAAA,UAAA,CAAW,SAAS,CAAA,EAAG,OAAO,GAAA;AACrC,IAAA,GAAA,GAAWA,aAAQ,GAAG,CAAA;AAAA,EACxB;AACA,EAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACzD;AAEA,IAAM,YAAA,GAAe,qBAAA;AACrB,IAAM,SAAA,GAAiBA,IAAA,CAAA,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,aAAa,CAAA;AAsBrD,SAAS,gBAAgB,GAAA,EAAsB;AAE7C,EAAA,IAAI,CAAC,OAAO,SAAA,CAAU,GAAG,KAAK,GAAA,IAAO,CAAA,IAAK,GAAA,GAAM,OAAA,EAAW,OAAO,KAAA;AAElE,EAAA,IAAI;AAEF,IAAA,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,EACrB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AAEA,EAAA,IAAI;AAEF,IAAA,MAAM,MAAM,QAAA,CAAS,CAAA,MAAA,EAAS,MAAA,CAAO,GAAG,CAAC,CAAA,YAAA,CAAA,EAAgB;AAAA,MACvD,QAAA,EAAU,OAAA;AAAA,MACV,OAAA,EAAS;AAAA,KACV,EAAE,IAAA,EAAK;AACR,IAAA,OAAO,GAAA,CAAI,SAAS,WAAW,CAAA;AAAA,EACjC,CAAA,CAAA,MAAQ;AAEN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEA,SAAS,cAAA,GAAoC;AAC3C,EAAA,IAAI;AACF,IAAA,IAAI,CAAI,GAAA,CAAA,UAAA,CAAW,KAAA,CAAM,MAAM,GAAG,OAAO,IAAA;AACzC,IAAA,MAAM,GAAA,GAAS,GAAA,CAAA,YAAA,CAAa,KAAA,CAAM,MAAA,EAAQ,OAAO,CAAA;AACjD,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC3B,IAAA,IAAI,CAAC,eAAA,CAAgB,IAAA,CAAK,GAAG,CAAA,EAAG;AAE9B,MAAA,IAAI;AACF,QAAG,GAAA,CAAA,UAAA,CAAW,MAAM,MAAM,CAAA;AAAA,MAC5B,CAAA,CAAA,MAAQ;AAAA,MAER;AACA,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA;AAAA,EACT,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAKA,eAAsB,UAAU,OAAA,EAA2C;AACzE,EAAA,IAAI,OAAO,cAAA,EAAe;AAE1B,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,IAAA,GAAO,MAAM,YAAY,OAAO,CAAA;AAAA,EAClC;AAEA,EAAA,MAAM,MAAA,GAAS,IAAI,QAAA,CAAS;AAAA,IAC1B,MAAM,IAAA,CAAK,IAAA;AAAA,IACX,OAAO,IAAA,CAAK,KAAA;AAAA,IACZ,SAAS,OAAA,CAAQ;AAAA,GAClB,CAAA;AAED,EAAA,MAAM,OAAO,OAAA,EAAQ;AACrB,EAAA,OAAO,MAAA;AACT;AAKA,eAAsB,cAAA,GAAgC;AACpD,EAAA,MAAM,OAAO,cAAA,EAAe;AAC5B,EAAA,IAAI,CAAC,IAAA,EAAM;AAGX,EAAA,IAAI;AACF,IAAA,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,GAAA,EAAK,SAAS,CAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AAAA,EAER;AAGA,EAAA,IAAI;AACF,IAAA,IAAO,eAAW,KAAA,CAAM,MAAM,GAAM,GAAA,CAAA,UAAA,CAAW,MAAM,MAAM,CAAA;AAAA,EAC7D,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AAMA,SAAS,WAAA,GAA6B;AACpC,EAAA,SAAA,CAAU,MAAM,IAAI,CAAA;AACpB,EAAA,IAAI;AAEF,IAAA,MAAM,EAAA,GAAQ,GAAA,CAAA,QAAA,CAAS,SAAA,EAAW,IAAI,CAAA;AACtC,IAAG,GAAA,CAAA,aAAA,CAAc,WAAW,MAAA,CAAO,OAAA,CAAQ,GAAG,CAAA,EAAG,EAAE,IAAA,EAAM,GAAA,EAAO,CAAA;AAChE,IAAA,OAAO,EAAA;AAAA,EACT,SAAS,GAAA,EAAc;AACrB,IAAA,IAAK,GAAA,CAA8B,SAAS,QAAA,EAAU;AAEpD,MAAA,IAAI;AACF,QAAA,MAAM,OAAA,GAAU,SAAY,GAAA,CAAA,YAAA,CAAa,SAAA,EAAW,OAAO,CAAA,CAAE,IAAA,IAAQ,EAAE,CAAA;AACvE,QAAA,IAAI,MAAA,CAAO,SAAA,CAAU,OAAO,CAAA,IAAK,UAAU,CAAA,EAAG;AAC5C,UAAA,IAAI;AACF,YAAA,OAAA,CAAQ,IAAA,CAAK,SAAS,CAAC,CAAA;AACvB,YAAA,OAAO,IAAA;AAAA,UACT,CAAA,CAAA,MAAQ;AAEN,YAAA,IAAI;AAAE,cAAG,eAAW,SAAS,CAAA;AAAA,YAAG,CAAA,CAAA,MAAQ;AAAA,YAAe;AACvD,YAAA,OAAO,WAAA,EAAY;AAAA,UACrB;AAAA,QACF;AAAA,MACF,CAAA,CAAA,MAAQ;AAEN,QAAA,IAAI;AAAE,UAAG,eAAW,SAAS,CAAA;AAAA,QAAG,CAAA,CAAA,MAAQ;AAAA,QAAe;AAAA,MACzD;AACA,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,MAAM,GAAA;AAAA,EACR;AACF;AAEA,SAAS,YAAY,EAAA,EAAkB;AACrC,EAAA,IAAI;AAAE,IAAG,cAAU,EAAE,CAAA;AAAA,EAAG,CAAA,CAAA,MAAQ;AAAA,EAAe;AAC/C,EAAA,IAAI;AAAE,IAAG,eAAW,SAAS,CAAA;AAAA,EAAG,CAAA,CAAA,MAAQ;AAAA,EAAe;AACzD;AAEA,eAAe,YAAY,OAAA,EAA6C;AACtE,EAAA,SAAA,CAAU,MAAM,IAAI,CAAA;AAGpB,EAAA,MAAM,SAAS,WAAA,EAAY;AAC3B,EAAA,IAAI,WAAW,IAAA,EAAM;AAEnB,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAI,GAAI,IAAA;AAC9B,IAAA,OAAO,IAAA,CAAK,GAAA,EAAI,GAAI,QAAA,EAAU;AAC5B,MAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,MAAM,UAAA,CAAW,CAAA,EAAG,GAAG,CAAC,CAAA;AAC3C,MAAA,MAAM,OAAO,cAAA,EAAe;AAC5B,MAAA,IAAI,MAAM,OAAO,IAAA;AAAA,IACnB;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAEA,EAAA,IAAI;AAEF,IAAA,MAAM,WAAW,cAAA,EAAe;AAChC,IAAA,IAAI,UAAU,OAAO,QAAA;AAErB,IAAA,MAAM,SAAS,UAAA,EAAW;AAE1B,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,MAAA,CAAO,OAAA,IAAW,YAAA;AACrD,IAAA,IAAI,CAAC,cAAA,CAAe,OAAO,CAAA,EAAG;AAC5B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,sBAAsB,OAAO,CAAA,iDAAA;AAAA,OAC/B;AAAA,IACF;AAEA,IAAA,MAAM,UAAA,GAAa;AAAA,MACjB,OAAA;AAAA,MACA,OAAA,EAAS,OAAA,CAAQ,OAAA,IAAW,MAAA,CAAO,YAAA,IAAgB,CAAA;AAAA,MACnD,GAAA,EAAK,OAAA,CAAQ,GAAA,IAAO,MAAA,CAAO,GAAA;AAAA,MAC3B,YAAA,EAAc,OAAA,CAAQ,YAAA,IAAgB,MAAA,CAAO,YAAA;AAAA,MAC7C,OAAA,EAAS,QAAQ,OAAA,IAAW;AAAA,KAC9B;AAEA,IAAA,MAAM,YAAA,GAAoBA,IAAA,CAAA,IAAA,CAAK,WAAA,EAAY,EAAG,OAAO,cAAc,CAAA;AAGnE,IAAA,IAAI;AACF,MAAA,IAAO,eAAW,KAAA,CAAM,MAAM,GAAM,GAAA,CAAA,UAAA,CAAW,MAAM,MAAM,CAAA;AAAA,IAC7D,CAAA,CAAA,MAAQ;AAAA,IAER;AAGA,IAAA,MAAM,KAAA,GAAW,GAAA,CAAA,QAAA,CAAS,KAAA,CAAM,SAAA,EAAW,KAAK,GAAK,CAAA;AACrD,IAAA,MAAM,KAAA,GAAQ,KAAA;AAAA,MACZ,OAAA,CAAQ,QAAA;AAAA,MACR,CAAC,YAAA,EAAc,IAAA,CAAK,SAAA,CAAU,UAAU,CAAC,CAAA;AAAA,MACzC;AAAA,QACE,QAAA,EAAU,IAAA;AAAA,QACV,KAAA,EAAO,CAAC,QAAA,EAAU,KAAA,EAAO,KAAK,CAAA;AAAA;AAAA,QAE9B,KAAK,EAAE,GAAG,QAAQ,GAAA,EAAK,kBAAA,EAAoB,QAAQ,MAAA;AAAO;AAC5D,KACF;AACA,IAAA,KAAA,CAAM,KAAA,EAAM;AACZ,IAAG,cAAU,KAAK,CAAA;AAGlB,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAI,GAAI,IAAA;AAC9B,IAAA,OAAO,IAAA,CAAK,GAAA,EAAI,GAAI,QAAA,EAAU;AAC5B,MAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,MAAM,UAAA,CAAW,CAAA,EAAG,GAAG,CAAC,CAAA;AAC3C,MAAA,MAAM,OAAO,cAAA,EAAe;AAC5B,MAAA,IAAI,MAAM,OAAO,IAAA;AAAA,IACnB;AAEA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,yCAAA,EAA4C,MAAM,SAAS,CAAA,aAAA;AAAA,KAC7D;AAAA,EACF,CAAA,SAAE;AACA,IAAA,WAAA,CAAY,MAAM,CAAA;AAAA,EACpB;AACF","file":"bridge-singleton.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n bridge: path.join(JAW_DIR, \"bridge.json\"),\n daemonLog: path.join(JAW_DIR, \"daemon.log\"),\n} as const;\n","export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n","import * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport type { JawConfig } from \"./types.js\";\nimport { isValidKeysUrl } from \"./validation.js\";\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, \"utf-8\");\n try {\n return JSON.parse(raw) as JawConfig;\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`,\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n };\n}\n\nexport function getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n","/**\n * WSBridge — CLI-side client\n *\n * Lightweight WebSocket client that connects to the background daemon.\n * Each CLI command creates a WSBridge, sends one request, and exits.\n * The daemon (and browser SDK) stay alive across commands.\n */\n\nimport * as crypto from \"node:crypto\";\nimport WebSocket from \"ws\";\n\nexport interface WSBridgeOptions {\n port: number;\n token: string;\n timeout?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\nexport class WSBridge {\n private readonly port: number;\n private readonly token: string;\n private readonly timeout: number;\n private ws: WebSocket | null = null;\n\n constructor(options: WSBridgeOptions) {\n this.port = options.port;\n this.token = options.token;\n this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n }\n\n /**\n * Connect to the daemon's WebSocket server.\n */\n async connect(): Promise<void> {\n return new Promise((resolve, reject) => {\n const url = `ws://127.0.0.1:${this.port}?token=${encodeURIComponent(this.token)}&role=cli`;\n const ws = new WebSocket(url);\n\n const timer = setTimeout(() => {\n ws.close();\n reject(\n new Error(\n \"Browser SDK did not connect in time.\\n\" +\n \"If the browser tab failed to open, run `jaw disconnect` then try again.\",\n ),\n );\n }, 30_000);\n\n ws.on(\"open\", () => {\n clearTimeout(timer);\n this.ws = ws;\n });\n\n ws.on(\"message\", (data) => {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return;\n }\n // Wait for the status message confirming daemon is ready.\n // If browser isn't connected yet, stay connected and wait for\n // a \"browser_connected\" event (the daemon notifies CLI clients\n // when the browser joins).\n if (msg.type === \"status\" && msg.browserConnected) {\n clearTimeout(timer);\n resolve();\n } else if (msg.type === \"browser_connected\") {\n clearTimeout(timer);\n resolve();\n }\n });\n\n ws.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n ws.on(\"close\", () => {\n clearTimeout(timer);\n });\n });\n }\n\n /**\n * Send an RPC request through the daemon to the browser SDK.\n */\n async request(method: string, params?: unknown): Promise<unknown> {\n const ws = this.ws;\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"Not connected to bridge daemon\");\n }\n\n const id = crypto.randomUUID();\n\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new Error(\n `Request timed out after ${this.timeout / 1000}s. ` +\n \"Did you complete the action in the browser?\",\n ),\n );\n this.close();\n }, this.timeout);\n\n const onMessage = (data: WebSocket.Data) => {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return;\n }\n\n if (msg.type === \"rpc_response\" && msg.id === id) {\n clearTimeout(timer);\n ws.off(\"message\", onMessage);\n\n if (msg.success) {\n resolve(msg.data);\n } else {\n const err = msg.error as\n | { code: number; message: string }\n | undefined;\n reject(\n new Error(\n err ? `[${err.code}] ${err.message}` : \"Request failed\",\n ),\n );\n }\n }\n };\n\n ws.on(\"message\", onMessage);\n\n ws.send(\n JSON.stringify({\n id,\n type: \"rpc_request\",\n method,\n params,\n }),\n );\n });\n }\n\n /**\n * Check if the WebSocket connection is open.\n */\n isOpen(): boolean {\n return this.ws?.readyState === WebSocket.OPEN;\n }\n\n /**\n * Send a shutdown signal to the daemon.\n */\n shutdown(): void {\n if (this.ws?.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify({ type: \"shutdown\" }));\n }\n this.close();\n }\n\n /**\n * Close the client connection (daemon stays alive).\n */\n close(): void {\n if (this.ws) {\n try {\n this.ws.close();\n } catch {\n // ignore\n }\n this.ws = null;\n }\n }\n}\n","/**\n * Bridge lifecycle management.\n *\n * - Spawns a background daemon if one isn't running\n * - Creates a WSBridge client connected to the daemon\n * - Provides shutdown to kill the daemon\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { spawn, execSync } from \"node:child_process\";\nimport { PATHS } from \"./paths.js\";\nimport { ensureDir } from \"./config.js\";\nimport { WSBridge } from \"./ws-bridge.js\";\nimport { loadConfig } from \"./config.js\";\nimport { isValidKeysUrl } from \"./validation.js\";\n\n/**\n * Find the dist directory root by walking up from the current file.\n * Needed because tsup inlines this code into consuming entry points,\n * so import.meta.url may point to e.g. dist/commands/rpc/call.js\n * rather than dist/lib/bridge-singleton.js.\n */\nfunction findDistDir(): string {\n let dir = path.dirname(fileURLToPath(import.meta.url));\n // Walk up until we find a directory that contains lib/ws-daemon.js\n for (let i = 0; i < 10; i++) {\n const candidate = path.join(dir, \"lib\", \"ws-daemon.js\");\n if (fs.existsSync(candidate)) return dir;\n dir = path.dirname(dir);\n }\n throw new Error(\"Cannot find ws-daemon.js in dist tree\");\n}\n\nconst JAW_KEYS_URL = \"https://keys.jaw.id\";\nconst LOCK_PATH = path.join(PATHS.root, \"daemon.lock\");\n\ninterface BridgeInfo {\n port: number;\n token: string;\n pid: number;\n startedAt: string;\n}\n\nexport interface BridgeOptions {\n keysUrl?: string;\n apiKey: string;\n chainId?: number;\n ens?: string;\n paymasterUrl?: string;\n timeout?: number;\n}\n\n/**\n * Verify that the process at `pid` is actually our ws-daemon, not an\n * impersonator that killed the real daemon and took over the port.\n */\nfunction isDaemonProcess(pid: number): boolean {\n // Validate PID is a positive integer to prevent command injection via bridge.json\n if (!Number.isInteger(pid) || pid <= 0 || pid > 4_194_304) return false;\n\n try {\n // Signal 0 checks if process exists\n process.kill(pid, 0);\n } catch {\n return false;\n }\n\n try {\n // Verify the process command line contains ws-daemon\n const cmd = execSync(`ps -p ${String(pid)} -o command=`, {\n encoding: \"utf-8\",\n timeout: 3000,\n }).trim();\n return cmd.includes(\"ws-daemon\");\n } catch {\n // If ps fails (process just died), treat as invalid\n return false;\n }\n}\n\nfunction loadBridgeInfo(): BridgeInfo | null {\n try {\n if (!fs.existsSync(PATHS.bridge)) return null;\n const raw = fs.readFileSync(PATHS.bridge, \"utf-8\");\n const info = JSON.parse(raw) as BridgeInfo;\n if (!isDaemonProcess(info.pid)) {\n // Process is dead or not our daemon — clean up stale file\n try {\n fs.unlinkSync(PATHS.bridge);\n } catch {\n /* ignore */\n }\n return null;\n }\n return info;\n } catch {\n return null;\n }\n}\n\n/**\n * Ensure a daemon is running and return a connected WSBridge client.\n */\nexport async function getBridge(options: BridgeOptions): Promise<WSBridge> {\n let info = loadBridgeInfo();\n\n if (!info) {\n info = await spawnDaemon(options);\n }\n\n const bridge = new WSBridge({\n port: info.port,\n token: info.token,\n timeout: options.timeout,\n });\n\n await bridge.connect();\n return bridge;\n}\n\n/**\n * Shutdown the running daemon.\n */\nexport async function shutdownDaemon(): Promise<void> {\n const info = loadBridgeInfo();\n if (!info) return;\n\n // Kill the daemon process directly\n try {\n process.kill(info.pid, \"SIGTERM\");\n } catch {\n // Process already dead\n }\n\n // Clean up bridge file\n try {\n if (fs.existsSync(PATHS.bridge)) fs.unlinkSync(PATHS.bridge);\n } catch {\n // ignore\n }\n}\n\n/**\n * Acquire an exclusive lockfile to prevent concurrent daemon spawning.\n * Returns the file descriptor on success, or null if another process holds the lock.\n */\nfunction acquireLock(): number | null {\n ensureDir(PATHS.root);\n try {\n // O_CREAT | O_EXCL — fails atomically if file already exists\n const fd = fs.openSync(LOCK_PATH, \"wx\");\n fs.writeFileSync(LOCK_PATH, String(process.pid), { mode: 0o600 });\n return fd;\n } catch (err: unknown) {\n if ((err as NodeJS.ErrnoException).code === \"EEXIST\") {\n // Check if the lock holder is still alive (stale lock recovery)\n try {\n const lockPid = parseInt(fs.readFileSync(LOCK_PATH, \"utf-8\").trim(), 10);\n if (Number.isInteger(lockPid) && lockPid > 0) {\n try {\n process.kill(lockPid, 0);\n return null; // Process alive — lock is held\n } catch {\n // Process dead — stale lock, remove and retry once\n try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ }\n return acquireLock();\n }\n }\n } catch {\n // Can't read lock — try removing stale file\n try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ }\n }\n return null;\n }\n throw err;\n }\n}\n\nfunction releaseLock(fd: number): void {\n try { fs.closeSync(fd); } catch { /* ignore */ }\n try { fs.unlinkSync(LOCK_PATH); } catch { /* ignore */ }\n}\n\nasync function spawnDaemon(options: BridgeOptions): Promise<BridgeInfo> {\n ensureDir(PATHS.root);\n\n // Acquire lock to prevent concurrent daemon spawning (TOCTOU guard)\n const lockFd = acquireLock();\n if (lockFd === null) {\n // Another process is spawning — wait for bridge.json to appear\n const deadline = Date.now() + 15_000;\n while (Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 300));\n const info = loadBridgeInfo();\n if (info) return info;\n }\n throw new Error(\n \"Another process is starting the daemon. Timed out waiting for it.\",\n );\n }\n\n try {\n // Re-check after acquiring lock — another process may have just finished\n const existing = loadBridgeInfo();\n if (existing) return existing;\n\n const config = loadConfig();\n\n const keysUrl = options.keysUrl ?? config.keysUrl ?? JAW_KEYS_URL;\n if (!isValidKeysUrl(keysUrl)) {\n throw new Error(\n `Untrusted keysUrl: ${keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n\n const daemonArgs = {\n keysUrl,\n chainId: options.chainId ?? config.defaultChain ?? 1,\n ens: options.ens ?? config.ens,\n paymasterUrl: options.paymasterUrl ?? config.paymasterUrl,\n timeout: options.timeout ?? 120_000,\n };\n\n const daemonScript = path.join(findDistDir(), \"lib\", \"ws-daemon.js\");\n\n // Remove stale bridge file before spawning\n try {\n if (fs.existsSync(PATHS.bridge)) fs.unlinkSync(PATHS.bridge);\n } catch {\n // ignore\n }\n\n // Truncate log to prevent unbounded growth from previous sessions\n const logFd = fs.openSync(PATHS.daemonLog, \"w\", 0o600);\n const child = spawn(\n process.execPath,\n [daemonScript, JSON.stringify(daemonArgs)],\n {\n detached: true,\n stdio: [\"ignore\", logFd, logFd],\n // Pass API key via env var instead of process args to avoid ps aux exposure\n env: { ...process.env, JAW_DAEMON_API_KEY: options.apiKey },\n },\n );\n child.unref();\n fs.closeSync(logFd);\n\n // Poll for bridge.json to appear (daemon writes it once WS server is ready)\n const deadline = Date.now() + 15_000;\n while (Date.now() < deadline) {\n await new Promise((r) => setTimeout(r, 200));\n const info = loadBridgeInfo();\n if (info) return info;\n }\n\n throw new Error(\n `Daemon failed to start within 15s. Check ${PATHS.daemonLog} for details.`,\n );\n } finally {\n releaseLock(lockFd);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/validation.ts","../../src/lib/config.ts","../../src/lib/crypto.ts","../../src/lib/ws-bridge.ts","../../src/lib/bridge-singleton.ts"],"names":["fs","crypto2"],"mappings":"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","file":"bridge-singleton.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n relay: path.join(JAW_DIR, \"relay.json\"),\n} as const;\n","export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n\nexport function isValidRelayUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"wss:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isWebSocket =\n parsed.protocol === \"wss:\" || parsed.protocol === \"ws:\";\n return isTrustedHost && isSecure && isWebSocket;\n } catch {\n return false;\n }\n}\n","import * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport type { JawConfig } from \"./types.js\";\nimport { isValidKeysUrl, isValidRelayUrl } from \"./validation.js\";\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, \"utf-8\");\n try {\n return JSON.parse(raw) as JawConfig;\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`,\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n };\n}\n\nexport function getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n if (\n key === \"relayUrl\" &&\n typeof value === \"string\" &&\n !isValidRelayUrl(value)\n ) {\n throw new Error(\n `Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n","/**\n * E2E encryption primitives for CLI ↔ browser communication.\n *\n * Uses ECDH P-256 for key exchange and AES-256-GCM for message encryption.\n * Mirrors the same crypto operations in @jaw.id/core but uses only\n * Node.js built-in crypto.subtle — no external dependencies.\n */\n\nimport type { webcrypto } from \"node:crypto\";\n\ntype CKey = webcrypto.CryptoKey;\ntype CKeyPair = webcrypto.CryptoKeyPair;\n\nconst subtle = globalThis.crypto.subtle;\n\n// ── Key generation ───────────────────────────────────────────────\n\nexport async function generateKeyPair(): Promise<CKeyPair> {\n return subtle.generateKey(\n { name: \"ECDH\", namedCurve: \"P-256\" },\n true,\n [\"deriveKey\"],\n ) as Promise<CKeyPair>;\n}\n\nexport async function deriveSharedSecret(\n privateKey: CKey,\n peerPublicKey: CKey,\n): Promise<CKey> {\n return subtle.deriveKey(\n { name: \"ECDH\", public: peerPublicKey },\n privateKey,\n { name: \"AES-GCM\", length: 256 },\n false,\n [\"encrypt\", \"decrypt\"],\n );\n}\n\n// ── Encrypt / Decrypt ────────────────────────────────────────────\n\nexport interface EncryptedEnvelope {\n iv: string; // base64\n ciphertext: string; // base64\n}\n\nexport async function encryptMessage(\n sharedSecret: CKey,\n payload: Record<string, unknown>,\n): Promise<EncryptedEnvelope> {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));\n const plaintext = new TextEncoder().encode(JSON.stringify(payload));\n const cipherBuf = await subtle.encrypt(\n { name: \"AES-GCM\", iv },\n sharedSecret,\n plaintext,\n );\n return {\n iv: bufferToBase64(iv),\n ciphertext: bufferToBase64(new Uint8Array(cipherBuf)),\n };\n}\n\nexport async function decryptMessage(\n sharedSecret: CKey,\n envelope: EncryptedEnvelope,\n): Promise<Record<string, unknown>> {\n const iv = Buffer.from(envelope.iv, \"base64\");\n const ciphertext = Buffer.from(envelope.ciphertext, \"base64\");\n const plainBuf = await subtle.decrypt(\n { name: \"AES-GCM\", iv },\n sharedSecret,\n ciphertext,\n );\n return JSON.parse(new TextDecoder().decode(plainBuf));\n}\n\n// ── Key import / export (hex) ────────────────────────────────────\n\nexport async function exportKeyToHex(\n type: \"private\" | \"public\",\n key: CKey,\n): Promise<string> {\n const format = type === \"private\" ? \"pkcs8\" : \"spki\";\n const buf = await subtle.exportKey(format, key);\n return bytesToHex(new Uint8Array(buf));\n}\n\nexport async function importKeyFromHex(\n type: \"private\" | \"public\",\n hex: string,\n): Promise<CKey> {\n const format = type === \"private\" ? \"pkcs8\" : \"spki\";\n return subtle.importKey(\n format,\n Buffer.from(hexToBytes(hex)),\n { name: \"ECDH\", namedCurve: \"P-256\" },\n true,\n type === \"private\" ? [\"deriveKey\"] : [],\n );\n}\n\n// ── Encoding helpers ─────────────────────────────────────────────\n\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error(\"Invalid hex: odd length\");\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < hex.length; i += 2) {\n bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);\n }\n return bytes;\n}\n\nfunction bufferToBase64(buf: Uint8Array): string {\n return Buffer.from(buf).toString(\"base64\");\n}\n","/**\n * WSBridge — CLI-side relay client\n *\n * Connects to the cloud relay (wss://relay.jaw.id) instead of a local daemon.\n * All messages (except key_exchange) are E2E encrypted via ECDH + AES-256-GCM.\n */\n\nimport * as crypto from \"node:crypto\";\nimport type { webcrypto } from \"node:crypto\";\nimport WebSocket from \"ws\";\nimport {\n deriveSharedSecret,\n encryptMessage,\n decryptMessage,\n importKeyFromHex,\n type EncryptedEnvelope,\n} from \"./crypto.js\";\n\ntype CKey = webcrypto.CryptoKey;\n\nexport interface WSBridgeConfig {\n apiKey: string;\n chainId: number;\n ens?: string;\n paymasterUrl?: string;\n}\n\nexport interface WSBridgeOptions {\n relayUrl: string;\n session: string;\n timeout?: number;\n config: WSBridgeConfig;\n /** CLI's ECDH private key (hex). Loaded from relay.json for existing sessions. */\n privateKeyHex: string;\n /** CLI's ECDH public key (hex). Sent to browser for key derivation. */\n publicKeyHex: string;\n /** Browser's ECDH public key (hex). Null if new session (key exchange needed). */\n peerPublicKeyHex: string | null;\n}\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\n/**\n * Maximum outbound message size (5 MB).\n *\n * wallet_sendCalls with large batches (50+ calls, complex calldata) can reach\n * hundreds of KB. Base64 encoding of the AES-GCM ciphertext adds ~33% overhead.\n * 5 MB is generous enough for any realistic batch while still preventing\n * accidental memory issues.\n */\nconst MAX_MESSAGE_BYTES = 5 * 1024 * 1024;\n\n/** Minimum time between browser reopen attempts (ms). */\nconst BROWSER_REOPEN_COOLDOWN_MS = 5_000;\n\n/** Maximum reconnection attempts before giving up. */\nconst MAX_RECONNECT_ATTEMPTS = 3;\n\n/** Base delay for exponential backoff (ms). */\nconst RECONNECT_BASE_DELAY_MS = 1_000;\n\nexport class WSBridge {\n private readonly relayUrl: string;\n private readonly session: string;\n private readonly timeout: number;\n private readonly config: WSBridgeConfig;\n private readonly privateKeyHex: string;\n readonly publicKeyHex: string;\n private peerPublicKeyHex: string | null;\n private sharedSecret: CKey | null = null;\n private ws: WebSocket | null = null;\n private disposed = false;\n\n // Auto-reopen browser state\n private onBrowserNeeded: (() => Promise<void>) | undefined;\n private onPeerKeyChanged: ((key: string) => void) | undefined;\n private lastBrowserOpenTime = 0;\n\n // Reconnection state\n private reconnectAttempts = 0;\n\n /** Updated after key exchange — caller should persist this. */\n get peerPublicKey(): string | null {\n return this.peerPublicKeyHex;\n }\n\n constructor(options: WSBridgeOptions) {\n this.relayUrl = options.relayUrl;\n this.session = options.session;\n this.timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;\n this.config = options.config;\n this.privateKeyHex = options.privateKeyHex;\n this.publicKeyHex = options.publicKeyHex;\n this.peerPublicKeyHex = options.peerPublicKeyHex;\n }\n\n /**\n * Connect to the relay and wait for the browser to be ready.\n *\n * @param onBrowserNeeded — called when the relay reports no browser connected.\n * @param onPeerKeyChanged — called when a key_exchange updates the peer key.\n */\n async connect(\n onBrowserNeeded?: () => Promise<void>,\n onPeerKeyChanged?: (newPeerPublicKeyHex: string) => void,\n ): Promise<void> {\n // Store callbacks for auto-reopen on browser disconnect\n this.onBrowserNeeded = onBrowserNeeded;\n this.onPeerKeyChanged = onPeerKeyChanged;\n\n // Pre-derive shared secret if we already have the peer key\n if (this.peerPublicKeyHex) {\n await this.deriveSecret();\n }\n\n return this.connectInternal(onBrowserNeeded, onPeerKeyChanged);\n }\n\n private async connectInternal(\n onBrowserNeeded?: () => Promise<void>,\n onPeerKeyChanged?: (newPeerPublicKeyHex: string) => void,\n ): Promise<void> {\n return new Promise((resolve, reject) => {\n const url = `${this.relayUrl}?session=${encodeURIComponent(this.session)}&role=cli`;\n const ws = new WebSocket(url);\n\n let browserOpened = false;\n let resolved = false;\n let expectingKeyExchange = !this.peerPublicKeyHex;\n\n const timer = setTimeout(() => {\n ws.close();\n reject(\n new Error(\n \"Browser did not connect in time.\\n\" +\n \"Run `jaw disconnect` then try again.\",\n ),\n );\n }, 30_000);\n\n const sendEncryptedInit = async () => {\n if (!this.sharedSecret) return;\n const envelope = await encryptMessage(this.sharedSecret, {\n type: \"init\",\n apiKey: this.config.apiKey,\n chainId: this.config.chainId,\n ens: this.config.ens,\n paymasterUrl: this.config.paymasterUrl,\n });\n this.sendRaw(ws, JSON.stringify({ type: \"encrypted\", ...envelope }));\n };\n\n const waitForReady = () => {\n const readyTimer = setTimeout(() => {\n ws.close();\n reject(new Error(\"Browser SDK did not become ready in time.\"));\n }, 15_000);\n\n const onMsg = async (data: WebSocket.Data) => {\n const msg = safeParse(data);\n if (!msg) return;\n\n if (msg.type === \"encrypted\" && this.sharedSecret) {\n try {\n const inner = await decryptMessage(\n this.sharedSecret,\n msg as unknown as EncryptedEnvelope,\n );\n if (inner.type === \"ready\") {\n clearTimeout(readyTimer);\n ws.off(\"message\", onMsg);\n this.reconnectAttempts = 0; // Reset on successful connect\n resolve();\n }\n } catch {\n // Not a valid encrypted message for us, ignore\n }\n }\n };\n ws.on(\"message\", onMsg);\n };\n\n const onBrowserReady = async () => {\n if (resolved) return;\n resolved = true;\n clearTimeout(timer);\n // Set up listener BEFORE sending init to avoid missing the ready response\n waitForReady();\n await sendEncryptedInit();\n };\n\n ws.on(\"open\", () => {\n this.ws = ws;\n });\n\n ws.on(\"message\", async (data) => {\n const msg = safeParse(data);\n if (!msg) return;\n\n if (msg.type === \"status\") {\n if (msg.browserConnected) {\n if (this.sharedSecret) {\n // Already have shared secret — skip key exchange\n await onBrowserReady();\n } else {\n // Browser is connected but we don't have its key yet.\n expectingKeyExchange = true;\n }\n } else if (!browserOpened && onBrowserNeeded) {\n browserOpened = true;\n expectingKeyExchange = true;\n onBrowserNeeded().catch(() => {\n /* best effort */\n });\n }\n } else if (msg.type === \"browser_connected\") {\n expectingKeyExchange = true;\n // Wait for key_exchange from browser\n } else if (msg.type === \"browser_disconnected\") {\n // Browser tab closed — attempt to reopen after cooldown\n this.handleBrowserDisconnect();\n } else if (msg.type === \"key_exchange\" && expectingKeyExchange) {\n expectingKeyExchange = false;\n const peerKey = msg.publicKey as string;\n this.peerPublicKeyHex = peerKey;\n await this.deriveSecret();\n onPeerKeyChanged?.(peerKey);\n await onBrowserReady();\n }\n });\n\n ws.on(\"error\", (err) => {\n clearTimeout(timer);\n reject(err);\n });\n\n ws.on(\"close\", () => {\n clearTimeout(timer);\n if (!this.disposed) {\n this.handleRelayDisconnect();\n }\n });\n });\n }\n\n /**\n * Send an encrypted RPC request through the relay to the browser SDK.\n */\n async request(method: string, params?: unknown): Promise<unknown> {\n const ws = this.ws;\n if (!ws || ws.readyState !== WebSocket.OPEN) {\n throw new Error(\"Not connected to relay\");\n }\n if (!this.sharedSecret) {\n throw new Error(\"No shared secret — key exchange not completed\");\n }\n\n const id = crypto.randomUUID();\n\n const envelope = await encryptMessage(this.sharedSecret, {\n type: \"rpc_request\",\n id,\n method,\n params,\n });\n\n const serialized = JSON.stringify({ type: \"encrypted\", ...envelope });\n assertMessageSize(serialized, method);\n\n return new Promise<unknown>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(\n new Error(\n `Request timed out after ${this.timeout / 1000}s. ` +\n \"Did you complete the action in the browser?\",\n ),\n );\n this.close();\n }, this.timeout);\n\n const onMessage = async (data: WebSocket.Data) => {\n const msg = safeParse(data);\n if (!msg || msg.type !== \"encrypted\" || !this.sharedSecret) return;\n\n try {\n const inner = await decryptMessage(\n this.sharedSecret,\n msg as unknown as EncryptedEnvelope,\n );\n if (inner.type === \"rpc_response\" && inner.id === id) {\n clearTimeout(timer);\n ws.off(\"message\", onMessage);\n\n if (inner.success) {\n resolve(inner.data);\n } else {\n const err = inner.error as\n | { code: number; message: string }\n | undefined;\n reject(\n new Error(\n err ? `[${err.code}] ${err.message}` : \"Request failed\",\n ),\n );\n }\n }\n } catch {\n // Decryption failed — not our message or tampered, ignore\n }\n };\n\n ws.on(\"message\", onMessage);\n this.sendRaw(ws, serialized);\n });\n }\n\n isOpen(): boolean {\n return this.ws?.readyState === WebSocket.OPEN;\n }\n\n async shutdown(): Promise<void> {\n this.disposed = true;\n if (this.ws?.readyState === WebSocket.OPEN && this.sharedSecret) {\n try {\n const envelope = await encryptMessage(this.sharedSecret, {\n type: \"shutdown\",\n });\n this.sendRaw(\n this.ws,\n JSON.stringify({ type: \"encrypted\", ...envelope }),\n );\n } catch {\n // Best effort\n }\n }\n this.close();\n }\n\n /**\n * Connect to relay and send shutdown directly — no init/ready handshake.\n * Used by `jaw disconnect` when we just need to tell the browser to close.\n */\n async connectAndShutdown(): Promise<void> {\n if (!this.peerPublicKeyHex) {\n // No peer key means browser never connected — nothing to shut down\n return;\n }\n\n this.disposed = true;\n await this.deriveSecret();\n\n return new Promise<void>((resolve) => {\n const url = `${this.relayUrl}?session=${encodeURIComponent(this.session)}&role=cli`;\n const ws = new WebSocket(url);\n\n const timer = setTimeout(() => {\n try {\n ws.close();\n } catch {\n /* ignore */\n }\n resolve();\n }, 3000);\n\n ws.on(\"open\", async () => {\n this.ws = ws;\n try {\n await this.shutdown();\n } catch {\n // Best effort\n }\n clearTimeout(timer);\n resolve();\n });\n\n ws.on(\"error\", () => {\n clearTimeout(timer);\n resolve();\n });\n });\n }\n\n close(): void {\n this.disposed = true;\n if (this.ws) {\n try {\n this.ws.close();\n } catch {\n // ignore\n }\n this.ws = null;\n }\n }\n\n /**\n * Auto-reopen browser when browser_disconnected is received from relay.\n * Respects a cooldown to prevent rapid re-opening.\n */\n private handleBrowserDisconnect(): void {\n if (this.disposed || !this.onBrowserNeeded) return;\n\n const now = Date.now();\n if (now - this.lastBrowserOpenTime < BROWSER_REOPEN_COOLDOWN_MS) {\n return; // Too soon — skip this reopen\n }\n\n this.lastBrowserOpenTime = now;\n\n // Reset shared secret — the new browser tab will do a fresh key exchange\n this.sharedSecret = null;\n this.peerPublicKeyHex = null;\n\n this.onBrowserNeeded().catch(() => {\n // Best effort — if browser open fails, the next request will error\n });\n }\n\n /**\n * Attempt to reconnect to the relay with exponential backoff\n * when the WebSocket connection drops unexpectedly.\n */\n private handleRelayDisconnect(): void {\n if (this.disposed) return;\n if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;\n\n const delay = RECONNECT_BASE_DELAY_MS * Math.pow(2, this.reconnectAttempts);\n this.reconnectAttempts++;\n\n setTimeout(() => {\n if (this.disposed) return;\n this.connectInternal(this.onBrowserNeeded, this.onPeerKeyChanged).catch(\n () => {\n // Reconnection failed — will retry if attempts remain\n },\n );\n }, delay);\n }\n\n /** Send a raw string over the WebSocket, enforcing message size limits. */\n private sendRaw(ws: WebSocket, data: string): void {\n ws.send(data);\n }\n\n private async deriveSecret(): Promise<void> {\n if (!this.peerPublicKeyHex) return;\n const privateKey = await importKeyFromHex(\"private\", this.privateKeyHex);\n const peerPublicKey = await importKeyFromHex(\n \"public\",\n this.peerPublicKeyHex,\n );\n this.sharedSecret = await deriveSharedSecret(privateKey, peerPublicKey);\n }\n}\n\n/**\n * Validate message size before sending to the relay.\n * Throws with a descriptive error if the message is too large.\n */\nfunction assertMessageSize(serialized: string, method: string): void {\n const byteLength = Buffer.byteLength(serialized, \"utf-8\");\n if (byteLength > MAX_MESSAGE_BYTES) {\n const sizeMB = (byteLength / (1024 * 1024)).toFixed(2);\n throw new Error(\n `Message for ${method} is too large (${sizeMB} MB, limit ${MAX_MESSAGE_BYTES / (1024 * 1024)} MB). ` +\n \"Try reducing the number of calls in your batch.\",\n );\n }\n}\n\nfunction safeParse(data: WebSocket.Data): Record<string, unknown> | null {\n try {\n return JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n","/**\n * Bridge lifecycle management — relay edition.\n *\n * - Generates a session token + ECDH keypair for new sessions\n * - Opens the browser to keys.jaw.id/cli-bridge\n * - Saves relay state to ~/.jaw/relay.json\n * - Creates a WSBridge client connected to the relay\n */\n\nimport * as fs from \"node:fs\";\nimport * as crypto from \"node:crypto\";\nimport { PATHS } from \"./paths.js\";\nimport { ensureDir, loadConfig } from \"./config.js\";\nimport { WSBridge } from \"./ws-bridge.js\";\nimport { isValidKeysUrl, isValidRelayUrl } from \"./validation.js\";\nimport { generateKeyPair, exportKeyToHex } from \"./crypto.js\";\n\nconst DEFAULT_KEYS_URL = \"https://keys.jaw.id\";\nconst DEFAULT_RELAY_URL = \"wss://relay.jaw.id\";\n\ninterface RelaySession {\n session: string;\n relayUrl: string;\n privateKey: string;\n publicKey: string;\n peerPublicKey: string | null;\n startedAt: string;\n}\n\nexport interface BridgeOptions {\n keysUrl?: string;\n relayUrl?: string;\n apiKey: string;\n chainId?: number;\n ens?: string;\n paymasterUrl?: string;\n timeout?: number;\n}\n\nfunction loadRelaySession(): RelaySession | null {\n try {\n if (!fs.existsSync(PATHS.relay)) return null;\n const raw = fs.readFileSync(PATHS.relay, \"utf-8\");\n const parsed = JSON.parse(raw) as RelaySession;\n // Basic validation\n if (!parsed.session || !parsed.relayUrl || !parsed.privateKey || !parsed.publicKey) {\n return null;\n }\n return parsed;\n } catch {\n return null;\n }\n}\n\nfunction saveRelaySession(info: RelaySession): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.relay, JSON.stringify(info, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nfunction deleteRelaySession(): void {\n try {\n if (fs.existsSync(PATHS.relay)) fs.unlinkSync(PATHS.relay);\n } catch {\n // ignore\n }\n}\n\n/**\n * Get or create a relay bridge connection.\n */\nexport async function getBridge(options: BridgeOptions): Promise<WSBridge> {\n const config = loadConfig();\n const keysUrl = options.keysUrl ?? config.keysUrl ?? DEFAULT_KEYS_URL;\n const relayUrl = options.relayUrl ?? config.relayUrl ?? DEFAULT_RELAY_URL;\n const chainId = options.chainId ?? config.defaultChain ?? 1;\n if (!isValidKeysUrl(keysUrl)) {\n throw new Error(`Untrusted keysUrl: ${keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.`);\n }\n if (!isValidRelayUrl(relayUrl)) {\n throw new Error(`Untrusted relayUrl: ${relayUrl}. Must be wss://*.jaw.id or ws://localhost.`);\n }\n\n // Try existing session first\n let relaySession = loadRelaySession();\n if (relaySession && relaySession.relayUrl === relayUrl) {\n try {\n return await connectBridge(relaySession, options, chainId, keysUrl, relayUrl);\n } catch {\n // Connection failed — stale session or relay restarted.\n // Delete and fall through to create a new one.\n deleteRelaySession();\n relaySession = null;\n }\n }\n\n // New session\n const session = await createNewSession(relayUrl);\n saveRelaySession(session);\n return await connectBridge(session, options, chainId, keysUrl, relayUrl);\n}\n\nasync function createNewSession(relayUrl: string): Promise<RelaySession> {\n const kp = await generateKeyPair();\n const privateKey = await exportKeyToHex(\"private\", kp.privateKey);\n const publicKey = await exportKeyToHex(\"public\", kp.publicKey);\n\n return {\n session: crypto.randomUUID(),\n relayUrl,\n privateKey,\n publicKey,\n peerPublicKey: null,\n startedAt: new Date().toISOString(),\n };\n}\n\nasync function connectBridge(\n relaySession: RelaySession,\n options: BridgeOptions,\n chainId: number,\n keysUrl: string,\n relayUrl: string,\n): Promise<WSBridge> {\n const config = loadConfig();\n const bridge = new WSBridge({\n relayUrl,\n session: relaySession.session,\n timeout: options.timeout,\n config: {\n apiKey: options.apiKey,\n chainId,\n ens: options.ens ?? config.ens,\n paymasterUrl: options.paymasterUrl ?? config.paymasterUrl,\n },\n privateKeyHex: relaySession.privateKey,\n publicKeyHex: relaySession.publicKey,\n peerPublicKeyHex: relaySession.peerPublicKey,\n });\n\n await bridge.connect(\n // onBrowserNeeded\n async () => {\n const bridgeUrl = buildBridgeUrl(keysUrl, relaySession.session, relayUrl, relaySession.publicKey);\n const { default: open } = await import(\"open\");\n await open(bridgeUrl);\n },\n // onPeerKeyChanged\n (newPeerKey) => {\n relaySession.peerPublicKey = newPeerKey;\n saveRelaySession(relaySession);\n },\n );\n\n return bridge;\n}\n\nfunction buildBridgeUrl(\n keysUrl: string,\n session: string,\n relayUrl: string,\n cliPublicKeyHex: string,\n): string {\n const url = new URL(\"/cli-bridge\", keysUrl);\n url.searchParams.set(\"session\", session);\n url.searchParams.set(\"relay\", relayUrl);\n // CLI public key in fragment — not sent to server\n url.hash = `pk=${cliPublicKeyHex}`;\n return url.toString();\n}\n\n/**\n * Shutdown: close the relay session and clean up.\n */\nexport async function shutdownDaemon(): Promise<void> {\n const session = loadRelaySession();\n if (!session) return;\n\n // Send shutdown directly — no init/ready handshake needed\n try {\n const bridge = new WSBridge({\n relayUrl: session.relayUrl,\n session: session.session,\n timeout: 5000,\n config: { apiKey: \"\", chainId: 1 },\n privateKeyHex: session.privateKey,\n publicKeyHex: session.publicKey,\n peerPublicKeyHex: session.peerPublicKey,\n });\n await bridge.connectAndShutdown();\n } catch {\n // Best effort — relay or browser may already be gone\n }\n\n deleteRelaySession();\n\n // Legacy cleanup: kill old daemon process and remove files\n const legacyBridge = PATHS.root + \"/bridge.json\";\n const legacyLog = PATHS.root + \"/daemon.log\";\n const legacyLock = PATHS.root + \"/daemon.lock\";\n try {\n if (fs.existsSync(legacyBridge)) {\n const info = JSON.parse(fs.readFileSync(legacyBridge, \"utf-8\"));\n if (info.pid && Number.isInteger(info.pid) && info.pid > 0) {\n try { process.kill(info.pid, \"SIGTERM\"); } catch { /* already dead */ }\n }\n }\n } catch { /* ignore */ }\n for (const f of [legacyBridge, legacyLog, legacyLock]) {\n try { if (fs.existsSync(f)) fs.unlinkSync(f); } catch { /* ignore */ }\n }\n}\n"]}
@@ -8,8 +8,7 @@ var PATHS = {
8
8
  root: JAW_DIR,
9
9
  config: path.join(JAW_DIR, "config.json"),
10
10
  session: path.join(JAW_DIR, "session.json"),
11
- bridge: path.join(JAW_DIR, "bridge.json"),
12
- daemonLog: path.join(JAW_DIR, "daemon.log")
11
+ relay: path.join(JAW_DIR, "relay.json")
13
12
  };
14
13
 
15
14
  // src/lib/validation.ts
@@ -23,6 +22,17 @@ function isValidKeysUrl(url) {
23
22
  return false;
24
23
  }
25
24
  }
25
+ function isValidRelayUrl(url) {
26
+ try {
27
+ const parsed = new URL(url);
28
+ const isTrustedHost = parsed.hostname.endsWith(".jaw.id") || parsed.hostname === "jaw.id" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
29
+ const isSecure = parsed.protocol === "wss:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
30
+ const isWebSocket = parsed.protocol === "wss:" || parsed.protocol === "ws:";
31
+ return isTrustedHost && isSecure && isWebSocket;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
26
36
 
27
37
  // src/lib/config.ts
28
38
  function ensureDir(dir) {
@@ -65,6 +75,11 @@ function setConfigValue(key, value) {
65
75
  `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`
66
76
  );
67
77
  }
78
+ if (key === "relayUrl" && typeof value === "string" && !isValidRelayUrl(value)) {
79
+ throw new Error(
80
+ `Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`
81
+ );
82
+ }
68
83
  const config = loadConfig();
69
84
  const updated = { ...config, [key]: value };
70
85
  saveConfig(updated);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/validation.ts","../../src/lib/config.ts"],"names":[],"mappings":"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","file":"config.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n bridge: path.join(JAW_DIR, \"bridge.json\"),\n daemonLog: path.join(JAW_DIR, \"daemon.log\"),\n} as const;\n","export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n","import * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport type { JawConfig } from \"./types.js\";\nimport { isValidKeysUrl } from \"./validation.js\";\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, \"utf-8\");\n try {\n return JSON.parse(raw) as JawConfig;\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`,\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n };\n}\n\nexport function getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n"]}
1
+ {"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/validation.ts","../../src/lib/config.ts"],"names":[],"mappings":";;;;;AAGA,IAAM,OAAA,GAAe,IAAA,CAAA,IAAA,CAAQ,EAAA,CAAA,OAAA,EAAQ,EAAG,MAAM,CAAA;AAEvC,IAAM,KAAA,GAAQ;AAAA,EACnB,IAAA,EAAM,OAAA;AAAA,EACN,MAAA,EAAa,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,aAAa,CAAA;AAAA,EACxC,OAAA,EAAc,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,cAAc,CAAA;AAAA,EAC1C,KAAA,EAAY,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,YAAY;AACxC,CAAA;;;AC0BO,SAAS,eAAe,GAAA,EAAsB;AACnD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,CAAS,SAAS,CAAA,IAClC,MAAA,CAAO,QAAA,KAAa,QAAA,IACpB,MAAA,CAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,QAAA,GACJ,OAAO,QAAA,KAAa,QAAA,IACpB,OAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,OAAO,aAAA,IAAiB,QAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEO,SAAS,gBAAgB,GAAA,EAAsB;AACpD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,CAAS,SAAS,CAAA,IAClC,MAAA,CAAO,QAAA,KAAa,QAAA,IACpB,MAAA,CAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,QAAA,GACJ,OAAO,QAAA,KAAa,MAAA,IACpB,OAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,WAAA,GACJ,MAAA,CAAO,QAAA,KAAa,MAAA,IAAU,OAAO,QAAA,KAAa,KAAA;AACpD,IAAA,OAAO,iBAAiB,QAAA,IAAY,WAAA;AAAA,EACtC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;;;ACnEO,SAAS,UAAU,GAAA,EAAmB;AAC3C,EAAG,aAAU,GAAA,EAAK,EAAE,WAAW,IAAA,EAAM,IAAA,EAAM,KAAO,CAAA;AAClD,EAAG,EAAA,CAAA,SAAA,CAAU,KAAK,GAAK,CAAA;AACzB;AAEO,SAAS,UAAA,GAAwB;AACtC,EAAA,IAAI,CAAI,EAAA,CAAA,UAAA,CAAW,KAAA,CAAM,MAAM,CAAA,EAAG;AAChC,IAAA,OAAO,EAAC;AAAA,EACV;AACA,EAAA,MAAM,GAAA,GAAS,EAAA,CAAA,YAAA,CAAa,KAAA,CAAM,MAAA,EAAQ,OAAO,CAAA;AACjD,EAAA,IAAI;AACF,IAAA,OAAO,IAAA,CAAK,MAAM,GAAG,CAAA;AAAA,EACvB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,eAAA,EAAkB,MAAM,MAAM,CAAA,oEAAA;AAAA,KAChC;AAAA,EACF;AACF;AAEO,SAAS,WAAW,MAAA,EAAyB;AAClD,EAAA,SAAA,CAAU,MAAM,IAAI,CAAA;AACpB,EAAG,EAAA,CAAA,aAAA,CAAc,MAAM,MAAA,EAAQ,IAAA,CAAK,UAAU,MAAA,EAAQ,IAAA,EAAM,CAAC,CAAA,GAAI,IAAA,EAAM;AAAA,IACrE,QAAA,EAAU,OAAA;AAAA,IACV,IAAA,EAAM;AAAA,GACP,CAAA;AACH;AAEO,SAAS,aAAa,MAAA,EAA4C;AACvE,EAAA,OAAO;AAAA,IACL,GAAG,MAAA;AAAA,IACH,MAAA,EAAQ,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG,MAAA,CAAO,OAAO,KAAA,CAAM,CAAA,EAAG,CAAC,CAAC,CAAA,GAAA,CAAA,GAAQ;AAAA,GAC9D;AACF;AAEO,SAAS,eACd,GAAA,EAC6B;AAC7B,EAAA,MAAM,SAAS,UAAA,EAAW;AAC1B,EAAA,OAAO,OAAO,GAAG,CAAA;AACnB;AAEO,SAAS,cAAA,CACd,KACA,KAAA,EACM;AACN,EAAA,IACE,GAAA,KAAQ,aACR,OAAO,KAAA,KAAU,YACjB,CAAC,cAAA,CAAe,KAAK,CAAA,EACrB;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,sBAAsB,KAAK,CAAA,iDAAA;AAAA,KAC7B;AAAA,EACF;AACA,EAAA,IACE,GAAA,KAAQ,cACR,OAAO,KAAA,KAAU,YACjB,CAAC,eAAA,CAAgB,KAAK,CAAA,EACtB;AACA,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,uBAAuB,KAAK,CAAA,2CAAA;AAAA,KAC9B;AAAA,EACF;AACA,EAAA,MAAM,SAAS,UAAA,EAAW;AAC1B,EAAA,MAAM,UAAU,EAAE,GAAG,QAAQ,CAAC,GAAG,GAAG,KAAA,EAAM;AAC1C,EAAA,UAAA,CAAW,OAAO,CAAA;AACpB","file":"config.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n relay: path.join(JAW_DIR, \"relay.json\"),\n} as const;\n","export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n\nexport function isValidRelayUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"wss:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isWebSocket =\n parsed.protocol === \"wss:\" || parsed.protocol === \"ws:\";\n return isTrustedHost && isSecure && isWebSocket;\n } catch {\n return false;\n }\n}\n","import * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport type { JawConfig } from \"./types.js\";\nimport { isValidKeysUrl, isValidRelayUrl } from \"./validation.js\";\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, \"utf-8\");\n try {\n return JSON.parse(raw) as JawConfig;\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`,\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n };\n}\n\nexport function getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n if (\n key === \"relayUrl\" &&\n typeof value === \"string\" &&\n !isValidRelayUrl(value)\n ) {\n throw new Error(\n `Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n"]}
@@ -0,0 +1,74 @@
1
+ // src/lib/crypto.ts
2
+ var subtle = globalThis.crypto.subtle;
3
+ async function generateKeyPair() {
4
+ return subtle.generateKey(
5
+ { name: "ECDH", namedCurve: "P-256" },
6
+ true,
7
+ ["deriveKey"]
8
+ );
9
+ }
10
+ async function deriveSharedSecret(privateKey, peerPublicKey) {
11
+ return subtle.deriveKey(
12
+ { name: "ECDH", public: peerPublicKey },
13
+ privateKey,
14
+ { name: "AES-GCM", length: 256 },
15
+ false,
16
+ ["encrypt", "decrypt"]
17
+ );
18
+ }
19
+ async function encryptMessage(sharedSecret, payload) {
20
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
21
+ const plaintext = new TextEncoder().encode(JSON.stringify(payload));
22
+ const cipherBuf = await subtle.encrypt(
23
+ { name: "AES-GCM", iv },
24
+ sharedSecret,
25
+ plaintext
26
+ );
27
+ return {
28
+ iv: bufferToBase64(iv),
29
+ ciphertext: bufferToBase64(new Uint8Array(cipherBuf))
30
+ };
31
+ }
32
+ async function decryptMessage(sharedSecret, envelope) {
33
+ const iv = Buffer.from(envelope.iv, "base64");
34
+ const ciphertext = Buffer.from(envelope.ciphertext, "base64");
35
+ const plainBuf = await subtle.decrypt(
36
+ { name: "AES-GCM", iv },
37
+ sharedSecret,
38
+ ciphertext
39
+ );
40
+ return JSON.parse(new TextDecoder().decode(plainBuf));
41
+ }
42
+ async function exportKeyToHex(type, key) {
43
+ const format = type === "private" ? "pkcs8" : "spki";
44
+ const buf = await subtle.exportKey(format, key);
45
+ return bytesToHex(new Uint8Array(buf));
46
+ }
47
+ async function importKeyFromHex(type, hex) {
48
+ const format = type === "private" ? "pkcs8" : "spki";
49
+ return subtle.importKey(
50
+ format,
51
+ Buffer.from(hexToBytes(hex)),
52
+ { name: "ECDH", namedCurve: "P-256" },
53
+ true,
54
+ type === "private" ? ["deriveKey"] : []
55
+ );
56
+ }
57
+ function bytesToHex(bytes) {
58
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
59
+ }
60
+ function hexToBytes(hex) {
61
+ if (hex.length % 2 !== 0) throw new Error("Invalid hex: odd length");
62
+ const bytes = new Uint8Array(hex.length / 2);
63
+ for (let i = 0; i < hex.length; i += 2) {
64
+ bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
65
+ }
66
+ return bytes;
67
+ }
68
+ function bufferToBase64(buf) {
69
+ return Buffer.from(buf).toString("base64");
70
+ }
71
+
72
+ export { decryptMessage, deriveSharedSecret, encryptMessage, exportKeyToHex, generateKeyPair, importKeyFromHex };
73
+ //# sourceMappingURL=crypto.js.map
74
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/crypto.ts"],"names":[],"mappings":"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","file":"crypto.js","sourcesContent":["/**\n * E2E encryption primitives for CLI ↔ browser communication.\n *\n * Uses ECDH P-256 for key exchange and AES-256-GCM for message encryption.\n * Mirrors the same crypto operations in @jaw.id/core but uses only\n * Node.js built-in crypto.subtle — no external dependencies.\n */\n\nimport type { webcrypto } from \"node:crypto\";\n\ntype CKey = webcrypto.CryptoKey;\ntype CKeyPair = webcrypto.CryptoKeyPair;\n\nconst subtle = globalThis.crypto.subtle;\n\n// ── Key generation ───────────────────────────────────────────────\n\nexport async function generateKeyPair(): Promise<CKeyPair> {\n return subtle.generateKey(\n { name: \"ECDH\", namedCurve: \"P-256\" },\n true,\n [\"deriveKey\"],\n ) as Promise<CKeyPair>;\n}\n\nexport async function deriveSharedSecret(\n privateKey: CKey,\n peerPublicKey: CKey,\n): Promise<CKey> {\n return subtle.deriveKey(\n { name: \"ECDH\", public: peerPublicKey },\n privateKey,\n { name: \"AES-GCM\", length: 256 },\n false,\n [\"encrypt\", \"decrypt\"],\n );\n}\n\n// ── Encrypt / Decrypt ────────────────────────────────────────────\n\nexport interface EncryptedEnvelope {\n iv: string; // base64\n ciphertext: string; // base64\n}\n\nexport async function encryptMessage(\n sharedSecret: CKey,\n payload: Record<string, unknown>,\n): Promise<EncryptedEnvelope> {\n const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));\n const plaintext = new TextEncoder().encode(JSON.stringify(payload));\n const cipherBuf = await subtle.encrypt(\n { name: \"AES-GCM\", iv },\n sharedSecret,\n plaintext,\n );\n return {\n iv: bufferToBase64(iv),\n ciphertext: bufferToBase64(new Uint8Array(cipherBuf)),\n };\n}\n\nexport async function decryptMessage(\n sharedSecret: CKey,\n envelope: EncryptedEnvelope,\n): Promise<Record<string, unknown>> {\n const iv = Buffer.from(envelope.iv, \"base64\");\n const ciphertext = Buffer.from(envelope.ciphertext, \"base64\");\n const plainBuf = await subtle.decrypt(\n { name: \"AES-GCM\", iv },\n sharedSecret,\n ciphertext,\n );\n return JSON.parse(new TextDecoder().decode(plainBuf));\n}\n\n// ── Key import / export (hex) ────────────────────────────────────\n\nexport async function exportKeyToHex(\n type: \"private\" | \"public\",\n key: CKey,\n): Promise<string> {\n const format = type === \"private\" ? \"pkcs8\" : \"spki\";\n const buf = await subtle.exportKey(format, key);\n return bytesToHex(new Uint8Array(buf));\n}\n\nexport async function importKeyFromHex(\n type: \"private\" | \"public\",\n hex: string,\n): Promise<CKey> {\n const format = type === \"private\" ? \"pkcs8\" : \"spki\";\n return subtle.importKey(\n format,\n Buffer.from(hexToBytes(hex)),\n { name: \"ECDH\", namedCurve: \"P-256\" },\n true,\n type === \"private\" ? [\"deriveKey\"] : [],\n );\n}\n\n// ── Encoding helpers ─────────────────────────────────────────────\n\nfunction bytesToHex(bytes: Uint8Array): string {\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, \"0\"))\n .join(\"\");\n}\n\nfunction hexToBytes(hex: string): Uint8Array {\n if (hex.length % 2 !== 0) throw new Error(\"Invalid hex: odd length\");\n const bytes = new Uint8Array(hex.length / 2);\n for (let i = 0; i < hex.length; i += 2) {\n bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);\n }\n return bytes;\n}\n\nfunction bufferToBase64(buf: Uint8Array): string {\n return Buffer.from(buf).toString(\"base64\");\n}\n"]}
package/dist/lib/paths.js CHANGED
@@ -7,8 +7,7 @@ var PATHS = {
7
7
  root: JAW_DIR,
8
8
  config: path.join(JAW_DIR, "config.json"),
9
9
  session: path.join(JAW_DIR, "session.json"),
10
- bridge: path.join(JAW_DIR, "bridge.json"),
11
- daemonLog: path.join(JAW_DIR, "daemon.log")
10
+ relay: path.join(JAW_DIR, "relay.json")
12
11
  };
13
12
 
14
13
  export { PATHS };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/paths.ts"],"names":[],"mappings":";;;;AAGA,IAAM,OAAA,GAAe,IAAA,CAAA,IAAA,CAAQ,EAAA,CAAA,OAAA,EAAQ,EAAG,MAAM,CAAA;AAEvC,IAAM,KAAA,GAAQ;AAAA,EACnB,IAAA,EAAM,OAAA;AAAA,EACN,MAAA,EAAa,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,aAAa,CAAA;AAAA,EACxC,OAAA,EAAc,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,cAAc,CAAA;AAAA,EAC1C,MAAA,EAAa,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,aAAa,CAAA;AAAA,EACxC,SAAA,EAAgB,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,YAAY;AAC5C","file":"paths.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n bridge: path.join(JAW_DIR, \"bridge.json\"),\n daemonLog: path.join(JAW_DIR, \"daemon.log\"),\n} as const;\n"]}
1
+ {"version":3,"sources":["../../src/lib/paths.ts"],"names":[],"mappings":";;;;AAGA,IAAM,OAAA,GAAe,IAAA,CAAA,IAAA,CAAQ,EAAA,CAAA,OAAA,EAAQ,EAAG,MAAM,CAAA;AAEvC,IAAM,KAAA,GAAQ;AAAA,EACnB,IAAA,EAAM,OAAA;AAAA,EACN,MAAA,EAAa,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,aAAa,CAAA;AAAA,EACxC,OAAA,EAAc,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,cAAc,CAAA;AAAA,EAC1C,KAAA,EAAY,IAAA,CAAA,IAAA,CAAK,OAAA,EAAS,YAAY;AACxC","file":"paths.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n relay: path.join(JAW_DIR, \"relay.json\"),\n} as const;\n"]}
@@ -8,8 +8,7 @@ var PATHS = {
8
8
  root: JAW_DIR,
9
9
  config: path.join(JAW_DIR, "config.json"),
10
10
  session: path.join(JAW_DIR, "session.json"),
11
- bridge: path.join(JAW_DIR, "bridge.json"),
12
- daemonLog: path.join(JAW_DIR, "daemon.log")
11
+ relay: path.join(JAW_DIR, "relay.json")
13
12
  };
14
13
 
15
14
  // src/lib/validation.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/validation.ts","../../src/lib/config.ts","../../src/lib/session-store.ts"],"names":["fs"],"mappings":"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","file":"session-store.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n bridge: path.join(JAW_DIR, \"bridge.json\"),\n daemonLog: path.join(JAW_DIR, \"daemon.log\"),\n} as const;\n","export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n","import * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport type { JawConfig } from \"./types.js\";\nimport { isValidKeysUrl } from \"./validation.js\";\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, \"utf-8\");\n try {\n return JSON.parse(raw) as JawConfig;\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`,\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n };\n}\n\nexport function getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n","/**\n * Persists CLI session state to ~/.jaw/session.json\n * Avoids re-authenticating on every CLI invocation.\n */\n\nimport * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport { ensureDir } from \"./config.js\";\nimport { isValidAddress } from \"./validation.js\";\n\nexport interface CLISession {\n readonly address: string;\n readonly chainId: number;\n readonly connectedAt: string;\n readonly expiresAt: string;\n}\n\nconst SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nfunction isValidSessionShape(session: unknown): session is CLISession {\n if (!session || typeof session !== \"object\") return false;\n const s = session as Record<string, unknown>;\n return (\n typeof s.address === \"string\" &&\n isValidAddress(s.address) &&\n typeof s.chainId === \"number\" &&\n Number.isInteger(s.chainId) &&\n s.chainId > 0 &&\n typeof s.connectedAt === \"string\" &&\n !Number.isNaN(Date.parse(s.connectedAt)) &&\n typeof s.expiresAt === \"string\" &&\n !Number.isNaN(Date.parse(s.expiresAt))\n );\n}\n\nexport function loadSession(): CLISession | null {\n try {\n if (!fs.existsSync(PATHS.session)) return null;\n const raw = fs.readFileSync(PATHS.session, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n if (!isValidSessionShape(parsed)) {\n clearSession();\n return null;\n }\n if (!isSessionValid(parsed)) {\n clearSession();\n return null;\n }\n return parsed;\n } catch {\n return null;\n }\n}\n\nexport function saveSession(session: CLISession): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.session, JSON.stringify(session, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function clearSession(): void {\n try {\n if (fs.existsSync(PATHS.session)) {\n fs.unlinkSync(PATHS.session);\n }\n } catch {\n // ignore\n }\n}\n\nexport function isSessionValid(session: CLISession): boolean {\n return new Date(session.expiresAt).getTime() > Date.now();\n}\n\nexport function createSession(address: string, chainId: number): CLISession {\n const now = new Date();\n return {\n address,\n chainId,\n connectedAt: now.toISOString(),\n expiresAt: new Date(now.getTime() + SESSION_TTL_MS).toISOString(),\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/lib/paths.ts","../../src/lib/validation.ts","../../src/lib/config.ts","../../src/lib/session-store.ts"],"names":["fs"],"mappings":"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","file":"session-store.js","sourcesContent":["import * as path from \"node:path\";\nimport * as os from \"node:os\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\n\nexport const PATHS = {\n root: JAW_DIR,\n config: path.join(JAW_DIR, \"config.json\"),\n session: path.join(JAW_DIR, \"session.json\"),\n relay: path.join(JAW_DIR, \"relay.json\"),\n} as const;\n","export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n\nexport function isValidRelayUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"wss:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isWebSocket =\n parsed.protocol === \"wss:\" || parsed.protocol === \"ws:\";\n return isTrustedHost && isSecure && isWebSocket;\n } catch {\n return false;\n }\n}\n","import * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport type { JawConfig } from \"./types.js\";\nimport { isValidKeysUrl, isValidRelayUrl } from \"./validation.js\";\n\nexport function ensureDir(dir: string): void {\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 });\n fs.chmodSync(dir, 0o700);\n}\n\nexport function loadConfig(): JawConfig {\n if (!fs.existsSync(PATHS.config)) {\n return {};\n }\n const raw = fs.readFileSync(PATHS.config, \"utf-8\");\n try {\n return JSON.parse(raw) as JawConfig;\n } catch {\n throw new Error(\n `Config file at ${PATHS.config} is not valid JSON. Run \\`jaw config set apiKey=<key>\\` to reset it.`,\n );\n }\n}\n\nexport function saveConfig(config: JawConfig): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.config, JSON.stringify(config, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function redactConfig(config: JawConfig): Record<string, unknown> {\n return {\n ...config,\n apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...` : undefined,\n };\n}\n\nexport function getConfigValue(\n key: keyof JawConfig,\n): string | number | undefined {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue(\n key: keyof JawConfig,\n value: string | number,\n): void {\n if (\n key === \"keysUrl\" &&\n typeof value === \"string\" &&\n !isValidKeysUrl(value)\n ) {\n throw new Error(\n `Untrusted keysUrl: ${value}. Must be a *.jaw.id domain (HTTPS) or localhost.`,\n );\n }\n if (\n key === \"relayUrl\" &&\n typeof value === \"string\" &&\n !isValidRelayUrl(value)\n ) {\n throw new Error(\n `Untrusted relayUrl: ${value}. Must be wss://*.jaw.id or ws://localhost.`,\n );\n }\n const config = loadConfig();\n const updated = { ...config, [key]: value };\n saveConfig(updated);\n}\n","/**\n * Persists CLI session state to ~/.jaw/session.json\n * Avoids re-authenticating on every CLI invocation.\n */\n\nimport * as fs from \"node:fs\";\nimport { PATHS } from \"./paths.js\";\nimport { ensureDir } from \"./config.js\";\nimport { isValidAddress } from \"./validation.js\";\n\nexport interface CLISession {\n readonly address: string;\n readonly chainId: number;\n readonly connectedAt: string;\n readonly expiresAt: string;\n}\n\nconst SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours\n\nfunction isValidSessionShape(session: unknown): session is CLISession {\n if (!session || typeof session !== \"object\") return false;\n const s = session as Record<string, unknown>;\n return (\n typeof s.address === \"string\" &&\n isValidAddress(s.address) &&\n typeof s.chainId === \"number\" &&\n Number.isInteger(s.chainId) &&\n s.chainId > 0 &&\n typeof s.connectedAt === \"string\" &&\n !Number.isNaN(Date.parse(s.connectedAt)) &&\n typeof s.expiresAt === \"string\" &&\n !Number.isNaN(Date.parse(s.expiresAt))\n );\n}\n\nexport function loadSession(): CLISession | null {\n try {\n if (!fs.existsSync(PATHS.session)) return null;\n const raw = fs.readFileSync(PATHS.session, \"utf-8\");\n const parsed = JSON.parse(raw) as unknown;\n if (!isValidSessionShape(parsed)) {\n clearSession();\n return null;\n }\n if (!isSessionValid(parsed)) {\n clearSession();\n return null;\n }\n return parsed;\n } catch {\n return null;\n }\n}\n\nexport function saveSession(session: CLISession): void {\n ensureDir(PATHS.root);\n fs.writeFileSync(PATHS.session, JSON.stringify(session, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\nexport function clearSession(): void {\n try {\n if (fs.existsSync(PATHS.session)) {\n fs.unlinkSync(PATHS.session);\n }\n } catch {\n // ignore\n }\n}\n\nexport function isSessionValid(session: CLISession): boolean {\n return new Date(session.expiresAt).getTime() > Date.now();\n}\n\nexport function createSession(address: string, chainId: number): CLISession {\n const now = new Date();\n return {\n address,\n chainId,\n connectedAt: now.toISOString(),\n expiresAt: new Date(now.getTime() + SESSION_TTL_MS).toISOString(),\n };\n}\n"]}
@@ -39,7 +39,18 @@ function isValidKeysUrl(url) {
39
39
  return false;
40
40
  }
41
41
  }
42
+ function isValidRelayUrl(url) {
43
+ try {
44
+ const parsed = new URL(url);
45
+ const isTrustedHost = parsed.hostname.endsWith(".jaw.id") || parsed.hostname === "jaw.id" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
46
+ const isSecure = parsed.protocol === "wss:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
47
+ const isWebSocket = parsed.protocol === "wss:" || parsed.protocol === "ws:";
48
+ return isTrustedHost && isSecure && isWebSocket;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
42
53
 
43
- export { assertAddress, isValidAddress, isValidChainId, isValidHex, isValidKeysUrl, parseChainId, parseWei };
54
+ export { assertAddress, isValidAddress, isValidChainId, isValidHex, isValidKeysUrl, isValidRelayUrl, parseChainId, parseWei };
44
55
  //# sourceMappingURL=validation.js.map
45
56
  //# sourceMappingURL=validation.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/validation.ts"],"names":[],"mappings":";AAAO,SAAS,eAAe,KAAA,EAAwB;AACrD,EAAA,OAAO,qBAAA,CAAsB,KAAK,KAAK,CAAA;AACzC;AAEO,SAAS,WAAW,KAAA,EAAwB;AACjD,EAAA,OAAO,kBAAA,CAAmB,KAAK,KAAK,CAAA;AACtC;AAEO,SAAS,eAAe,KAAA,EAAiC;AAC9D,EAAA,MAAM,MAAM,OAAO,KAAA,KAAU,WAAW,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA,GAAI,KAAA;AAC9D,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,GAAG,CAAA,IAAK,GAAA,GAAM,CAAA;AACxC;AAEO,SAAS,aAAa,KAAA,EAAuB;AAClD,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA;AAC9B,EAAA,IAAI,CAAC,cAAA,CAAe,GAAG,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,GAAA;AACT;AAEO,SAAS,aAAA,CAAc,KAAA,EAAe,KAAA,GAAQ,SAAA,EAA0B;AAC7E,EAAA,IAAI,CAAC,cAAA,CAAe,KAAK,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,EAAA,EAAK,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,QAAA,CAAS,GAAA,EAAa,KAAA,GAAQ,OAAA,EAAiB;AAC7D,EAAA,IAAI;AACF,IAAA,OAAO,OAAO,GAAG,CAAA;AAAA,EACnB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,GAAA,EAAM,GAAG,CAAA,2BAAA,CAA6B,CAAA;AAAA,EACxE;AACF;AAEO,SAAS,eAAe,GAAA,EAAsB;AACnD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,CAAS,SAAS,CAAA,IAClC,MAAA,CAAO,QAAA,KAAa,QAAA,IACpB,MAAA,CAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,QAAA,GACJ,OAAO,QAAA,KAAa,QAAA,IACpB,OAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,OAAO,aAAA,IAAiB,QAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF","file":"validation.js","sourcesContent":["export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/lib/validation.ts"],"names":[],"mappings":";AAAO,SAAS,eAAe,KAAA,EAAwB;AACrD,EAAA,OAAO,qBAAA,CAAsB,KAAK,KAAK,CAAA;AACzC;AAEO,SAAS,WAAW,KAAA,EAAwB;AACjD,EAAA,OAAO,kBAAA,CAAmB,KAAK,KAAK,CAAA;AACtC;AAEO,SAAS,eAAe,KAAA,EAAiC;AAC9D,EAAA,MAAM,MAAM,OAAO,KAAA,KAAU,WAAW,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA,GAAI,KAAA;AAC9D,EAAA,OAAO,MAAA,CAAO,SAAA,CAAU,GAAG,CAAA,IAAK,GAAA,GAAM,CAAA;AACxC;AAEO,SAAS,aAAa,KAAA,EAAuB;AAClD,EAAA,MAAM,GAAA,GAAM,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA;AAC9B,EAAA,IAAI,CAAC,cAAA,CAAe,GAAG,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,GAAA;AACT;AAEO,SAAS,aAAA,CAAc,KAAA,EAAe,KAAA,GAAQ,SAAA,EAA0B;AAC7E,EAAA,IAAI,CAAC,cAAA,CAAe,KAAK,CAAA,EAAG;AAC1B,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,EAAA,EAAK,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACA,EAAA,OAAO,KAAA;AACT;AAEO,SAAS,QAAA,CAAS,GAAA,EAAa,KAAA,GAAQ,OAAA,EAAiB;AAC7D,EAAA,IAAI;AACF,IAAA,OAAO,OAAO,GAAG,CAAA;AAAA,EACnB,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,KAAK,CAAA,GAAA,EAAM,GAAG,CAAA,2BAAA,CAA6B,CAAA;AAAA,EACxE;AACF;AAEO,SAAS,eAAe,GAAA,EAAsB;AACnD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,CAAS,SAAS,CAAA,IAClC,MAAA,CAAO,QAAA,KAAa,QAAA,IACpB,MAAA,CAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,QAAA,GACJ,OAAO,QAAA,KAAa,QAAA,IACpB,OAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,OAAO,aAAA,IAAiB,QAAA;AAAA,EAC1B,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEO,SAAS,gBAAgB,GAAA,EAAsB;AACpD,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAS,IAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,QAAA,CAAS,QAAA,CAAS,SAAS,CAAA,IAClC,MAAA,CAAO,QAAA,KAAa,QAAA,IACpB,MAAA,CAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,QAAA,GACJ,OAAO,QAAA,KAAa,MAAA,IACpB,OAAO,QAAA,KAAa,WAAA,IACpB,OAAO,QAAA,KAAa,WAAA;AACtB,IAAA,MAAM,WAAA,GACJ,MAAA,CAAO,QAAA,KAAa,MAAA,IAAU,OAAO,QAAA,KAAa,KAAA;AACpD,IAAA,OAAO,iBAAiB,QAAA,IAAY,WAAA;AAAA,EACtC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACF","file":"validation.js","sourcesContent":["export function isValidAddress(value: string): boolean {\n return /^0x[0-9a-fA-F]{40}$/.test(value);\n}\n\nexport function isValidHex(value: string): boolean {\n return /^0x[0-9a-fA-F]*$/.test(value);\n}\n\nexport function isValidChainId(value: string | number): boolean {\n const num = typeof value === \"string\" ? parseInt(value, 10) : value;\n return Number.isInteger(num) && num > 0;\n}\n\nexport function parseChainId(value: string): number {\n const num = parseInt(value, 10);\n if (!isValidChainId(num)) {\n throw new Error(`Invalid chain ID: ${value}`);\n }\n return num;\n}\n\nexport function assertAddress(value: string, label = \"address\"): `0x${string}` {\n if (!isValidAddress(value)) {\n throw new Error(`Invalid ${label}: ${value}`);\n }\n return value as `0x${string}`;\n}\n\nexport function parseWei(raw: string, label = \"value\"): bigint {\n try {\n return BigInt(raw);\n } catch {\n throw new Error(`Invalid ${label}: \"${raw}\" is not a valid wei amount`);\n }\n}\n\nexport function isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n\nexport function isValidRelayUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"wss:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isWebSocket =\n parsed.protocol === \"wss:\" || parsed.protocol === \"ws:\";\n return isTrustedHost && isSecure && isWebSocket;\n } catch {\n return false;\n }\n}\n"]}