chainlesschain 0.162.147 → 0.162.149
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.
- package/README.md +1 -1
- package/package.json +2 -2
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-DywJ7xTv.js → AIOps-BJif14yR.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BTlqcY-q.js → ActionButton-CXek6gJY.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-FdG1Mvwy.js → Analytics-BgQhdAJi.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CVsnbvN1.js → AppLayout-DTYl5NtR.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-ITdpJ7vR.js → Audit-BzATQAeR.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BfXqnXo1.js → Backup-BzdPEQhL.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-C09ip3_E.js → BaseInput-BM0KVh8E.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-qdkhB42l.js → Chat-D7fHXHkz.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-D2KfETZD.js → ChatBubbleRenderer-Wm34RR-b.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-j6WZCuff.js → Checkbox-CAiSQ9vO.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CGZ-K3uK.js → Codegen-Df40CrEe.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CIlPOAu6.js → Col-DIT2fNFU.js} +1 -1
- package/src/assets/web-panel/assets/{Community-sY-i-hic.js → Community-CSTIbW2k.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-DcoEfyL8.js → Compact-DtqXZZUi.js} +1 -1
- package/src/assets/web-panel/assets/Compliance-CUtzw6o7.js +1 -0
- package/src/assets/web-panel/assets/{Cowork-DLY_fkLv.js → Cowork-CezmlnmU.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-D049pZBC.js → Cron-P4BITarg.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DCXdE8M9.js → Crosschain-BRRiYwvq.js} +1 -1
- package/src/assets/web-panel/assets/{DID-JCvPZtjW.js → DID-L5ijB9i4.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Cg2xu3iE.js → Dashboard-CFxro4ob.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DjJLjbLh.js → Dropdown-BmAFCQ71.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DugNcSLR.js → EmailListRenderer-DJBCEuon.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-J6Nb-E30.js → FamilyGuardDashboard-z4oLq6eT.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CQ3Ttpbl.js → Federation-DsVCpHMF.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cz0NKag4.js → FormItemContext-DP3bP5th.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-NYcMP6iz.js → GenericCardRenderer-BAXP2Gc6.js} +1 -1
- package/src/assets/web-panel/assets/{Git-B4hYN18N.js → Git-I3g_bAw9.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DOQPCcC0.js → Governance-jHS5-ioS.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-HtLF746W.js → Inference-CPL7sfx9.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BXrk8rMm.js → KnowledgeGraph-Jf236h4C.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DS0JnD3-.js → Logs-Cm2tcSKg.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-Cqnjihrz.js → Marketplace-CZK82rhi.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-Cx3Psk-U.js → McpTools-CZCeji7a.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BjyysPtj.js → Memory-3l2KVS9k.js} +2 -2
- package/src/assets/web-panel/assets/MobileBridge-D3Arxfeg.css +1 -0
- package/src/assets/web-panel/assets/MobileBridge-VYdpoLh6.js +1 -0
- package/src/assets/web-panel/assets/{MobileProjects-Xwf-fdOQ.js → MobileProjects-CiB9cmm1.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Djkql5m5.js → Mtc-wCFZbBuL.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-D2BAZlu0.js → MtcAudit-1a3THIyG.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-ByqP1ZzH.js → Multisig-DMzLB2hG.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-C_kn0nE2.js → NLProgramming-D9wZbf6l.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-7l7eXHPq.js → Notes-hrFe2ZM-.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BrCDoitw.js → NotificationSettings-CHGX5Jwm.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CRIFE2Tj.js +1 -0
- package/src/assets/web-panel/assets/{Organization-DdoNAxsM.js → Organization-By7FkhtY.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-Bw7R6dE3.js → Overflow--khGSzJn.js} +1 -1
- package/src/assets/web-panel/assets/{OverrideContext-C2r4vyBb.js → OverrideContext-BlgL9440.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DJNtBugA.js → P2P-CAG9dH35.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Dwhd5_ue.js → PdhVaultBrowser-B3SQZmK9.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-BYoSfhPR.js → Permissions-D35ec6JA.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-aYCYJbnH.js → PersonalDataHub-CxBF7hW_.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-DJp6CprF.js → Pipeline-s6EtOO1s.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-Cq9HZweR.js → Privacy-BbJYmWmO.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CmF0HsSt.js → ProjectInit-VGQq1jMT.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-P8V5Aea2.js → ProjectSettings-B1ew3Teh.js} +2 -2
- package/src/assets/web-panel/assets/Projects-CGcNIs_g.js +1 -0
- package/src/assets/web-panel/assets/{Providers-D589v0q2.js → Providers-mrO47FIK.js} +1 -1
- package/src/assets/web-panel/assets/QrScannerModal-IJF2mHyw.js +1 -0
- package/src/assets/web-panel/assets/{MobileBridge-zJpPuf4E.css → QrScannerModal-NwAHNfiJ.css} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-lvRV2osB.js → QuickAsk-CU0wN_Z0.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-sb84MTpY.js → Recommend-DhN37R6G.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-D_xOX_hu.js +4 -0
- package/src/assets/web-panel/assets/{Reputation-d9Y3vy-W.js → Reputation-D_Ssv7uH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DMdhLUTp.js → Row-Kx5dKxX7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-OL-SMDkz.js → RssFeed-BlGwqZkY.js} +2 -2
- package/src/assets/web-panel/assets/{Search-BjStLKq1.js → Search-hLFQzxpb.js} +1 -1
- package/src/assets/web-panel/assets/{Security-MeI411qr.js → Security-DMlvsVt4.js} +2 -2
- package/src/assets/web-panel/assets/{Services-UajosuhT.js → Services-Bd0Qsj2m.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CGpG-QJ1.js → Skeleton-DFclq9X3.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C6P2RPY-.js → Skills-BNtln2qY.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-B_fPprgw.js → Sla-Dch5H19G.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CqxLjSlQ.js → SpeechSettings-2UfQcU1g.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DixSfFOQ.js → SyncSettings-CD9YQr4i.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-PavzPBxc.js → Tasks-BNu-7MzI.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BYdGRnfX.js → Templates-D1n9CaIR.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BQLgnarD.js → Tenant-DenL3iBW.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-ZzU0Io1Y.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-Bv7uZRM7.js → TimelineRenderer-D87IfukO.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DxhgszRJ.js → Tokens-BkiZc8E3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-nQYHayuc.js → Trigger-BNRSytGN.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-CvqZDuZ4.js → Trust-Bwbd4X2m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DaR8GV2I.js → UkeySign-B8O2bs0o.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-C6Mpsokw.js → VideoEditing-C2fL8zmH.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-B57VRrWc.js → Wallet-BORYDdE1.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BbtEEtpq.js → WebAuthn-C0V5S2FM.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-C5LwvKmH.js → WorkflowEditor-2dR0fcHL.js} +1 -1
- package/src/assets/web-panel/assets/{chat-MzuPR5jh.js → chat-BmuAFAn8.js} +1 -1
- package/src/assets/web-panel/assets/{collapseMotion-mxzzcDsg.js → collapseMotion-DVfhbsdP.js} +1 -1
- package/src/assets/web-panel/assets/{colors-HyW8mi_y.js → colors-CN3smMcK.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DvUSzWAp.js → compact-item-DCnxb4kW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-4MppZl7X.js → createContext-vlR43pHn.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DxwmsL4j.js +1 -0
- package/src/assets/web-panel/assets/{echarts-jxgIOFUZ.js → echarts-CL0F2SnX.js} +1 -1
- package/src/assets/web-panel/assets/{hasIn-D9q3CJ-u.js → hasIn-D04tHYFd.js} +1 -1
- package/src/assets/web-panel/assets/{icons-DP3uiYxy.js → icons-BsDmGpcT.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9I5MuJ0.js → index-0DnaZGkC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Caqtu6Et.js → index-430S68MN.js} +1 -1
- package/src/assets/web-panel/assets/{index-eOQO6KnV.js → index-B78xL3s2.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ci6adKh8.js → index-BTPEZTk1.js} +1 -1
- package/src/assets/web-panel/assets/{index-CifKUNtV.js → index-BXRg8GFQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D7TDMMfu.js → index-BasvsWDM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DNYqw0MO.js → index-BeGDEXFs.js} +1 -1
- package/src/assets/web-panel/assets/{index-m0b6Fj9q.js → index-BqhASEL4.js} +1 -1
- package/src/assets/web-panel/assets/{index-BF2JsbiX.js → index-BudvKQfG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFqvWeGc.js → index-C-6NO5il.js} +1 -1
- package/src/assets/web-panel/assets/{index-SDblbKj6.js → index-C5Sxb1sE.js} +1 -1
- package/src/assets/web-panel/assets/{index-UUMCA2Sq.js → index-CP9m9Ggr.js} +4 -4
- package/src/assets/web-panel/assets/{index--Bm6OqmU.js → index-CTOCnIQ7.js} +1 -1
- package/src/assets/web-panel/assets/index-Cg2ldRfU.js +1 -0
- package/src/assets/web-panel/assets/{index-Dv7ffI2g.js → index-ClhAHDK3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BcunsBIx.js → index-CnXebZLL.js} +1 -1
- package/src/assets/web-panel/assets/{index-CvAqCAo9.js → index-CngCC8hC.js} +1 -1
- package/src/assets/web-panel/assets/{index-DcMkjb92.js → index-Cr-A0FYJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFGfC5RS.js → index-CsAsCPJ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDZ6fP5Z.js → index-D28DeAAB.js} +1 -1
- package/src/assets/web-panel/assets/{index-CgIfXOD9.js → index-D5LZVZ0L.js} +1 -1
- package/src/assets/web-panel/assets/{index-DmZ6-suL.js → index-D65_XseV.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cs-RKRUM.js → index-D8vwp4qp.js} +1 -1
- package/src/assets/web-panel/assets/{index-B994UQfE.js → index-DDwLgQY9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Btz8pU68.js → index-DIpY0qM5.js} +1 -1
- package/src/assets/web-panel/assets/{index-B5E4DhVc.js → index-DTWg-18U.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2jrnaq3.js → index-DTtRscUa.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmCo2Xdp.js → index-DYAtmqiv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Zvtru2oE.js → index-D_n8_vfI.js} +1 -1
- package/src/assets/web-panel/assets/{index-CLAZs1Vd.js → index-DbADHwhr.js} +1 -1
- package/src/assets/web-panel/assets/{index-CucRo4Vz.js → index-DbVsQBOH.js} +1 -1
- package/src/assets/web-panel/assets/index-Dwzo-dtJ.js +1 -0
- package/src/assets/web-panel/assets/{index-C0DiL97c.js → index-HhNjnCfe.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bl-E4W4q.js → index-ToUh2b1-.js} +1 -1
- package/src/assets/web-panel/assets/{index-ryFLxB1p.js → index-TslMTwNy.js} +1 -1
- package/src/assets/web-panel/assets/{index-BGQtFso0.js → index-_FIkN3jd.js} +1 -1
- package/src/assets/web-panel/assets/{index-zLwYpvX0.js → index-hpvvtAZ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dvg5xYuP.js → index-jzR7Ujuw.js} +1 -1
- package/src/assets/web-panel/assets/{index-xvUVuFnA.js → index-ljuBiQW9.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CC--PMsC.js → initDefaultProps-C3wUb4je.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DdrBjRF1.js → motion-D-jYFUNA.js} +1 -1
- package/src/assets/web-panel/assets/{move-D712Gwl2.js → move-tqb8nwJ2.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-39y-keKO.js → mtc-parser-DSOjMJMz.js} +1 -1
- package/src/assets/web-panel/assets/{omit-6Y51gDB9.js → omit-CJ7VimIW.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-Cy6EHbq9.js → pickAttrs-qXiG1iT3.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BYxdAm0p.js → placementArrow-CkGBcy1Z.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-C9R6q_cr.js → responsiveObserve-D3WvTlLO.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CXUJlilB.js → slide-BEE2ftge.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DOtrOeql.js → statusUtils-CsfBpxiG.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-D5r39o92.js → styleChecker-xPrIiJPI.js} +1 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-C5Yd-SCR.js +1 -0
- package/src/assets/web-panel/assets/{useFs-BkrT-Zbc.js → useFs-D_MDS8HI.js} +1 -1
- package/src/assets/web-panel/assets/{useMergedState-Cl2q1fes.js → useMergedState-Dc9sO2d7.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BwnnnZjU.js → usePersonalDataHub-BqJh-usA.js} +1 -1
- package/src/assets/web-panel/assets/{useRefs-DD9q4mOT.js → useRefs-CAeFRzPf.js} +1 -1
- package/src/assets/web-panel/assets/{useState-BK4hy8Ma.js → useState-LwExUYDO.js} +1 -1
- package/src/assets/web-panel/assets/{vendor-BvqAck49.js → vendor-BMxUs6UX.js} +16 -16
- package/src/assets/web-panel/assets/{vnode-SU-N4pum.js → vnode-chm_p-gz.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DHScfpTP.js → zoom-3-YDNz0Z.js} +1 -1
- package/src/assets/web-panel/index.html +3 -3
- package/src/assets/web-panel/remote-session-sw.js +43 -0
- package/src/command-manifest.json +22 -1
- package/src/commands/agent.js +44 -5
- package/src/commands/codeintel.js +252 -0
- package/src/commands/eval.js +248 -0
- package/src/commands/plugin.js +354 -0
- package/src/commands/serve.js +10 -0
- package/src/commands/team.js +456 -0
- package/src/gateways/remote-session-relay.js +160 -0
- package/src/gateways/ws/message-dispatcher.js +77 -2
- package/src/gateways/ws/remote-session-protocol.js +466 -0
- package/src/gateways/ws/ws-server.js +299 -0
- package/src/harness/golden-transcript.js +65 -0
- package/src/harness/mcp-client.js +6 -2
- package/src/harness/prompt-compressor.js +27 -1
- package/src/harness/remote-command-ledger.js +158 -0
- package/src/harness/remote-session-audit-sink.js +132 -0
- package/src/harness/remote-session-audit.js +110 -0
- package/src/harness/remote-session-crypto.js +217 -0
- package/src/harness/remote-session-push-apns.js +246 -0
- package/src/harness/remote-session-push-errors.js +15 -0
- package/src/harness/remote-session-push-fcm.js +238 -0
- package/src/harness/remote-session-push-huawei.js +174 -0
- package/src/harness/remote-session-push-oppo.js +171 -0
- package/src/harness/remote-session-push-senders.js +42 -0
- package/src/harness/remote-session-push-vivo.js +179 -0
- package/src/harness/remote-session-push-web.js +299 -0
- package/src/harness/remote-session-push-xiaomi.js +99 -0
- package/src/harness/remote-session-push.js +104 -0
- package/src/harness/remote-session-registry.js +420 -0
- package/src/index.js +6 -0
- package/src/lib/agent-core.js +2 -0
- package/src/lib/agent-sandbox.js +161 -5
- package/src/lib/agent-team/task-lease.js +434 -0
- package/src/lib/agent-team/team-budget.js +165 -0
- package/src/lib/agent-team/team-mailbox.js +106 -0
- package/src/lib/agent-team/team-runner.js +266 -0
- package/src/lib/agent-team/team-worktree.js +204 -0
- package/src/lib/agents.js +18 -0
- package/src/lib/async-hook-supervisor.cjs +305 -0
- package/src/lib/did-manager.js +6 -4
- package/src/lib/eval/runner.js +163 -0
- package/src/lib/eval/tasks.js +456 -0
- package/src/lib/eval/trend.js +185 -0
- package/src/lib/lsp/__tests__/code-intelligence.integration.test.js +129 -0
- package/src/lib/lsp/__tests__/code-intelligence.test.js +228 -0
- package/src/lib/lsp/__tests__/jsonrpc-stream.test.js +104 -0
- package/src/lib/lsp/__tests__/lsp-client.test.js +258 -0
- package/src/lib/lsp/__tests__/lsp-manager.test.js +264 -0
- package/src/lib/lsp/__tests__/lsp-server-registry.test.js +128 -0
- package/src/lib/lsp/code-intelligence.js +356 -0
- package/src/lib/lsp/jsonrpc-stream.js +139 -0
- package/src/lib/lsp/lsp-client.js +318 -0
- package/src/lib/lsp/lsp-manager.js +323 -0
- package/src/lib/lsp/lsp-server-registry.js +196 -0
- package/src/lib/mcp-oauth.js +23 -6
- package/src/lib/plugin-monitor-supervisor.js +238 -0
- package/src/lib/plugin-runtime/agents.js +51 -0
- package/src/lib/plugin-runtime/bin.js +100 -0
- package/src/lib/plugin-runtime/hooks.js +100 -0
- package/src/lib/plugin-runtime/install.js +388 -0
- package/src/lib/plugin-runtime/lsp.js +75 -0
- package/src/lib/plugin-runtime/manifest.js +458 -0
- package/src/lib/plugin-runtime/mcp.js +89 -0
- package/src/lib/plugin-runtime/monitors.js +100 -0
- package/src/lib/plugin-runtime/policy.js +144 -0
- package/src/lib/plugin-runtime/reload.js +79 -0
- package/src/lib/plugin-runtime/scopes.js +197 -0
- package/src/lib/plugin-runtime/settings.js +119 -0
- package/src/lib/plugin-runtime/signature.js +107 -0
- package/src/lib/plugin-runtime/skills.js +43 -0
- package/src/lib/plugin-runtime/trust.js +140 -0
- package/src/lib/sandbox-egress-proxy.js +162 -0
- package/src/lib/sandbox-network-policy.js +134 -0
- package/src/lib/sandbox-v2.js +10 -4
- package/src/lib/session-manager.js +7 -3
- package/src/lib/settings-hook-events.cjs +78 -6
- package/src/lib/settings-hooks.cjs +29 -3
- package/src/lib/settings-loader.cjs +35 -1
- package/src/lib/skill-loader.js +18 -0
- package/src/lib/telemetry/span-recorder.js +237 -0
- package/src/repl/agent-repl.js +400 -4
- package/src/runtime/agent-core.js +546 -52
- package/src/runtime/agent-runtime.js +8 -13
- package/src/runtime/coding-agent-contract-shared.cjs +82 -8
- package/src/runtime/coding-agent-policy.cjs +32 -7
- package/src/runtime/fallback-model.js +134 -7
- package/src/runtime/headless-runner.js +353 -108
- package/src/runtime/mcp-config.js +46 -0
- package/src/runtime/policies/agent-policy.js +2 -0
- package/src/assets/web-panel/assets/Compliance-T8EtTecW.js +0 -1
- package/src/assets/web-panel/assets/MobileBridge-CYaITZ7w.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CmtqIdwr.js +0 -1
- package/src/assets/web-panel/assets/Projects-DPd5-lNS.js +0 -1
- package/src/assets/web-panel/assets/Terminal-BzOHpq-B.js +0 -3
- package/src/assets/web-panel/assets/devWarning-6YJJMjSH.js +0 -1
- package/src/assets/web-panel/assets/index-D_oeGLr6.js +0 -1
- package/src/assets/web-panel/assets/index-Qa57YG19.js +0 -1
- package/src/assets/web-panel/assets/useFlexGapSupport-DfAD5U4Z.js +0 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Remote Session audit log — durable JSONL sink.
|
|
2
|
+
//
|
|
3
|
+
// Attaches to RemoteSessionAuditLog's `sink` seam to persist the (already
|
|
4
|
+
// privacy-filtered) audit trail to a newline-delimited JSON file so it survives
|
|
5
|
+
// a host restart for forensics. Each entry is one JSON line; the file is
|
|
6
|
+
// size-bounded with simple rotation (audit.jsonl → audit.jsonl.1 → …) so it can
|
|
7
|
+
// never grow unbounded. Reads tolerate a torn last line (crash mid-append) by
|
|
8
|
+
// skipping any line that will not parse.
|
|
9
|
+
//
|
|
10
|
+
// Deliberately dependency-free (node:fs only) and injectable — a SQLite-backed
|
|
11
|
+
// sink could implement the same { handler, readAll } shape later without
|
|
12
|
+
// touching the audit log or its call sites.
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
18
|
+
const DEFAULT_BACKUPS = 1;
|
|
19
|
+
|
|
20
|
+
export class RemoteSessionAuditFileSink {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
if (!options.path) {
|
|
23
|
+
throw new Error("RemoteSessionAuditFileSink requires a path");
|
|
24
|
+
}
|
|
25
|
+
this.path = options.path;
|
|
26
|
+
this.maxBytes = options.maxBytes || DEFAULT_MAX_BYTES;
|
|
27
|
+
this.backups = Number.isInteger(options.backups)
|
|
28
|
+
? options.backups
|
|
29
|
+
: DEFAULT_BACKUPS;
|
|
30
|
+
this.fs = options.fs || fs;
|
|
31
|
+
this._ensureDir();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_ensureDir() {
|
|
35
|
+
try {
|
|
36
|
+
this.fs.mkdirSync(path.dirname(this.path), { recursive: true });
|
|
37
|
+
} catch {
|
|
38
|
+
// Best effort — a later append will surface a real error if the dir is
|
|
39
|
+
// genuinely unwritable, and the audit log swallows sink throws anyway.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Bound function to pass as RemoteSessionAuditLog({ sink }). */
|
|
44
|
+
get handler() {
|
|
45
|
+
return (entry) => this.append(entry);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Append one entry as a JSON line, rotating first if it would overflow. */
|
|
49
|
+
append(entry) {
|
|
50
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
51
|
+
this._rotateIfNeeded(Buffer.byteLength(line, "utf8"));
|
|
52
|
+
this.fs.appendFileSync(this.path, line, "utf8");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_rotateIfNeeded(incomingBytes) {
|
|
56
|
+
let size = 0;
|
|
57
|
+
try {
|
|
58
|
+
size = this.fs.statSync(this.path).size;
|
|
59
|
+
} catch {
|
|
60
|
+
return; // No file yet — nothing to rotate.
|
|
61
|
+
}
|
|
62
|
+
if (size + incomingBytes <= this.maxBytes) return;
|
|
63
|
+
if (this.backups <= 0) {
|
|
64
|
+
// No history kept — truncate by removing the active file.
|
|
65
|
+
try {
|
|
66
|
+
this.fs.rmSync(this.path);
|
|
67
|
+
} catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Shift backups: .(<n-1>) → .(<n>), …, active → .1, dropping the oldest.
|
|
73
|
+
for (let i = this.backups; i >= 1; i -= 1) {
|
|
74
|
+
const src = i === 1 ? this.path : `${this.path}.${i - 1}`;
|
|
75
|
+
const dst = `${this.path}.${i}`;
|
|
76
|
+
try {
|
|
77
|
+
if (this.fs.existsSync(src)) this.fs.renameSync(src, dst);
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore a single failed rotation step */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read persisted entries oldest-first across the active file and its backups.
|
|
86
|
+
* Corrupt/partial lines are skipped. `limit` keeps only the newest N.
|
|
87
|
+
*/
|
|
88
|
+
readAll({ limit } = {}) {
|
|
89
|
+
const files = [];
|
|
90
|
+
for (let i = this.backups; i >= 1; i -= 1) files.push(`${this.path}.${i}`);
|
|
91
|
+
files.push(this.path);
|
|
92
|
+
const entries = [];
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = this.fs.readFileSync(file, "utf8");
|
|
97
|
+
} catch {
|
|
98
|
+
continue; // Missing backup slot — expected.
|
|
99
|
+
}
|
|
100
|
+
for (const line of raw.split("\n")) {
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (!trimmed) continue;
|
|
103
|
+
try {
|
|
104
|
+
entries.push(JSON.parse(trimmed));
|
|
105
|
+
} catch {
|
|
106
|
+
// Torn write (crash mid-append) or manual edit — skip the line.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (limit && limit > 0 && entries.length > limit) {
|
|
111
|
+
return entries.slice(entries.length - limit);
|
|
112
|
+
}
|
|
113
|
+
return entries;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Build a sink from env, or null when no audit file is configured. Enable with
|
|
118
|
+
* CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_FILE (path); tune the rotation ceiling
|
|
119
|
+
* with CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_MAX_BYTES.
|
|
120
|
+
*/
|
|
121
|
+
static fromEnv(env = {}, options = {}) {
|
|
122
|
+
const filePath = env.CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_FILE;
|
|
123
|
+
if (!filePath) return null;
|
|
124
|
+
const maxBytes =
|
|
125
|
+
Number(env.CHAINLESSCHAIN_REMOTE_SESSION_AUDIT_MAX_BYTES) || undefined;
|
|
126
|
+
return new RemoteSessionAuditFileSink({
|
|
127
|
+
path: filePath,
|
|
128
|
+
maxBytes,
|
|
129
|
+
...options,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Remote Session audit log.
|
|
2
|
+
//
|
|
3
|
+
// A bounded, in-memory, privacy-respecting audit trail for one CLI host process
|
|
4
|
+
// — matched to the ephemeral (in-memory, 12h TTL) nature of remote sessions.
|
|
5
|
+
// Captures WHO did WHAT and WHEN across a session's lifecycle and remote control
|
|
6
|
+
// events, but deliberately does NOT record prompt/tool payloads (only shapes
|
|
7
|
+
// like content length), keeping the trail useful for forensics without
|
|
8
|
+
// hoarding session content. A pluggable `sink` seam lets a durable backend
|
|
9
|
+
// (JSONL file, SQLite audit_log) be attached later without touching call sites.
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MAX_ENTRIES = 1000;
|
|
12
|
+
|
|
13
|
+
export const REMOTE_SESSION_AUDIT_ACTIONS = Object.freeze([
|
|
14
|
+
"session.created",
|
|
15
|
+
"pairing-token.issued",
|
|
16
|
+
"device.joined",
|
|
17
|
+
"device.revoked",
|
|
18
|
+
"device.disconnected",
|
|
19
|
+
"session.closed",
|
|
20
|
+
"control.prompt",
|
|
21
|
+
"control.approval",
|
|
22
|
+
"control.interrupt",
|
|
23
|
+
"push.registered",
|
|
24
|
+
"push.sent",
|
|
25
|
+
"push.failed",
|
|
26
|
+
"push.skipped",
|
|
27
|
+
"push.unregistered",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export class RemoteSessionAuditLog {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.now = options.now || Date.now;
|
|
33
|
+
this.maxEntries = options.maxEntries || DEFAULT_MAX_ENTRIES;
|
|
34
|
+
this.sink = typeof options.sink === "function" ? options.sink : null;
|
|
35
|
+
this.entries = [];
|
|
36
|
+
this.seq = 0;
|
|
37
|
+
// Hydrate from a durable sink so audit queries survive a host restart. The
|
|
38
|
+
// persisted entries are NOT re-written to the sink (they are already
|
|
39
|
+
// there); we only pre-fill the in-memory ring and carry the seq high-water
|
|
40
|
+
// forward so new entries stay strictly monotonic.
|
|
41
|
+
if (Array.isArray(options.initialEntries)) {
|
|
42
|
+
this._hydrate(options.initialEntries);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_hydrate(entries) {
|
|
47
|
+
const recent = entries.slice(-this.maxEntries);
|
|
48
|
+
for (const entry of recent) {
|
|
49
|
+
if (!entry || !entry.action) continue;
|
|
50
|
+
this.entries.push({ ...entry });
|
|
51
|
+
if (typeof entry.seq === "number" && entry.seq > this.seq) {
|
|
52
|
+
this.seq = entry.seq;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Append an audit entry. Never throws into the caller — a broken sink or bad
|
|
59
|
+
* argument must not take down the live session it is auditing.
|
|
60
|
+
*/
|
|
61
|
+
record({ sessionId = null, actor = null, action, detail = null } = {}) {
|
|
62
|
+
if (!action) return null;
|
|
63
|
+
const entry = {
|
|
64
|
+
seq: (this.seq += 1),
|
|
65
|
+
timestamp: this.now(),
|
|
66
|
+
sessionId,
|
|
67
|
+
actor,
|
|
68
|
+
action,
|
|
69
|
+
detail: detail && typeof detail === "object" ? { ...detail } : detail,
|
|
70
|
+
};
|
|
71
|
+
this.entries.push(entry);
|
|
72
|
+
if (this.entries.length > this.maxEntries) {
|
|
73
|
+
this.entries.splice(0, this.entries.length - this.maxEntries);
|
|
74
|
+
}
|
|
75
|
+
if (this.sink) {
|
|
76
|
+
try {
|
|
77
|
+
this.sink(entry);
|
|
78
|
+
} catch {
|
|
79
|
+
// A durable-sink failure must not break the session being audited.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Newest-first, optionally filtered by session and/or action. */
|
|
86
|
+
list({ sessionId, action, limit } = {}) {
|
|
87
|
+
let out = this.entries;
|
|
88
|
+
if (sessionId) out = out.filter((entry) => entry.sessionId === sessionId);
|
|
89
|
+
if (action) out = out.filter((entry) => entry.action === action);
|
|
90
|
+
out = [...out].reverse();
|
|
91
|
+
if (limit && limit > 0) out = out.slice(0, limit);
|
|
92
|
+
return out.map((entry) => ({ ...entry }));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
stats(sessionId) {
|
|
96
|
+
const scope = sessionId
|
|
97
|
+
? this.entries.filter((entry) => entry.sessionId === sessionId)
|
|
98
|
+
: this.entries;
|
|
99
|
+
const byAction = {};
|
|
100
|
+
for (const entry of scope) {
|
|
101
|
+
byAction[entry.action] = (byAction[entry.action] || 0) + 1;
|
|
102
|
+
}
|
|
103
|
+
return { total: scope.length, byAction };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
clear() {
|
|
107
|
+
this.entries = [];
|
|
108
|
+
this.seq = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHash,
|
|
5
|
+
createPrivateKey,
|
|
6
|
+
createPublicKey,
|
|
7
|
+
diffieHellman,
|
|
8
|
+
generateKeyPairSync,
|
|
9
|
+
hkdfSync,
|
|
10
|
+
randomBytes,
|
|
11
|
+
} from "crypto";
|
|
12
|
+
|
|
13
|
+
const PROTOCOL = "chainlesschain.remote-session.e2ee.v1";
|
|
14
|
+
const PAIRING_SCHEME = "chainlesschain://remote-session/pair#";
|
|
15
|
+
|
|
16
|
+
function b64(value) {
|
|
17
|
+
return Buffer.from(value).toString("base64url");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unb64(value) {
|
|
21
|
+
return Buffer.from(value, "base64url");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function exportPublicKey(key) {
|
|
25
|
+
return b64(key.export({ type: "spki", format: "der" }));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function importPublicKey(value) {
|
|
29
|
+
let der = unb64(value);
|
|
30
|
+
if (der.length === 32) {
|
|
31
|
+
// RFC 8410 SubjectPublicKeyInfo prefix for a raw X25519 public key.
|
|
32
|
+
der = Buffer.concat([Buffer.from("302a300506032b656e032100", "hex"), der]);
|
|
33
|
+
}
|
|
34
|
+
return createPublicKey({ key: der, type: "spki", format: "der" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function aad(sessionId, senderId, sequence) {
|
|
38
|
+
return Buffer.from(
|
|
39
|
+
`${PROTOCOL}\n${sessionId}\n${senderId}\n${sequence}`,
|
|
40
|
+
"utf8",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function deriveKey(privateKey, peerPublicKey, sessionId, pairingToken) {
|
|
45
|
+
const secret = diffieHellman({ privateKey, publicKey: peerPublicKey });
|
|
46
|
+
const salt = createHash("sha256").update(pairingToken, "utf8").digest();
|
|
47
|
+
return Buffer.from(
|
|
48
|
+
hkdfSync(
|
|
49
|
+
"sha256",
|
|
50
|
+
secret,
|
|
51
|
+
salt,
|
|
52
|
+
Buffer.from(`${PROTOCOL}:${sessionId}`),
|
|
53
|
+
32,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createRemoteSessionKeyPair() {
|
|
59
|
+
const { publicKey, privateKey } = generateKeyPairSync("x25519");
|
|
60
|
+
return {
|
|
61
|
+
publicKey: exportPublicKey(publicKey),
|
|
62
|
+
privateKey: b64(privateKey.export({ type: "pkcs8", format: "der" })),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createRemotePairingUri({
|
|
67
|
+
relayUrl,
|
|
68
|
+
remoteSessionId,
|
|
69
|
+
hostPeerId,
|
|
70
|
+
hostPublicKey,
|
|
71
|
+
pairingToken,
|
|
72
|
+
expiresAt,
|
|
73
|
+
}) {
|
|
74
|
+
if (
|
|
75
|
+
!relayUrl ||
|
|
76
|
+
!remoteSessionId ||
|
|
77
|
+
!hostPeerId ||
|
|
78
|
+
!hostPublicKey ||
|
|
79
|
+
!pairingToken
|
|
80
|
+
) {
|
|
81
|
+
throw new Error("Incomplete Remote Session pairing payload");
|
|
82
|
+
}
|
|
83
|
+
const payload = {
|
|
84
|
+
v: 1,
|
|
85
|
+
relayUrl,
|
|
86
|
+
remoteSessionId,
|
|
87
|
+
hostPeerId,
|
|
88
|
+
hostPublicKey,
|
|
89
|
+
pairingToken,
|
|
90
|
+
expiresAt,
|
|
91
|
+
};
|
|
92
|
+
return `${PAIRING_SCHEME}${b64(JSON.stringify(payload))}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function parseRemotePairingUri(uri, now = Date.now()) {
|
|
96
|
+
if (typeof uri !== "string" || !uri.startsWith(PAIRING_SCHEME)) {
|
|
97
|
+
throw new Error("Invalid Remote Session pairing URI");
|
|
98
|
+
}
|
|
99
|
+
let payload;
|
|
100
|
+
try {
|
|
101
|
+
payload = JSON.parse(
|
|
102
|
+
unb64(uri.slice(PAIRING_SCHEME.length)).toString("utf8"),
|
|
103
|
+
);
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error("Malformed Remote Session pairing payload");
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
payload.v !== 1 ||
|
|
109
|
+
!payload.relayUrl ||
|
|
110
|
+
!payload.remoteSessionId ||
|
|
111
|
+
!payload.hostPeerId ||
|
|
112
|
+
!payload.hostPublicKey ||
|
|
113
|
+
!payload.pairingToken
|
|
114
|
+
) {
|
|
115
|
+
throw new Error("Incomplete Remote Session pairing payload");
|
|
116
|
+
}
|
|
117
|
+
if (payload.expiresAt && payload.expiresAt <= now) {
|
|
118
|
+
throw new Error("Remote Session pairing payload expired");
|
|
119
|
+
}
|
|
120
|
+
return payload;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class RemoteSessionCryptoContext {
|
|
124
|
+
constructor({ sessionId, localPeerId, privateKey, publicKey } = {}) {
|
|
125
|
+
if (!sessionId || !localPeerId)
|
|
126
|
+
throw new Error("sessionId and localPeerId are required");
|
|
127
|
+
let keyPair;
|
|
128
|
+
if (!privateKey || !publicKey) keyPair = createRemoteSessionKeyPair();
|
|
129
|
+
this.sessionId = sessionId;
|
|
130
|
+
this.localPeerId = localPeerId;
|
|
131
|
+
this.privateKey = createPrivateKey({
|
|
132
|
+
key: unb64(privateKey || keyPair.privateKey),
|
|
133
|
+
type: "pkcs8",
|
|
134
|
+
format: "der",
|
|
135
|
+
});
|
|
136
|
+
this.publicKey = publicKey || keyPair.publicKey;
|
|
137
|
+
this.keys = new Map();
|
|
138
|
+
this.sendSequence = 0;
|
|
139
|
+
this.receivedSequences = new Map();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pair(peerId, peerPublicKey, pairingToken) {
|
|
143
|
+
if (!peerId || !peerPublicKey || !pairingToken)
|
|
144
|
+
throw new Error("Incomplete peer key material");
|
|
145
|
+
this.keys.set(
|
|
146
|
+
peerId,
|
|
147
|
+
deriveKey(
|
|
148
|
+
this.privateKey,
|
|
149
|
+
importPublicKey(peerPublicKey),
|
|
150
|
+
this.sessionId,
|
|
151
|
+
pairingToken,
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
encrypt(peerId, message) {
|
|
157
|
+
const key = this.keys.get(peerId);
|
|
158
|
+
if (!key) throw new Error(`No encryption key for peer: ${peerId}`);
|
|
159
|
+
const sequence = ++this.sendSequence;
|
|
160
|
+
const nonce = randomBytes(12);
|
|
161
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
162
|
+
cipher.setAAD(aad(this.sessionId, this.localPeerId, sequence));
|
|
163
|
+
const ciphertext = Buffer.concat([
|
|
164
|
+
cipher.update(JSON.stringify(message), "utf8"),
|
|
165
|
+
cipher.final(),
|
|
166
|
+
]);
|
|
167
|
+
return {
|
|
168
|
+
v: 1,
|
|
169
|
+
sessionId: this.sessionId,
|
|
170
|
+
senderId: this.localPeerId,
|
|
171
|
+
sequence,
|
|
172
|
+
nonce: b64(nonce),
|
|
173
|
+
ciphertext: b64(ciphertext),
|
|
174
|
+
tag: b64(cipher.getAuthTag()),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
decrypt(envelope) {
|
|
179
|
+
if (envelope?.v !== 1 || envelope.sessionId !== this.sessionId) {
|
|
180
|
+
throw new Error("Invalid encrypted Remote Session envelope");
|
|
181
|
+
}
|
|
182
|
+
const key = this.keys.get(envelope.senderId);
|
|
183
|
+
if (!key)
|
|
184
|
+
throw new Error(`No encryption key for peer: ${envelope.senderId}`);
|
|
185
|
+
const previous = this.receivedSequences.get(envelope.senderId) || 0;
|
|
186
|
+
if (
|
|
187
|
+
!Number.isSafeInteger(envelope.sequence) ||
|
|
188
|
+
envelope.sequence <= previous
|
|
189
|
+
) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Remote Session replay or out-of-order envelope rejected",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const decipher = createDecipheriv(
|
|
196
|
+
"aes-256-gcm",
|
|
197
|
+
key,
|
|
198
|
+
unb64(envelope.nonce),
|
|
199
|
+
);
|
|
200
|
+
decipher.setAAD(
|
|
201
|
+
aad(this.sessionId, envelope.senderId, envelope.sequence),
|
|
202
|
+
);
|
|
203
|
+
decipher.setAuthTag(unb64(envelope.tag));
|
|
204
|
+
const plaintext = Buffer.concat([
|
|
205
|
+
decipher.update(unb64(envelope.ciphertext)),
|
|
206
|
+
decipher.final(),
|
|
207
|
+
]);
|
|
208
|
+
const message = JSON.parse(plaintext.toString("utf8"));
|
|
209
|
+
this.receivedSequences.set(envelope.senderId, envelope.sequence);
|
|
210
|
+
return message;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Remote Session envelope authentication failed: ${error.message}`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// Remote Session vendor push — Apple Push Notification service (APNs) sender.
|
|
2
|
+
//
|
|
3
|
+
// A concrete implementation of the RemoteSessionPushDispatcher `sender` contract
|
|
4
|
+
// for iOS/macOS devices, using APNs' HTTP/2 provider API with token-based
|
|
5
|
+
// (JWT ES256) auth: a short-lived bearer signed with the .p8 auth key
|
|
6
|
+
// (key id + team id). APNs mandates HTTP/2, so the network transport is an
|
|
7
|
+
// injectable `request` function (default: node:http2) — everything else is
|
|
8
|
+
// injectable too (clock, signer), so the whole chain is unit-testable with a
|
|
9
|
+
// generated EC keypair and a fake transport, no real credentials or network.
|
|
10
|
+
// The payload carries NO session content, only routing ids.
|
|
11
|
+
|
|
12
|
+
import { sign as cryptoSign } from "node:crypto";
|
|
13
|
+
import http2 from "node:http2";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import { PushTokenUnregisteredError } from "./remote-session-push-errors.js";
|
|
16
|
+
|
|
17
|
+
const PROD_HOST = "https://api.push.apple.com";
|
|
18
|
+
const SANDBOX_HOST = "https://api.sandbox.push.apple.com";
|
|
19
|
+
// APNs auth tokens are valid up to 60 min; refresh well inside that window.
|
|
20
|
+
const JWT_TTL_MS = 50 * 60 * 1000;
|
|
21
|
+
const JWT_SKEW_MS = 60 * 1000;
|
|
22
|
+
|
|
23
|
+
function base64url(input) {
|
|
24
|
+
return Buffer.from(input)
|
|
25
|
+
.toString("base64")
|
|
26
|
+
.replace(/\+/g, "-")
|
|
27
|
+
.replace(/\//g, "_")
|
|
28
|
+
.replace(/=+$/, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function dropUndefined(obj) {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
34
|
+
if (value !== undefined) out[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Default HTTP/2 transport for APNs. Resolves `{ status, headers, body }`. */
|
|
40
|
+
function http2Request({ url, headers, body }) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const target = new URL(url);
|
|
43
|
+
const client = http2.connect(`${target.protocol}//${target.host}`);
|
|
44
|
+
client.on("error", reject);
|
|
45
|
+
const req = client.request({
|
|
46
|
+
":method": "POST",
|
|
47
|
+
":path": `${target.pathname}${target.search}`,
|
|
48
|
+
...headers,
|
|
49
|
+
});
|
|
50
|
+
let status = 0;
|
|
51
|
+
let responseHeaders = {};
|
|
52
|
+
let data = "";
|
|
53
|
+
req.on("response", (h) => {
|
|
54
|
+
status = Number(h[":status"]) || 0;
|
|
55
|
+
responseHeaders = h;
|
|
56
|
+
});
|
|
57
|
+
req.setEncoding("utf8");
|
|
58
|
+
req.on("data", (chunk) => {
|
|
59
|
+
data += chunk;
|
|
60
|
+
});
|
|
61
|
+
req.on("end", () => {
|
|
62
|
+
client.close();
|
|
63
|
+
resolve({ status, headers: responseHeaders, body: data });
|
|
64
|
+
});
|
|
65
|
+
req.on("error", (error) => {
|
|
66
|
+
client.close();
|
|
67
|
+
reject(error);
|
|
68
|
+
});
|
|
69
|
+
req.end(body);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Mints + caches the ES256 provider JWT from a .p8 auth key. */
|
|
74
|
+
export class ApnsTokenProvider {
|
|
75
|
+
constructor(options = {}) {
|
|
76
|
+
if (!options.keyId) throw new Error("ApnsTokenProvider requires a keyId");
|
|
77
|
+
if (!options.teamId) throw new Error("ApnsTokenProvider requires a teamId");
|
|
78
|
+
if (!options.privateKey) {
|
|
79
|
+
throw new Error("ApnsTokenProvider requires a privateKey (.p8 contents)");
|
|
80
|
+
}
|
|
81
|
+
this.keyId = options.keyId;
|
|
82
|
+
this.teamId = options.teamId;
|
|
83
|
+
this.privateKey = options.privateKey;
|
|
84
|
+
this.now = options.now || Date.now;
|
|
85
|
+
// ES256 = ECDSA P-256 / SHA-256 with a raw R||S signature (JOSE form).
|
|
86
|
+
this.sign =
|
|
87
|
+
options.sign ||
|
|
88
|
+
((input, key) =>
|
|
89
|
+
cryptoSign("sha256", Buffer.from(input), {
|
|
90
|
+
key,
|
|
91
|
+
dsaEncoding: "ieee-p1363",
|
|
92
|
+
}));
|
|
93
|
+
this._cached = null; // { token, expiresAt }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getToken() {
|
|
97
|
+
const now = this.now();
|
|
98
|
+
if (this._cached && this._cached.expiresAt - JWT_SKEW_MS > now) {
|
|
99
|
+
return this._cached.token;
|
|
100
|
+
}
|
|
101
|
+
const header = base64url(
|
|
102
|
+
JSON.stringify({ alg: "ES256", kid: this.keyId, typ: "JWT" }),
|
|
103
|
+
);
|
|
104
|
+
const payload = base64url(
|
|
105
|
+
JSON.stringify({ iss: this.teamId, iat: Math.floor(now / 1000) }),
|
|
106
|
+
);
|
|
107
|
+
const signingInput = `${header}.${payload}`;
|
|
108
|
+
const signature = base64url(this.sign(signingInput, this.privateKey));
|
|
109
|
+
const token = `${signingInput}.${signature}`;
|
|
110
|
+
this._cached = { token, expiresAt: now + JWT_TTL_MS };
|
|
111
|
+
return token;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Sends one high-priority alert to a device token via APNs HTTP/2. */
|
|
116
|
+
export class ApnsPushSender {
|
|
117
|
+
constructor(options = {}) {
|
|
118
|
+
if (!options.tokenProvider) {
|
|
119
|
+
throw new Error("ApnsPushSender requires a tokenProvider");
|
|
120
|
+
}
|
|
121
|
+
if (!options.topic) {
|
|
122
|
+
throw new Error("ApnsPushSender requires a topic (app bundle id)");
|
|
123
|
+
}
|
|
124
|
+
this.tokenProvider = options.tokenProvider;
|
|
125
|
+
this.topic = options.topic;
|
|
126
|
+
this.host = options.host || (options.production ? PROD_HOST : SANDBOX_HOST);
|
|
127
|
+
this.request = options.request || http2Request;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Bound function matching the dispatcher's `sender` contract. */
|
|
131
|
+
get handler() {
|
|
132
|
+
return (payload) => this.send(payload);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async send({ token, notification, sessionId, clientId } = {}) {
|
|
136
|
+
if (!token) throw new Error("APNs send requires a device token");
|
|
137
|
+
const jwt = this.tokenProvider.getToken();
|
|
138
|
+
const payload = {
|
|
139
|
+
aps: dropUndefined({
|
|
140
|
+
alert: notification
|
|
141
|
+
? dropUndefined({
|
|
142
|
+
title: notification.title,
|
|
143
|
+
body: notification.body,
|
|
144
|
+
})
|
|
145
|
+
: undefined,
|
|
146
|
+
sound: "default",
|
|
147
|
+
}),
|
|
148
|
+
// Routing ids only; no session content.
|
|
149
|
+
"remote-session": dropUndefined({
|
|
150
|
+
type: "approval-request",
|
|
151
|
+
sessionId: sessionId != null ? String(sessionId) : undefined,
|
|
152
|
+
clientId: clientId != null ? String(clientId) : undefined,
|
|
153
|
+
}),
|
|
154
|
+
};
|
|
155
|
+
const res = await this.request({
|
|
156
|
+
url: `${this.host}/3/device/${token}`,
|
|
157
|
+
headers: {
|
|
158
|
+
authorization: `bearer ${jwt}`,
|
|
159
|
+
"apns-topic": this.topic,
|
|
160
|
+
"apns-push-type": "alert",
|
|
161
|
+
"apns-priority": "10",
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify(payload),
|
|
164
|
+
});
|
|
165
|
+
if (res.status === 200) {
|
|
166
|
+
return { id: res.headers?.["apns-id"] || null };
|
|
167
|
+
}
|
|
168
|
+
const reason = parseReason(res.body);
|
|
169
|
+
// A retired/invalid device token: 410 Unregistered or 400 BadDeviceToken —
|
|
170
|
+
// prune it rather than retry forever.
|
|
171
|
+
if (
|
|
172
|
+
res.status === 410 ||
|
|
173
|
+
reason === "Unregistered" ||
|
|
174
|
+
reason === "BadDeviceToken"
|
|
175
|
+
) {
|
|
176
|
+
throw new PushTokenUnregisteredError(
|
|
177
|
+
`APNs token unregistered: ${reason || res.status}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`APNs send failed (${res.status}): ${reason || res.body}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseReason(body) {
|
|
185
|
+
if (!body) return null;
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(body).reason || null;
|
|
188
|
+
} catch {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function loadPrivateKey(env, options) {
|
|
194
|
+
if (options.privateKey) return options.privateKey;
|
|
195
|
+
const inline = env.CHAINLESSCHAIN_REMOTE_SESSION_APNS_KEY_P8;
|
|
196
|
+
if (inline) return inline;
|
|
197
|
+
const filePath = env.CHAINLESSCHAIN_REMOTE_SESSION_APNS_KEY;
|
|
198
|
+
if (filePath) {
|
|
199
|
+
const fsMod = options.fs || fs;
|
|
200
|
+
try {
|
|
201
|
+
return fsMod.readFileSync(filePath, "utf8");
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build a bound APNs `sender` from env, or null when APNs is not configured.
|
|
211
|
+
*
|
|
212
|
+
* APNs env:
|
|
213
|
+
* CHAINLESSCHAIN_REMOTE_SESSION_APNS_KEY (path to .p8), or
|
|
214
|
+
* CHAINLESSCHAIN_REMOTE_SESSION_APNS_KEY_P8 (inline .p8 PEM)
|
|
215
|
+
* CHAINLESSCHAIN_REMOTE_SESSION_APNS_KEY_ID
|
|
216
|
+
* CHAINLESSCHAIN_REMOTE_SESSION_APNS_TEAM_ID
|
|
217
|
+
* CHAINLESSCHAIN_REMOTE_SESSION_APNS_TOPIC (app bundle id)
|
|
218
|
+
* CHAINLESSCHAIN_REMOTE_SESSION_APNS_PRODUCTION ("true" → prod host)
|
|
219
|
+
*/
|
|
220
|
+
export function createApnsPushSender(env = {}, options = {}) {
|
|
221
|
+
const privateKey = loadPrivateKey(env, options);
|
|
222
|
+
const keyId = options.keyId || env.CHAINLESSCHAIN_REMOTE_SESSION_APNS_KEY_ID;
|
|
223
|
+
const teamId =
|
|
224
|
+
options.teamId || env.CHAINLESSCHAIN_REMOTE_SESSION_APNS_TEAM_ID;
|
|
225
|
+
const topic = options.topic || env.CHAINLESSCHAIN_REMOTE_SESSION_APNS_TOPIC;
|
|
226
|
+
if (!privateKey || !keyId || !teamId || !topic) return null;
|
|
227
|
+
let tokenProvider;
|
|
228
|
+
try {
|
|
229
|
+
tokenProvider =
|
|
230
|
+
options.tokenProvider ||
|
|
231
|
+
new ApnsTokenProvider({ keyId, teamId, privateKey, ...options });
|
|
232
|
+
} catch {
|
|
233
|
+
return null; // Malformed credentials — stay disabled rather than crash.
|
|
234
|
+
}
|
|
235
|
+
const production =
|
|
236
|
+
options.production !== undefined
|
|
237
|
+
? options.production
|
|
238
|
+
: String(env.CHAINLESSCHAIN_REMOTE_SESSION_APNS_PRODUCTION) === "true";
|
|
239
|
+
const sender = new ApnsPushSender({
|
|
240
|
+
tokenProvider,
|
|
241
|
+
topic,
|
|
242
|
+
production,
|
|
243
|
+
...options,
|
|
244
|
+
});
|
|
245
|
+
return sender.handler;
|
|
246
|
+
}
|