chainlesschain 0.166.7 → 0.166.9
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 +21 -0
- package/package.json +3 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-BlrmQxa2.js → AIOps-DJvpifGx.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-zyRyLITg.js → ActionButton-D2UhuF_A.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DwGz7OGN.js → Analytics-C9ho9obu.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-Dc-nj7Zd.js → AppLayout-BvRCpFK3.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-7Hu3O73p.js → Artifacts-jxrySvpk.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-Bnoe3-fR.js → Audit-ovEw0ZYl.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BqYFrDKZ.js → BackgroundAgents-D7H5z6zS.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-B3hwW1nz.js → Backup-BuXob8za.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-XFVxLA4w.js → BaseInput-CtnszuQ2.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CWRDNw7e.js → Chat-B_mYE8fJ.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-Bf30VM8G.js → ChatBubbleRenderer-DaiA6CsX.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-kFeeBgac.js → Checkbox-ierdtcJD.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CCxp8xoI.js → Codegen-3VKPJD_B.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BLKZalle.js → Col-1EdMyf5P.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DD0eWCez.js → Community-D6H7UA8K.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CHqHWdy0.js → Compact-BBAZvYIm.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Bw54uPiK.js → Compliance-CgsTrAoj.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DZ13zKNJ.js → Cowork-tnZgLoTE.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-Dlj5Jevg.js → Cron-CPpZw2Nh.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-Nvz0aR0j.js → Crosschain-BymBcp5z.js} +1 -1
- package/src/assets/web-panel/assets/{DID-Ct89eUyY.js → DID-DKPxS_K1.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BHUH5IAe.js → Dashboard-DvJhEmH9.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BhZG-PO7.js → Dropdown-b8iQyQIZ.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-ae4RVwwJ.js → EmailListRenderer--0-A-zKb.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-AOcxLfLr.js → FamilyGuardDashboard-C12iDN06.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BK2PJQOF.js → Federation-BRhaE2Z8.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-C2n3xO2_.js → FormItemContext-C4dnq7yf.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DcyUVFZq.js → GenericCardRenderer-CGAdbR5n.js} +1 -1
- package/src/assets/web-panel/assets/{Git-tBae9Lt8.js → Git-4eA2fYzL.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DmNR3AP4.js → Governance-BJuY9Y-9.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CoseEigi.js → Inference-DZ5Dy4up.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-Bdjd1sxr.js → KnowledgeGraph-BMTjRs4A.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-CtlkKIcn.js → Logs-BKEeQEfJ.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-B05DgASC.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BTYb7nEx.js → Marketplace-Dhho4fEg.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D0iUdW3P.js → McpTools-BW6xxSlU.js} +2 -2
- package/src/assets/web-panel/assets/{Memory--gwI7w4Y.js → Memory-mPqlpSkq.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-JmFBi24T.js → MobileBridge-Cb9N_DQI.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-BK01cQRx.js → MobileProjects-BNs8tah_.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-u7avzyoO.js → Mtc-CaxBt8tn.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-gT3z5fo_.js → MtcAudit-DRPQbp3t.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-BIqZTz5n.js → Multisig-CRm-d8bK.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DdhVfXYq.js → NLProgramming-BCCk3LPs.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CBlyUmY3.js → Notes-Cv8SN8gN.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-DSyI26N3.js → NotificationSettings-CggIvTh5.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-B7IEuNz4.js → OrderTableRenderer-BW3_bV12.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-ozhi4kGS.js → Organization-BuRE-VCO.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CBy57RVM.js → Overflow-DBrm1gju.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CA-ZRv77.js → P2P-CWVnDYgS.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-DOHc9zm8.js → PdhVaultBrowser-8RKUIpte.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-DiP632GP.js → Permissions-tDUAxogW.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-AnU8O-SS.js → PersonalDataHub-Dv2-CFis.js} +5 -5
- package/src/assets/web-panel/assets/{Pipeline-DDibOUaP.js → Pipeline-DM6kvFni.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-D421t36T.js → Privacy-DzK-yL0d.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DR879ZPJ.js → ProjectInit-B5UrNpkj.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-Ded8o6aY.js → ProjectSettings-CxJD43oW.js} +2 -2
- package/src/assets/web-panel/assets/Projects-Z2Orm8Ma.js +1 -0
- package/src/assets/web-panel/assets/{Providers-Boivuu-E.js → Providers-CYl4jRYK.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-xKD_ntKp.js → QrScannerModal-DeH2T0fH.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-D3y_A6FV.js → QuickAsk-Bw9k_kTk.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-BjKM-YC2.js → Recommend-Bl_jGwoi.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-d6u01Kq0.js +5 -0
- package/src/assets/web-panel/assets/{Reputation-Ci2GjH5p.js → Reputation-Ds_Dfr-k.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B0-T2BSh.js → Row-BpQUQTNj.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Bet-ur-r.js → RssFeed-BatpZBGt.js} +2 -2
- package/src/assets/web-panel/assets/{Search-DWtRAS2R.js → Search-D7hOObzi.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BDfu2Qn6.js → Security-dXQ4FKNt.js} +4 -4
- package/src/assets/web-panel/assets/{Services-CvJmvSMM.js → Services-D8wInGPa.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-CEbrKpdC.js → Skeleton-DXQCoYpM.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CjVeiETf.js → Skills-D3oLRIVn.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-Dibypxum.js → Sla-BmP4-LnJ.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-XflYXp7a.js → SpeechSettings-DLj-xzro.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-ro1Ivx1E.js → SyncSettings-8KKaaRMr.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-CHq1qFbo.js → Tasks-GyK42I7C.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CRnUZxtZ.js → Templates-n1y_8UBG.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-jUX72JCA.js → Tenant-B39nBph8.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BBhAs7pt.js → Terminal-DCQoOKXe.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CD5M3RGp.js → TimelineRenderer-BkhbDfXk.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-sah3v0gu.js → Tokens-BeHIIU8D.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BNT3Uz3s.js → Trigger-BHvWXVOb.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BytMGPMo.js → Trust-y2WaOSto.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BkYVMRQs.js → UkeySign-DBFeJBzt.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-psvb5Q7Y.js → VideoEditing-BEOWitC-.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-PFQTW0MH.js → Wallet-BITedTe6.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DgO-OwRT.js → WebAuthn-DMwlJVin.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DL2l9VNl.js → WorkflowEditor-CKfGFEu1.js} +1 -1
- package/src/assets/web-panel/assets/{chat-C84akEzN.js → chat-DR6gnBGF.js} +1 -1
- package/src/assets/web-panel/assets/{colors-DzTWuQKP.js → colors-C_Hw5NCn.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CR9LJQFv.js → compact-item-B8IPwsEu.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CTTov5Kn.js → createContext-D-u4JVnJ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-Cq6lSd_q.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DALj523o.js → hasIn-CZubdS4D.js} +1 -1
- package/src/assets/web-panel/assets/{index-Be6Os5S8.js → index-11ASqx-A.js} +1 -1
- package/src/assets/web-panel/assets/index-BQ3213fz.js +1 -0
- package/src/assets/web-panel/assets/{index-CXaCRXN4.js → index-BXS6wYJZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-7ux_XohH.js → index-Bi21Etg4.js} +1 -1
- package/src/assets/web-panel/assets/{index-CXTEpfGe.js → index-Bizm87cX.js} +1 -1
- package/src/assets/web-panel/assets/{index-DH6OG-ci.js → index-Bv2132PO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CZhhFQi9.js → index-BzoFanEQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6QOJbSn.js → index-C25rOz7O.js} +1 -1
- package/src/assets/web-panel/assets/{index-CunGDV0g.js → index-C2VmtNaQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmELknPe.js → index-Cbm8F0M2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZER6Lj3.js → index-CfniPP5p.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2qpjU-D.js → index-Cg2bCeol.js} +1 -1
- package/src/assets/web-panel/assets/{index-QW0EXx14.js → index-CiXkhOkY.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5eeMl_2.js → index-Cl5RpreD.js} +1 -1
- package/src/assets/web-panel/assets/{index-s4CFyn-n.js → index-CqmLw8cs.js} +1 -1
- package/src/assets/web-panel/assets/{index-eJcFleIV.js → index-Ctje8Vl3.js} +1 -1
- package/src/assets/web-panel/assets/{index-3-XJoYgX.js → index-D1w96gg9.js} +1 -1
- package/src/assets/web-panel/assets/{index-_8vetPbU.js → index-D47jt8ET.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cef0n4k_.js → index-DJ6pG79m.js} +1 -1
- package/src/assets/web-panel/assets/{index-AXnERsyV.js → index-DTosx2Wu.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtW6Zr_i.js → index-DY6MHo9D.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNmTDXmf.js → index-DcFjRT-K.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dah8M4l2.js → index-DfnzrUxU.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0MqUnKO.js → index-DjRrqooR.js} +1 -1
- package/src/assets/web-panel/assets/{index-CqUgXYIQ.js → index-DmRcgiYT.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9ScRFxz.js → index-DpjHpRoq.js} +3 -3
- package/src/assets/web-panel/assets/{index-DjE_ew7U.js → index-Dt-w3xQ7.js} +1 -1
- package/src/assets/web-panel/assets/{index-W9axN_Ce.js → index-DuOU5apg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BDyt57zL.js → index-LvjAKDSl.js} +1 -1
- package/src/assets/web-panel/assets/{index-hzzTITR9.js → index-_nL79NT8.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNaQt49r.js → index-auyNYjLo.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBIPnoTj.js → index-cVqbas5C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnxjXIsK.js → index-ffe7bvqP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHRXKiU1.js → index-idZKyADQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_Bg0LdK.js → index-k1Xa5iw1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKNIzw0U.js → index-mYZJxGH6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DxdzSljS.js → index-rYBHSDcJ.js} +1 -1
- package/src/assets/web-panel/assets/index-vJagfET2.js +1 -0
- package/src/assets/web-panel/assets/{index-CjkPDSX4.js → index-xoNssgz_.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-JuW8mFj7.js → initDefaultProps-lcU_dT2r.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Cwb0HBve.js → motion-Dc7UNXZ1.js} +1 -1
- package/src/assets/web-panel/assets/{move-DHokzMgu.js → move-Cll8ZXaL.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-DX31y6QS.js → mtc-parser-CAb_yLIM.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BCIUYDA6.js → omit-DurYMyTe.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DMCj4EoZ.js → pickAttrs-BITQyOlw.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Dy5K-u1s.js → placementArrow-Dg1rH91i.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-DkGeZu54.js → responsiveObserve-D_EWIeac.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Dc5dyJdg.js → slide-lK4d3zsh.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DIUBjOaC.js → statusUtils-Bflq-Git.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DmyEOxm5.js → styleChecker-Ccp0I-wt.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-FRhakk1e.js → useFlexGapSupport-CYZbByYA.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-clXyV1WQ.js → useFs-B_x0nxFD.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-UF6DG41z.js → usePersonalDataHub-DW55rL85.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CoCt95G7.js → vnode-Ca6MH3W8.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-yVWt-ENm.js → zoom-NdHelOog.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-help-index.json +1 -1
- package/src/commands/orchestrate.js +61 -9
- package/src/data/changelog.json +34 -1
- package/src/gateways/ws/message-dispatcher.js +4 -0
- package/src/gateways/ws/session-protocol.js +89 -11
- package/src/gateways/ws/ws-agent-handler.js +65 -9
- package/src/gateways/ws/ws-approval-gate.js +181 -16
- package/src/gateways/ws/ws-server.js +16 -6
- package/src/generated/cc-agent-protocol.schema.json +148 -2
- package/src/lib/agent-team/task-lease.js +172 -3
- package/src/lib/agent-team/team-runner.js +60 -1
- package/src/lib/app-server/graph-runtime.js +326 -2
- package/src/lib/app-server/server.js +60 -0
- package/src/lib/approval-grant-ledger.js +69 -0
- package/src/lib/codex-app-server-adapter.js +11 -20
- package/src/lib/graph-kernel/compiler.js +66 -13
- package/src/lib/graph-kernel/cutover-authority-resolver.js +26 -0
- package/src/lib/graph-kernel/cutover-ledger.js +53 -11
- package/src/lib/graph-kernel/retirement-evidence.js +567 -0
- package/src/lib/graph-kernel/runtime.js +76 -14
- package/src/lib/interaction-adapter.js +61 -9
- package/src/lib/policy-decision-event.js +149 -0
- package/src/lib/process-execution-broker/platform-sandbox.js +15 -1
- package/src/lib/record-replay/index.js +1 -0
- package/src/lib/record-replay/playwright-ui-driver.js +327 -0
- package/src/lib/webhook-security.js +484 -36
- package/src/runtime/coding-agent-events.cjs +13 -11
- package/src/runtime/headless-runner.js +17 -1
- package/src/runtime/headless-stream.js +66 -3
- package/src/runtime/session-slash-commands.js +57 -6
- package/src/assets/web-panel/assets/MarkdownRenderer-CZsTBEQE.js +0 -1
- package/src/assets/web-panel/assets/Projects-BHPxiucB.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-CgqKMXG4.js +0 -5
- package/src/assets/web-panel/assets/devWarning-BOhvmlpc.js +0 -1
- package/src/assets/web-panel/assets/index-DDyMJexx.js +0 -1
- package/src/assets/web-panel/assets/index-btiojFYn.js +0 -1
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createDecipheriv,
|
|
3
|
+
createHash,
|
|
4
|
+
createHmac,
|
|
5
|
+
timingSafeEqual,
|
|
6
|
+
} from "node:crypto";
|
|
7
|
+
|
|
8
|
+
const VENDOR_CHANNELS = new Set(["dingtalk", "feishu", "wecom"]);
|
|
2
9
|
|
|
3
10
|
function webhookError(code, message, statusCode, details = {}) {
|
|
4
11
|
const error = new Error(message);
|
|
@@ -14,6 +21,87 @@ function header(headers, name) {
|
|
|
14
21
|
return Array.isArray(value) ? value[0] : value == null ? "" : String(value);
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
function queryValue(query, name) {
|
|
25
|
+
const value = query?.[name];
|
|
26
|
+
return Array.isArray(value) ? value[0] : value == null ? "" : String(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function safeTextEqual(expected, supplied) {
|
|
30
|
+
const expectedBytes = Buffer.from(String(expected), "utf8");
|
|
31
|
+
const suppliedBytes = Buffer.from(String(supplied), "utf8");
|
|
32
|
+
return (
|
|
33
|
+
expectedBytes.length === suppliedBytes.length &&
|
|
34
|
+
timingSafeEqual(expectedBytes, suppliedBytes)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function requireCredential(value, channel, name) {
|
|
39
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
40
|
+
throw webhookError(
|
|
41
|
+
"CC_WEBHOOK_VENDOR_CREDENTIAL_REQUIRED",
|
|
42
|
+
`${channel} vendor authentication requires ${name}`,
|
|
43
|
+
503,
|
|
44
|
+
{ channel, credential: name },
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseTimestamp(timestampText) {
|
|
51
|
+
const value = Number(timestampText);
|
|
52
|
+
if (!Number.isFinite(value) || value <= 0) return Number.NaN;
|
|
53
|
+
return value < 100_000_000_000 ? value * 1000 : value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertFreshTimestamp(timestampText, now, maxClockSkewMs) {
|
|
57
|
+
const timestamp = parseTimestamp(timestampText);
|
|
58
|
+
if (
|
|
59
|
+
!Number.isFinite(timestamp) ||
|
|
60
|
+
Math.abs(now - timestamp) > maxClockSkewMs
|
|
61
|
+
) {
|
|
62
|
+
throw webhookError(
|
|
63
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
64
|
+
"webhook timestamp is invalid or stale",
|
|
65
|
+
401,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function fallbackDeliveryId(channel, signature, body) {
|
|
71
|
+
return `${channel}-${createHash("sha256")
|
|
72
|
+
.update(signature)
|
|
73
|
+
.update("\0")
|
|
74
|
+
.update(body)
|
|
75
|
+
.digest("hex")
|
|
76
|
+
.slice(0, 40)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeDeliveryId(value, channel, signature, body) {
|
|
80
|
+
const deliveryId = value == null ? "" : String(value);
|
|
81
|
+
if (deliveryId && deliveryId.length <= 256) return deliveryId;
|
|
82
|
+
return fallbackDeliveryId(channel, signature, body);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function jsonObject(body) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(body);
|
|
88
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
89
|
+
? parsed
|
|
90
|
+
: {};
|
|
91
|
+
} catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function xmlTag(xml, tag) {
|
|
97
|
+
const pattern = new RegExp(
|
|
98
|
+
`<${tag}>(?:<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|([\\s\\S]*?))<\\/${tag}>`,
|
|
99
|
+
"i",
|
|
100
|
+
);
|
|
101
|
+
const match = String(xml).match(pattern);
|
|
102
|
+
return match ? (match[1] ?? match[2] ?? "").trim() : "";
|
|
103
|
+
}
|
|
104
|
+
|
|
17
105
|
export function signWebhookBody(secret, timestamp, body) {
|
|
18
106
|
return `sha256=${createHmac("sha256", secret)
|
|
19
107
|
.update(String(timestamp))
|
|
@@ -22,22 +110,179 @@ export function signWebhookBody(secret, timestamp, body) {
|
|
|
22
110
|
.digest("hex")}`;
|
|
23
111
|
}
|
|
24
112
|
|
|
113
|
+
export function signDingTalkWebhook(secret, timestamp) {
|
|
114
|
+
return createHmac("sha256", secret)
|
|
115
|
+
.update(`${timestamp}\n${secret}`)
|
|
116
|
+
.digest("base64");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function signFeishuWebhook(encryptKey, timestamp, nonce, body) {
|
|
120
|
+
return createHash("sha256")
|
|
121
|
+
.update(`${timestamp}${nonce}${encryptKey}${body}`)
|
|
122
|
+
.digest("hex");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function signWeComWebhook(token, timestamp, nonce, encrypted) {
|
|
126
|
+
return createHash("sha1")
|
|
127
|
+
.update([token, timestamp, nonce, encrypted].map(String).sort().join(""))
|
|
128
|
+
.digest("hex");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function decryptFeishuWebhook(encryptKey, encrypted) {
|
|
132
|
+
let encryptedBytes;
|
|
133
|
+
try {
|
|
134
|
+
encryptedBytes = Buffer.from(encrypted, "base64");
|
|
135
|
+
} catch {
|
|
136
|
+
encryptedBytes = Buffer.alloc(0);
|
|
137
|
+
}
|
|
138
|
+
if (encryptedBytes.length < 32 || encryptedBytes.length % 16 !== 0) {
|
|
139
|
+
throw webhookError(
|
|
140
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
141
|
+
"Feishu encrypted webhook body is invalid",
|
|
142
|
+
401,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const key = createHash("sha256").update(encryptKey).digest();
|
|
147
|
+
const decipher = createDecipheriv(
|
|
148
|
+
"aes-256-cbc",
|
|
149
|
+
key,
|
|
150
|
+
encryptedBytes.subarray(0, 16),
|
|
151
|
+
);
|
|
152
|
+
return Buffer.concat([
|
|
153
|
+
decipher.update(encryptedBytes.subarray(16)),
|
|
154
|
+
decipher.final(),
|
|
155
|
+
]).toString("utf8");
|
|
156
|
+
} catch {
|
|
157
|
+
throw webhookError(
|
|
158
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
159
|
+
"Feishu encrypted webhook body could not be decrypted",
|
|
160
|
+
401,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function decodeWeComAesKey(encodingAesKey) {
|
|
166
|
+
if (!/^[A-Za-z0-9+/]{43}={0,1}$/u.test(encodingAesKey)) {
|
|
167
|
+
throw webhookError(
|
|
168
|
+
"CC_WEBHOOK_VENDOR_CREDENTIAL_REQUIRED",
|
|
169
|
+
"WeCom EncodingAESKey must contain 43 base64 characters",
|
|
170
|
+
503,
|
|
171
|
+
{ channel: "wecom", credential: "encodingAesKey" },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
const key = Buffer.from(`${encodingAesKey.replace(/=+$/u, "")}=`, "base64");
|
|
175
|
+
if (key.length !== 32) {
|
|
176
|
+
throw webhookError(
|
|
177
|
+
"CC_WEBHOOK_VENDOR_CREDENTIAL_REQUIRED",
|
|
178
|
+
"WeCom EncodingAESKey must decode to 32 bytes",
|
|
179
|
+
503,
|
|
180
|
+
{ channel: "wecom", credential: "encodingAesKey" },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return key;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function decryptWeComWebhook(encodingAesKey, encrypted, receiveId) {
|
|
187
|
+
const key = decodeWeComAesKey(encodingAesKey);
|
|
188
|
+
let ciphertext;
|
|
189
|
+
try {
|
|
190
|
+
ciphertext = Buffer.from(encrypted, "base64");
|
|
191
|
+
} catch {
|
|
192
|
+
ciphertext = Buffer.alloc(0);
|
|
193
|
+
}
|
|
194
|
+
if (ciphertext.length === 0 || ciphertext.length % 16 !== 0) {
|
|
195
|
+
throw webhookError(
|
|
196
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
197
|
+
"WeCom encrypted webhook body is invalid",
|
|
198
|
+
401,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let padded;
|
|
203
|
+
try {
|
|
204
|
+
const decipher = createDecipheriv("aes-256-cbc", key, key.subarray(0, 16));
|
|
205
|
+
decipher.setAutoPadding(false);
|
|
206
|
+
padded = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
207
|
+
} catch {
|
|
208
|
+
throw webhookError(
|
|
209
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
210
|
+
"WeCom encrypted webhook body could not be decrypted",
|
|
211
|
+
401,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const padding = padded.at(-1) || 0;
|
|
216
|
+
if (
|
|
217
|
+
padding < 1 ||
|
|
218
|
+
padding > 32 ||
|
|
219
|
+
padded.length <= padding ||
|
|
220
|
+
!padded.subarray(padded.length - padding).every((byte) => byte === padding)
|
|
221
|
+
) {
|
|
222
|
+
throw webhookError(
|
|
223
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
224
|
+
"WeCom encrypted webhook padding is invalid",
|
|
225
|
+
401,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const plaintext = padded.subarray(0, padded.length - padding);
|
|
229
|
+
if (plaintext.length < 20) {
|
|
230
|
+
throw webhookError(
|
|
231
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
232
|
+
"WeCom encrypted webhook envelope is truncated",
|
|
233
|
+
401,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const messageLength = plaintext.readUInt32BE(16);
|
|
237
|
+
const messageEnd = 20 + messageLength;
|
|
238
|
+
if (messageLength <= 0 || messageEnd > plaintext.length) {
|
|
239
|
+
throw webhookError(
|
|
240
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
241
|
+
"WeCom encrypted webhook message length is invalid",
|
|
242
|
+
401,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const actualReceiveId = plaintext.subarray(messageEnd).toString("utf8");
|
|
246
|
+
if (receiveId && !safeTextEqual(receiveId, actualReceiveId)) {
|
|
247
|
+
throw webhookError(
|
|
248
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
249
|
+
"WeCom webhook receiver identity does not match",
|
|
250
|
+
401,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return plaintext.subarray(20, messageEnd).toString("utf8");
|
|
254
|
+
}
|
|
255
|
+
|
|
25
256
|
export class WebhookSecurityGate {
|
|
26
257
|
constructor({
|
|
27
258
|
secret,
|
|
259
|
+
authMode = "cc",
|
|
260
|
+
vendorCredentials = {},
|
|
28
261
|
now = Date.now,
|
|
29
262
|
maxClockSkewMs = 5 * 60_000,
|
|
30
263
|
maxRequestsPerMinute = 30,
|
|
31
264
|
maxReplayEntries = 10_000,
|
|
32
265
|
} = {}) {
|
|
33
|
-
if (
|
|
266
|
+
if (!new Set(["cc", "vendor"]).has(authMode)) {
|
|
267
|
+
throw webhookError(
|
|
268
|
+
"CC_WEBHOOK_AUTH_MODE_INVALID",
|
|
269
|
+
"webhook authentication mode must be cc or vendor",
|
|
270
|
+
503,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
if (
|
|
274
|
+
authMode === "cc" &&
|
|
275
|
+
(typeof secret !== "string" || secret.length < 16)
|
|
276
|
+
) {
|
|
34
277
|
throw webhookError(
|
|
35
278
|
"CC_WEBHOOK_SECRET_REQUIRED",
|
|
36
279
|
"webhook mode requires an authentication secret of at least 16 characters",
|
|
37
280
|
503,
|
|
38
281
|
);
|
|
39
282
|
}
|
|
40
|
-
this.secret = secret;
|
|
283
|
+
this.secret = secret || "";
|
|
284
|
+
this.authMode = authMode;
|
|
285
|
+
this.vendorCredentials = Object.freeze({ ...vendorCredentials });
|
|
41
286
|
this.now = now;
|
|
42
287
|
this.maxClockSkewMs = Math.max(1_000, Number(maxClockSkewMs) || 300_000);
|
|
43
288
|
this.maxRequestsPerMinute = Math.max(1, Number(maxRequestsPerMinute) || 30);
|
|
@@ -46,52 +291,255 @@ export class WebhookSecurityGate {
|
|
|
46
291
|
this.rates = new Map();
|
|
47
292
|
}
|
|
48
293
|
|
|
49
|
-
verify({ channel, headers, body, remoteAddress = "unknown" }) {
|
|
294
|
+
verify({ channel, headers, body, query = {}, remoteAddress = "unknown" }) {
|
|
50
295
|
const now = this.now();
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
296
|
+
this._checkRate(channel, remoteAddress, now);
|
|
297
|
+
const authenticated =
|
|
298
|
+
this.authMode === "vendor"
|
|
299
|
+
? this._verifyVendor({ channel, headers, body, query, now })
|
|
300
|
+
: this._verifyCc({ channel, headers, body, now });
|
|
301
|
+
this._claimDelivery(channel, authenticated.deliveryId, now);
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
deliveryId: authenticated.deliveryId,
|
|
304
|
+
body: authenticated.body,
|
|
305
|
+
authMode: this.authMode,
|
|
306
|
+
dataPolicy: this._dataPolicy(channel, authenticated.deliveryId),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
verifyWeComUrl({ query = {}, remoteAddress = "unknown" }) {
|
|
311
|
+
if (this.authMode !== "vendor") {
|
|
312
|
+
throw webhookError(
|
|
313
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
314
|
+
"WeCom URL verification requires vendor authentication mode",
|
|
315
|
+
401,
|
|
316
|
+
);
|
|
57
317
|
}
|
|
58
|
-
|
|
318
|
+
const now = this.now();
|
|
319
|
+
this._checkRate("wecom", remoteAddress, now);
|
|
320
|
+
const token = requireCredential(
|
|
321
|
+
this.vendorCredentials.wecomToken,
|
|
322
|
+
"wecom",
|
|
323
|
+
"token",
|
|
324
|
+
);
|
|
325
|
+
const encodingAesKey = requireCredential(
|
|
326
|
+
this.vendorCredentials.wecomEncodingAesKey,
|
|
327
|
+
"wecom",
|
|
328
|
+
"encodingAesKey",
|
|
329
|
+
);
|
|
330
|
+
const receiveId = requireCredential(
|
|
331
|
+
this.vendorCredentials.wecomReceiveId,
|
|
332
|
+
"wecom",
|
|
333
|
+
"receiveId",
|
|
334
|
+
);
|
|
335
|
+
const timestamp = queryValue(query, "timestamp");
|
|
336
|
+
const nonce = queryValue(query, "nonce");
|
|
337
|
+
const encryptedEcho = queryValue(query, "echostr");
|
|
338
|
+
const supplied = queryValue(query, "msg_signature");
|
|
339
|
+
assertFreshTimestamp(timestamp, now, this.maxClockSkewMs);
|
|
340
|
+
if (!nonce || nonce.length > 256 || !encryptedEcho || !supplied) {
|
|
59
341
|
throw webhookError(
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
|
|
342
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
343
|
+
"WeCom URL verification parameters are invalid",
|
|
344
|
+
401,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const expected = signWeComWebhook(token, timestamp, nonce, encryptedEcho);
|
|
348
|
+
if (!safeTextEqual(expected, supplied)) {
|
|
349
|
+
throw webhookError(
|
|
350
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
351
|
+
"WeCom URL verification signature is invalid",
|
|
352
|
+
401,
|
|
64
353
|
);
|
|
65
354
|
}
|
|
355
|
+
return Object.freeze({
|
|
356
|
+
echo: decryptWeComWebhook(encodingAesKey, encryptedEcho, receiveId),
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_verifyCc({ headers, body, now }) {
|
|
66
361
|
const timestampText = header(headers, "x-cc-webhook-timestamp");
|
|
67
362
|
const deliveryId = header(headers, "x-cc-webhook-delivery");
|
|
68
363
|
const supplied = header(headers, "x-cc-webhook-signature");
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
!Number.isFinite(timestamp) ||
|
|
72
|
-
Math.abs(now - timestamp) > this.maxClockSkewMs ||
|
|
73
|
-
!deliveryId ||
|
|
74
|
-
deliveryId.length > 256
|
|
75
|
-
) {
|
|
364
|
+
assertFreshTimestamp(timestampText, now, this.maxClockSkewMs);
|
|
365
|
+
if (!deliveryId || deliveryId.length > 256) {
|
|
76
366
|
throw webhookError(
|
|
77
367
|
"CC_WEBHOOK_AUTH_INVALID",
|
|
78
|
-
"webhook
|
|
368
|
+
"webhook delivery identity is invalid",
|
|
79
369
|
401,
|
|
80
370
|
);
|
|
81
371
|
}
|
|
82
372
|
const expected = signWebhookBody(this.secret, timestampText, body);
|
|
83
|
-
|
|
84
|
-
const suppliedBytes = Buffer.from(supplied, "utf8");
|
|
85
|
-
if (
|
|
86
|
-
expectedBytes.length !== suppliedBytes.length ||
|
|
87
|
-
!timingSafeEqual(expectedBytes, suppliedBytes)
|
|
88
|
-
) {
|
|
373
|
+
if (!safeTextEqual(expected, supplied)) {
|
|
89
374
|
throw webhookError(
|
|
90
375
|
"CC_WEBHOOK_AUTH_INVALID",
|
|
91
376
|
"webhook signature is invalid",
|
|
92
377
|
401,
|
|
93
378
|
);
|
|
94
379
|
}
|
|
380
|
+
return { deliveryId, body };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
_verifyVendor({ channel, headers, body, query, now }) {
|
|
384
|
+
if (!VENDOR_CHANNELS.has(channel)) {
|
|
385
|
+
throw webhookError(
|
|
386
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
387
|
+
"unsupported vendor webhook channel",
|
|
388
|
+
401,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (channel === "dingtalk") {
|
|
392
|
+
return this._verifyDingTalk({ headers, body, now });
|
|
393
|
+
}
|
|
394
|
+
if (channel === "feishu") {
|
|
395
|
+
return this._verifyFeishu({ headers, body, now });
|
|
396
|
+
}
|
|
397
|
+
return this._verifyWeCom({ body, query, now });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_verifyDingTalk({ headers, body, now }) {
|
|
401
|
+
const secret = requireCredential(
|
|
402
|
+
this.vendorCredentials.dingtalkSecret,
|
|
403
|
+
"dingtalk",
|
|
404
|
+
"secret",
|
|
405
|
+
);
|
|
406
|
+
const timestamp = header(headers, "timestamp");
|
|
407
|
+
const supplied = header(headers, "sign");
|
|
408
|
+
assertFreshTimestamp(timestamp, now, this.maxClockSkewMs);
|
|
409
|
+
const expected = signDingTalkWebhook(secret, timestamp);
|
|
410
|
+
let decodedSupplied = supplied;
|
|
411
|
+
try {
|
|
412
|
+
decodedSupplied = decodeURIComponent(supplied);
|
|
413
|
+
} catch {
|
|
414
|
+
// A raw Base64 header is the normal form.
|
|
415
|
+
}
|
|
416
|
+
if (!safeTextEqual(expected, decodedSupplied)) {
|
|
417
|
+
throw webhookError(
|
|
418
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
419
|
+
"DingTalk webhook signature is invalid",
|
|
420
|
+
401,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const parsed = jsonObject(body);
|
|
424
|
+
return {
|
|
425
|
+
deliveryId: normalizeDeliveryId(parsed.msgId, "dingtalk", supplied, body),
|
|
426
|
+
body,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
_verifyFeishu({ headers, body, now }) {
|
|
431
|
+
const encryptKey = requireCredential(
|
|
432
|
+
this.vendorCredentials.feishuEncryptKey,
|
|
433
|
+
"feishu",
|
|
434
|
+
"encryptKey",
|
|
435
|
+
);
|
|
436
|
+
const timestamp = header(headers, "x-lark-request-timestamp");
|
|
437
|
+
const nonce = header(headers, "x-lark-request-nonce");
|
|
438
|
+
const supplied = header(headers, "x-lark-signature");
|
|
439
|
+
assertFreshTimestamp(timestamp, now, this.maxClockSkewMs);
|
|
440
|
+
if (!nonce || nonce.length > 256) {
|
|
441
|
+
throw webhookError(
|
|
442
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
443
|
+
"Feishu webhook nonce is invalid",
|
|
444
|
+
401,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const expected = signFeishuWebhook(encryptKey, timestamp, nonce, body);
|
|
448
|
+
if (!safeTextEqual(expected, supplied)) {
|
|
449
|
+
throw webhookError(
|
|
450
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
451
|
+
"Feishu webhook signature is invalid",
|
|
452
|
+
401,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const envelope = jsonObject(body);
|
|
457
|
+
const authenticatedBody = envelope.encrypt
|
|
458
|
+
? decryptFeishuWebhook(encryptKey, envelope.encrypt)
|
|
459
|
+
: body;
|
|
460
|
+
const parsed = jsonObject(authenticatedBody);
|
|
461
|
+
return {
|
|
462
|
+
deliveryId: normalizeDeliveryId(
|
|
463
|
+
parsed.header?.event_id ?? parsed.uuid,
|
|
464
|
+
"feishu",
|
|
465
|
+
supplied,
|
|
466
|
+
body,
|
|
467
|
+
),
|
|
468
|
+
body: authenticatedBody,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
_verifyWeCom({ body, query, now }) {
|
|
473
|
+
const token = requireCredential(
|
|
474
|
+
this.vendorCredentials.wecomToken,
|
|
475
|
+
"wecom",
|
|
476
|
+
"token",
|
|
477
|
+
);
|
|
478
|
+
const encodingAesKey = requireCredential(
|
|
479
|
+
this.vendorCredentials.wecomEncodingAesKey,
|
|
480
|
+
"wecom",
|
|
481
|
+
"encodingAesKey",
|
|
482
|
+
);
|
|
483
|
+
const receiveId = requireCredential(
|
|
484
|
+
this.vendorCredentials.wecomReceiveId,
|
|
485
|
+
"wecom",
|
|
486
|
+
"receiveId",
|
|
487
|
+
);
|
|
488
|
+
const timestamp = queryValue(query, "timestamp");
|
|
489
|
+
const nonce = queryValue(query, "nonce");
|
|
490
|
+
const supplied = queryValue(query, "msg_signature");
|
|
491
|
+
const encrypted = xmlTag(body, "Encrypt");
|
|
492
|
+
assertFreshTimestamp(timestamp, now, this.maxClockSkewMs);
|
|
493
|
+
if (!nonce || nonce.length > 256 || !encrypted || !supplied) {
|
|
494
|
+
throw webhookError(
|
|
495
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
496
|
+
"WeCom webhook authentication parameters are invalid",
|
|
497
|
+
401,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const expected = signWeComWebhook(token, timestamp, nonce, encrypted);
|
|
501
|
+
if (!safeTextEqual(expected, supplied)) {
|
|
502
|
+
throw webhookError(
|
|
503
|
+
"CC_WEBHOOK_AUTH_INVALID",
|
|
504
|
+
"WeCom webhook signature is invalid",
|
|
505
|
+
401,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const authenticatedBody = decryptWeComWebhook(
|
|
509
|
+
encodingAesKey,
|
|
510
|
+
encrypted,
|
|
511
|
+
receiveId,
|
|
512
|
+
);
|
|
513
|
+
return {
|
|
514
|
+
deliveryId: normalizeDeliveryId(
|
|
515
|
+
xmlTag(authenticatedBody, "MsgId"),
|
|
516
|
+
"wecom",
|
|
517
|
+
supplied,
|
|
518
|
+
body,
|
|
519
|
+
),
|
|
520
|
+
body: authenticatedBody,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
_checkRate(channel, remoteAddress, now) {
|
|
525
|
+
const window = Math.floor(now / 60_000);
|
|
526
|
+
const rateKey = `${channel}\0${remoteAddress}\0${window}`;
|
|
527
|
+
const requests = (this.rates.get(rateKey) || 0) + 1;
|
|
528
|
+
this.rates.set(rateKey, requests);
|
|
529
|
+
for (const key of this.rates.keys()) {
|
|
530
|
+
if (!key.endsWith(`\0${window}`)) this.rates.delete(key);
|
|
531
|
+
}
|
|
532
|
+
if (requests > this.maxRequestsPerMinute) {
|
|
533
|
+
throw webhookError(
|
|
534
|
+
"CC_WEBHOOK_RATE_LIMITED",
|
|
535
|
+
"webhook rate limit exceeded",
|
|
536
|
+
429,
|
|
537
|
+
{ retryAfterSeconds: 60 - Math.floor((now % 60_000) / 1000) },
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
_claimDelivery(channel, deliveryId, now) {
|
|
95
543
|
for (const [key, expiresAt] of this.replays) {
|
|
96
544
|
if (expiresAt <= now) this.replays.delete(key);
|
|
97
545
|
}
|
|
@@ -111,14 +559,14 @@ export class WebhookSecurityGate {
|
|
|
111
559
|
);
|
|
112
560
|
}
|
|
113
561
|
this.replays.set(replayKey, now + this.maxClockSkewMs);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
_dataPolicy(channel, deliveryId) {
|
|
114
565
|
return Object.freeze({
|
|
115
|
-
deliveryId
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
sensitivity: "internal",
|
|
120
|
-
allowedSinks: Object.freeze(["agent:*", "artifact:local"]),
|
|
121
|
-
}),
|
|
566
|
+
origin: `webhook:${channel}:${deliveryId}`,
|
|
567
|
+
trust: "authenticated_user",
|
|
568
|
+
sensitivity: "internal",
|
|
569
|
+
allowedSinks: Object.freeze(["agent:*", "artifact:local"]),
|
|
122
570
|
});
|
|
123
571
|
}
|
|
124
572
|
}
|
|
@@ -55,6 +55,10 @@ const CODING_AGENT_EVENT_TYPES = Object.freeze({
|
|
|
55
55
|
APPROVAL_DENIED: "approval.denied",
|
|
56
56
|
APPROVAL_EXPIRED: "approval.expired",
|
|
57
57
|
|
|
58
|
+
// Policy decisions from both Hook and tool admission surfaces use one
|
|
59
|
+
// bounded, digest-bearing payload so hosts never need to parse tool errors.
|
|
60
|
+
POLICY_DECISION: "policy.decision",
|
|
61
|
+
|
|
58
62
|
// Context compaction
|
|
59
63
|
CONTEXT_COMPACTION_STARTED: "context.compaction.started",
|
|
60
64
|
CONTEXT_COMPACTION_COMPLETED: "context.compaction.completed",
|
|
@@ -173,6 +177,7 @@ const LEGACY_TO_UNIFIED_TYPE = Object.freeze({
|
|
|
173
177
|
"approval-granted": CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED,
|
|
174
178
|
"approval-denied": CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED,
|
|
175
179
|
"approval-expired": CODING_AGENT_EVENT_TYPES.APPROVAL_EXPIRED,
|
|
180
|
+
"policy-decision": CODING_AGENT_EVENT_TYPES.POLICY_DECISION,
|
|
176
181
|
"high-risk-confirmation-required":
|
|
177
182
|
CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMATION_REQUIRED,
|
|
178
183
|
"high-risk-confirmed": CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMED,
|
|
@@ -261,6 +266,9 @@ function projectAgentStreamMessage(message) {
|
|
|
261
266
|
? CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED
|
|
262
267
|
: CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED;
|
|
263
268
|
break;
|
|
269
|
+
case "policy_decision":
|
|
270
|
+
type = CODING_AGENT_EVENT_TYPES.POLICY_DECISION;
|
|
271
|
+
break;
|
|
264
272
|
case "plan_update":
|
|
265
273
|
type = CODING_AGENT_EVENT_TYPES.PLAN_UPDATED;
|
|
266
274
|
break;
|
|
@@ -344,19 +352,14 @@ const defaultSequenceTracker = new CodingAgentSequenceTracker();
|
|
|
344
352
|
*/
|
|
345
353
|
function createCodingAgentEvent(type, payload = {}, context = {}) {
|
|
346
354
|
if (payload && typeof payload !== "object") {
|
|
347
|
-
throw new TypeError(
|
|
348
|
-
"createCodingAgentEvent: payload must be an object",
|
|
349
|
-
);
|
|
355
|
+
throw new TypeError("createCodingAgentEvent: payload must be an object");
|
|
350
356
|
}
|
|
351
357
|
|
|
352
358
|
const normalizedType = mapLegacyType(type);
|
|
353
359
|
const tracker = context.tracker || defaultSequenceTracker;
|
|
354
|
-
const sessionId =
|
|
355
|
-
context.sessionId || (payload && payload.sessionId) || null;
|
|
360
|
+
const sessionId = context.sessionId || (payload && payload.sessionId) || null;
|
|
356
361
|
const requestId =
|
|
357
|
-
context.requestId ||
|
|
358
|
-
(payload && (payload.requestId || payload.id)) ||
|
|
359
|
-
null;
|
|
362
|
+
context.requestId || (payload && (payload.requestId || payload.id)) || null;
|
|
360
363
|
|
|
361
364
|
let sequence;
|
|
362
365
|
if (Number.isInteger(context.sequence)) {
|
|
@@ -404,9 +407,7 @@ function createCodingAgentEvent(type, payload = {}, context = {}) {
|
|
|
404
407
|
*/
|
|
405
408
|
function wrapLegacyMessage(message, context = {}) {
|
|
406
409
|
if (!message || typeof message !== "object") {
|
|
407
|
-
throw new TypeError(
|
|
408
|
-
"wrapLegacyMessage: message must be a non-null object",
|
|
409
|
-
);
|
|
410
|
+
throw new TypeError("wrapLegacyMessage: message must be a non-null object");
|
|
410
411
|
}
|
|
411
412
|
|
|
412
413
|
const { type, ...payload } = message;
|
|
@@ -475,6 +476,7 @@ const CodingAgentEventType = Object.freeze({
|
|
|
475
476
|
APPROVAL_REQUESTED: CODING_AGENT_EVENT_TYPES.APPROVAL_REQUESTED,
|
|
476
477
|
APPROVAL_GRANTED: CODING_AGENT_EVENT_TYPES.APPROVAL_GRANTED,
|
|
477
478
|
APPROVAL_DENIED: CODING_AGENT_EVENT_TYPES.APPROVAL_DENIED,
|
|
479
|
+
POLICY_DECISION: CODING_AGENT_EVENT_TYPES.POLICY_DECISION,
|
|
478
480
|
HIGH_RISK_CONFIRMATION_REQUIRED:
|
|
479
481
|
CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMATION_REQUIRED,
|
|
480
482
|
HIGH_RISK_CONFIRMED: CODING_AGENT_EVENT_TYPES.HIGH_RISK_CONFIRMED,
|
|
@@ -89,6 +89,10 @@ import {
|
|
|
89
89
|
executeHooksV2Event,
|
|
90
90
|
resolvePromptExpansion,
|
|
91
91
|
} from "../lib/hooks-v2-producers.js";
|
|
92
|
+
import {
|
|
93
|
+
projectHookPolicyDecision,
|
|
94
|
+
projectToolPolicyDecision,
|
|
95
|
+
} from "../lib/policy-decision-event.js";
|
|
92
96
|
import { runWithHostHooksV2Workspace } from "../lib/hooks-v2-workspace-context.js";
|
|
93
97
|
import { CLISkillLoader } from "../lib/skill-loader.js";
|
|
94
98
|
import { expandFileRefsAsync } from "./file-ref-expander.js";
|
|
@@ -731,7 +735,11 @@ async function runAgentHeadlessInWorkspace(
|
|
|
731
735
|
if (includeHookEvents) {
|
|
732
736
|
const subscribeHookEvents =
|
|
733
737
|
deps.addHooksV2EventObserver || addHooksV2EventObserver;
|
|
734
|
-
removeHookObserver = subscribeHookEvents(sessionId,
|
|
738
|
+
removeHookObserver = subscribeHookEvents(sessionId, (event) => {
|
|
739
|
+
queuePendingHookEvent(event);
|
|
740
|
+
const policyEvent = projectHookPolicyDecision(event);
|
|
741
|
+
if (policyEvent) queuePendingHookEvent(policyEvent);
|
|
742
|
+
});
|
|
735
743
|
}
|
|
736
744
|
|
|
737
745
|
const emitHeadlessError = (resultMsg) => {
|
|
@@ -3098,6 +3106,14 @@ async function runAgentHeadlessInWorkspace(
|
|
|
3098
3106
|
? { permission_decision: event.permission_decision }
|
|
3099
3107
|
: {}),
|
|
3100
3108
|
});
|
|
3109
|
+
if (includeHookEvents) {
|
|
3110
|
+
const policyEvent = projectToolPolicyDecision(event, {
|
|
3111
|
+
sessionId,
|
|
3112
|
+
turnId: event.turn_id,
|
|
3113
|
+
toolUseId: event.tool_use_id,
|
|
3114
|
+
});
|
|
3115
|
+
if (policyEvent) emitStream(policyEvent);
|
|
3116
|
+
}
|
|
3101
3117
|
const settledCall =
|
|
3102
3118
|
(event.tool_use_id
|
|
3103
3119
|
? activeToolCalls.get(event.tool_use_id)
|