@desplega.ai/agent-swarm 1.164.1 → 1.166.0
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/dist/acp-adapter-yzfxn8vj.js +23 -0
- package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
- package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
- package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
- package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
- package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
- package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
- package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
- package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
- package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
- package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
- package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
- package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
- package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
- package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
- package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
- package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
- package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
- package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
- package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
- package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
- package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
- package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
- package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
- package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
- package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
- package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
- package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
- package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
- package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
- package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
- package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
- package/dist/cli-fn697bnt.js +126 -0
- package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
- package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
- package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
- package/dist/cli-j2jbr8af.js +516 -0
- package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
- package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
- package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
- package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
- package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
- package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
- package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
- package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
- package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
- package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
- package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
- package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
- package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
- package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
- package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
- package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
- package/dist/cli-zp67qg5x.js +92 -0
- package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
- package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
- package/dist/cli.js +79 -26
- package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
- package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
- package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
- package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
- package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
- package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
- package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
- package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
- package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
- package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
- package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
- package/dist/grok-adapter-ar9cw9jg.js +29 -0
- package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
- package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
- package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
- package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
- package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
- package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
- package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
- package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
- package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
- package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
- package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
- package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
- package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
- package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
- package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
- package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
- package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
- package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
- package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
- package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
- package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
- package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
- package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
- package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
- package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +457 -31
- package/package.json +14 -6
- package/plugin/commands/create-pr.md +7 -1
- package/plugin/pi-skills/create-pr/SKILL.md +7 -1
- package/src/agentmail/app.ts +5 -4
- package/src/be/automation-preflight.ts +11 -9
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/connector-codes.ts +81 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/197_connector_codes.sql +23 -0
- package/src/be/migrations/198_permission_audit_guest.sql +33 -0
- package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/model-tier-resolution.ts +2 -1
- package/src/be/model-validation.ts +1 -1
- package/src/be/pricing-normalize.ts +3 -0
- package/src/be/rbac-audit.ts +2 -1
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/seed-pricing.ts +14 -0
- package/src/be/users.ts +7 -1
- package/src/be/workflow-replay.ts +77 -0
- package/src/cli.tsx +7 -0
- package/src/commands/provider-credentials.ts +46 -8
- package/src/commands/runner.ts +2 -10
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/github/app.ts +4 -4
- package/src/heartbeat/heartbeat.ts +20 -8
- package/src/hooks/pr-body-guard.ts +1 -1
- package/src/http/agents.ts +1 -0
- package/src/http/approval-requests.ts +1 -0
- package/src/http/apps.ts +3 -0
- package/src/http/auth.ts +4 -1
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/codex-oauth-keep-warm.ts +2 -2
- package/src/http/config-values.ts +59 -0
- package/src/http/config.ts +5 -0
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/extensions.ts +1 -0
- package/src/http/index.ts +23 -6
- package/src/http/integrations.ts +1 -38
- package/src/http/ip-rate-limit.ts +82 -0
- package/src/http/kv.ts +38 -4
- package/src/http/mcp-servers.ts +1 -0
- package/src/http/memory.ts +9 -0
- package/src/http/models-catalog.ts +2 -0
- package/src/http/operator-actor.ts +4 -0
- package/src/http/page-proxy.ts +22 -0
- package/src/http/pages.ts +4 -2
- package/src/http/realtime.ts +1 -1
- package/src/http/request-principal.ts +14 -1
- package/src/http/script-connections.ts +8 -0
- package/src/http/script-runs.ts +12 -4
- package/src/http/scripts.ts +15 -0
- package/src/http/session-cost-recompute.ts +3 -2
- package/src/http/tasks.ts +9 -0
- package/src/http/users.ts +226 -0
- package/src/http/workflows.ts +3 -2
- package/src/http.ts +1 -0
- package/src/integrations/kapso/client.ts +3 -1
- package/src/jira/app.ts +2 -2
- package/src/kv-view.ts +324 -0
- package/src/linear/app.ts +2 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/prompts/memories.ts +42 -1
- package/src/providers/acp-adapter.ts +85 -24
- package/src/providers/acp-target-catalog.ts +54 -1
- package/src/providers/acp-targets.ts +358 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/grok-adapter.ts +186 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/pricing-sources.md +6 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/rbac/can.ts +4 -3
- package/src/rbac/legacy-policy.ts +3 -3
- package/src/rbac/types.ts +6 -0
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +27 -8
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
- package/src/server-user.ts +2 -20
- package/src/server.ts +3 -1
- package/src/slack/app.ts +4 -0
- package/src/slack/config.ts +3 -1
- package/src/slack/egress-scrub.ts +87 -0
- package/src/tests/acp-adapter.test.ts +191 -2
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/budget-user-scope.test.ts +7 -21
- package/src/tests/check-pr-body.test.ts +149 -6
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/claude-adapter-binary.test.ts +1 -1
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
- package/src/tests/console-scrub.test.ts +141 -0
- package/src/tests/credential-check.test.ts +90 -0
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/explicit-model-validation.test.ts +81 -0
- package/src/tests/extensions-pre-task.test.ts +33 -1
- package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
- package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/grok-adapter.test.ts +647 -0
- package/src/tests/heartbeat-checklist.test.ts +44 -0
- package/src/tests/integration-disable-flags.test.ts +156 -0
- package/src/tests/kv-http.test.ts +34 -0
- package/src/tests/kv-tool.test.ts +274 -1
- package/src/tests/kv-view.test.ts +147 -0
- package/src/tests/mcp-user-route.test.ts +257 -15
- package/src/tests/memory-injection-outcome.test.ts +143 -0
- package/src/tests/model-tier-resolution.test.ts +76 -3
- package/src/tests/page-guest-session.test.ts +424 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-viewer-auth.test.ts +22 -1
- package/src/tests/pr-body-leaks.test.ts +157 -2
- package/src/tests/preload.ts +10 -0
- package/src/tests/provider-registration.test.ts +3 -1
- package/src/tests/rbac-audit.test.ts +32 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
- package/src/tests/script-connections.test.ts +9 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +126 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scripts-mcp-e2e.test.ts +42 -6
- package/src/tests/scripts-runtime.test.ts +37 -0
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +317 -1
- package/src/tests/session-costs-golden.test.ts +17 -0
- package/src/tests/slack-egress-scrub.test.ts +216 -0
- package/src/tests/swarm-tool-result-gate.test.ts +143 -30
- package/src/tests/task-tools-ctx.test.ts +3 -0
- package/src/tests/user-token-routes.test.ts +342 -3
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +737 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/kv/kv-get.ts +136 -6
- package/src/tools/send-task.ts +61 -17
- package/src/tools/utils.ts +186 -43
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +16 -1
- package/src/utils/console-scrub-install.ts +5 -0
- package/src/utils/console-scrub.ts +57 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/page-session.ts +6 -2
- package/src/utils/pr-body-leaks.ts +149 -10
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +662 -36
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/executors/notify.ts +6 -1
- package/src/workflows/executors/system-one-decision.ts +31 -12
- package/src/workflows/executors/system-one-openai-wire.ts +168 -0
- package/src/workflows/executors/system-one-providers.ts +112 -11
- package/src/workflows/executors/system-one-wire.ts +79 -0
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/skills/agent-browser/SKILL.md +5 -5
- package/templates/skills/agent-browser/config.json +1 -1
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/artifacts/content.md +17 -7
- package/templates/skills/jira-interaction/config.json +1 -1
- package/templates/skills/jira-interaction/content.md +56 -205
- package/templates/skills/linear-interaction/config.json +1 -1
- package/templates/skills/linear-interaction/content.md +31 -50
- package/templates/skills/pages/content.md +8 -4
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/workflow-iterate/content.md +4 -4
- package/tsconfig.json +3 -3
- package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
- package/dist/cli-0a7r5331.js +0 -168
- package/dist/cli-93rxw9yd.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-dgnj0zcc.js +0 -40
- package/dist/cli-qt5wzq6v.js +0 -256
- package/dist/cli-v7fqehqd.js +0 -9281
- package/dist/cli-vbr859rz.js +0 -572
- package/src/be/boot-scrub-logs.ts +0 -132
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
clearVolatileSecretsForTesting,
|
|
4
|
+
isSensitiveKey,
|
|
4
5
|
refreshSecretScrubberCache,
|
|
5
6
|
registerSensitiveKeyName,
|
|
6
7
|
registerVolatileSecret,
|
|
7
8
|
scrubObject,
|
|
8
9
|
scrubSecrets,
|
|
9
10
|
} from "../utils/secret-scrubber";
|
|
11
|
+
import { randomToken } from "./synthetic-secret-helpers";
|
|
10
12
|
|
|
11
13
|
// Snapshot/restore process.env between tests so env-derived cache entries
|
|
12
14
|
// don't leak across cases.
|
|
@@ -233,7 +235,8 @@ describe("scrubSecrets — regex patterns", () => {
|
|
|
233
235
|
|
|
234
236
|
test("regex patterns catch tokens even when env is empty", () => {
|
|
235
237
|
// Fresh env — no secrets registered — regex should still catch well-known shapes.
|
|
236
|
-
|
|
238
|
+
// `note=` is not a sensitive key, so only the token-shape pass can fire.
|
|
239
|
+
const out = scrubSecrets(`note=example-${"ghp_"}1234567890abcdefABCDEF1234567890ABCD`);
|
|
237
240
|
expect(out).toContain("[REDACTED:github_token]");
|
|
238
241
|
});
|
|
239
242
|
|
|
@@ -538,6 +541,32 @@ describe("scrubSecrets — env dumps and escaped forms", () => {
|
|
|
538
541
|
expect(scrubSecrets(jsonBody(s))).toBe(jsonBody(want));
|
|
539
542
|
});
|
|
540
543
|
|
|
544
|
+
// retry: wall-clock bound on a shared CI runner; a quadratic rescan takes seconds.
|
|
545
|
+
test(
|
|
546
|
+
"an unterminated escaped JSON value resumes the key scan where it stopped",
|
|
547
|
+
() => {
|
|
548
|
+
// The value walk stops on the bare quote; the next key still redacts.
|
|
549
|
+
const s = String.raw`log \"token\":\"abc\\"password\":\"s3cr3tval\" end`;
|
|
550
|
+
expect(scrubSecrets(s)).toBe(
|
|
551
|
+
String.raw`log \"token\":\"abc\\"password\":\"[REDACTED:password]\" end`,
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// ~200 KB single lines of escaped key fragments whose values never close.
|
|
555
|
+
for (const fragment of [
|
|
556
|
+
String.raw`\\\"token\\\":\\\"`,
|
|
557
|
+
String.raw`\"token\":\"\\\"`,
|
|
558
|
+
String.raw`\"token\":\"\\`,
|
|
559
|
+
String.raw`\"token\":\"`,
|
|
560
|
+
]) {
|
|
561
|
+
const input = fragment.repeat(Math.ceil(200_000 / fragment.length));
|
|
562
|
+
const start = performance.now();
|
|
563
|
+
scrubSecrets(input);
|
|
564
|
+
expect(performance.now() - start).toBeLessThan(50);
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
{ retry: 2 },
|
|
568
|
+
);
|
|
569
|
+
|
|
541
570
|
test("a quoted value closed at end of line may span lines", () => {
|
|
542
571
|
const dump = `${declareX("DEMO_MULTI_TOKEN", "line1\nline2")}\n${declareX("USER", "deploy")}`;
|
|
543
572
|
const want = `declare -x DEMO_MULTI_TOKEN="[REDACTED:DEMO_MULTI_TOKEN]"\n${declareX("USER", "deploy")}`;
|
|
@@ -558,3 +587,290 @@ describe("scrubSecrets — env dumps and escaped forms", () => {
|
|
|
558
587
|
}
|
|
559
588
|
});
|
|
560
589
|
});
|
|
590
|
+
|
|
591
|
+
// Every credential below is built at runtime; nothing secret-shaped is a literal.
|
|
592
|
+
function pemBlock(label: string): string {
|
|
593
|
+
const body = Array.from({ length: 5 }, () => randomToken(64)).join("\n");
|
|
594
|
+
return `-----BEGIN ${label}PRIVATE KEY-----\n${body}\n-----END ${label}PRIVATE KEY-----`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
interface ProbeCase {
|
|
598
|
+
shape: string;
|
|
599
|
+
input: string;
|
|
600
|
+
want: string;
|
|
601
|
+
secret: string;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function probeCases(): ProbeCase[] {
|
|
605
|
+
const cases: ProbeCase[] = [];
|
|
606
|
+
const add = (shape: string, secret: string, input: string, want: string) =>
|
|
607
|
+
cases.push({ shape, input, want, secret });
|
|
608
|
+
let s = randomToken(24);
|
|
609
|
+
add("bare PRIVATE_KEY=", s, `PRIVATE_KEY=${s}`, "PRIVATE_KEY=[REDACTED:PRIVATE_KEY]");
|
|
610
|
+
s = randomToken(24);
|
|
611
|
+
add("bare SECRET=", s, `SECRET=${s} next`, "SECRET=[REDACTED:SECRET] next");
|
|
612
|
+
s = randomToken(10);
|
|
613
|
+
add("bare PASSWORD=", s, `PASSWORD='${s}'`, "PASSWORD='[REDACTED:PASSWORD]'");
|
|
614
|
+
s = randomToken(32);
|
|
615
|
+
add("lowercase api_key=", s, `api_key=${s}`, "api_key=[REDACTED:api_key]");
|
|
616
|
+
s = randomToken(32);
|
|
617
|
+
add(
|
|
618
|
+
"dotted SLACK.BOT.TOKEN=",
|
|
619
|
+
s,
|
|
620
|
+
`SLACK.BOT.TOKEN=${s}`,
|
|
621
|
+
"SLACK.BOT.TOKEN=[REDACTED:SLACK.BOT.TOKEN]",
|
|
622
|
+
);
|
|
623
|
+
s = randomToken(40);
|
|
624
|
+
add(
|
|
625
|
+
"AWS_SECRET_ACCESS_KEY=",
|
|
626
|
+
s,
|
|
627
|
+
`export AWS_SECRET_ACCESS_KEY=${s}`,
|
|
628
|
+
"export AWS_SECRET_ACCESS_KEY=[REDACTED:AWS_SECRET_ACCESS_KEY]",
|
|
629
|
+
);
|
|
630
|
+
s = randomToken(40);
|
|
631
|
+
add(
|
|
632
|
+
"INI aws_secret_access_key = …",
|
|
633
|
+
s,
|
|
634
|
+
`[default]\naws_secret_access_key = ${s}\nregion = us-east-1`,
|
|
635
|
+
"[default]\naws_secret_access_key = [REDACTED:aws_secret_access_key]\nregion = us-east-1",
|
|
636
|
+
);
|
|
637
|
+
s = randomToken(20);
|
|
638
|
+
add(
|
|
639
|
+
"DATABASE_URL with userinfo",
|
|
640
|
+
s,
|
|
641
|
+
`DATABASE_URL=postgres://app:${s}@db.example:5432/x`,
|
|
642
|
+
"DATABASE_URL=postgres://app:[REDACTED:url_password]@db.example:5432/x",
|
|
643
|
+
);
|
|
644
|
+
s = randomToken(20);
|
|
645
|
+
add(
|
|
646
|
+
"https URL with userinfo",
|
|
647
|
+
s,
|
|
648
|
+
`git clone https://bob:${s}@git.example/r.git`,
|
|
649
|
+
"git clone https://bob:[REDACTED:url_password]@git.example/r.git",
|
|
650
|
+
);
|
|
651
|
+
for (const label of ["RSA ", "OPENSSH "]) {
|
|
652
|
+
const pem = pemBlock(label);
|
|
653
|
+
add(
|
|
654
|
+
`${label}PEM block`,
|
|
655
|
+
pem.split("\n")[2] ?? "",
|
|
656
|
+
`key:\n${pem}\ndone`,
|
|
657
|
+
`key:\n-----BEGIN ${label}PRIVATE KEY-----[REDACTED:private_key]-----END ${label}PRIVATE KEY-----\ndone`,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const gcpPem = pemBlock("");
|
|
661
|
+
add(
|
|
662
|
+
"GCP service-account JSON private_key",
|
|
663
|
+
gcpPem.split("\n")[2] ?? "",
|
|
664
|
+
JSON.stringify({
|
|
665
|
+
type: "service_account",
|
|
666
|
+
private_key: `${gcpPem}\n`,
|
|
667
|
+
client_email: "bot@x.example",
|
|
668
|
+
}),
|
|
669
|
+
'{"type":"service_account","private_key":"-----BEGIN PRIVATE KEY-----[REDACTED:private_key]-----END PRIVATE KEY-----\\n","client_email":"bot@x.example"}',
|
|
670
|
+
);
|
|
671
|
+
s = `${randomToken(30)}.${randomToken(20)}`;
|
|
672
|
+
add(
|
|
673
|
+
"Authorization: Bearer (raw)",
|
|
674
|
+
s,
|
|
675
|
+
`Authorization: Bearer ${s}\nAccept: */*`,
|
|
676
|
+
"Authorization: Bearer [REDACTED:authorization]\nAccept: */*",
|
|
677
|
+
);
|
|
678
|
+
s = randomToken(40);
|
|
679
|
+
add(
|
|
680
|
+
"Authorization: Bearer (JSON headers)",
|
|
681
|
+
s,
|
|
682
|
+
JSON.stringify({ headers: { Authorization: `Bearer ${s}`, Accept: "x" } }),
|
|
683
|
+
'{"headers":{"Authorization":"Bearer [REDACTED:authorization]","Accept":"x"}}',
|
|
684
|
+
);
|
|
685
|
+
s = randomToken(32);
|
|
686
|
+
add("x-api-key header", s, `x-api-key: ${s}`, "x-api-key: [REDACTED:x_api_key]");
|
|
687
|
+
s = randomToken(40);
|
|
688
|
+
add(
|
|
689
|
+
"curl -H 'Authorization: token …'",
|
|
690
|
+
s,
|
|
691
|
+
`curl -H 'Authorization: token ${s}' https://api.example/x`,
|
|
692
|
+
"curl -H 'Authorization: token [REDACTED:authorization]' https://api.example/x",
|
|
693
|
+
);
|
|
694
|
+
s = randomToken(14);
|
|
695
|
+
add(
|
|
696
|
+
"curl --password",
|
|
697
|
+
s,
|
|
698
|
+
`curl --user bob --password ${s} https://x.example`,
|
|
699
|
+
"curl --user bob --password [REDACTED:curl_password] https://x.example",
|
|
700
|
+
);
|
|
701
|
+
s = randomToken(14);
|
|
702
|
+
add(
|
|
703
|
+
"curl -u user:pass",
|
|
704
|
+
s,
|
|
705
|
+
`curl -u bob:${s} https://x.example`,
|
|
706
|
+
"curl -u bob:[REDACTED:curl_password] https://x.example",
|
|
707
|
+
);
|
|
708
|
+
s = `${randomToken(8)}\\"${randomToken(8)}`;
|
|
709
|
+
add(
|
|
710
|
+
'JSON "password" (with an escaped quote)',
|
|
711
|
+
s,
|
|
712
|
+
`{"user":"bob","password":"${s}"}`,
|
|
713
|
+
'{"user":"bob","password":"[REDACTED:password]"}',
|
|
714
|
+
);
|
|
715
|
+
s = randomToken(32);
|
|
716
|
+
add('JSON "apiKey"', s, JSON.stringify({ apiKey: s }), '{"apiKey":"[REDACTED:apiKey]"}');
|
|
717
|
+
s = randomToken(32);
|
|
718
|
+
add(
|
|
719
|
+
'JSON "client_secret"',
|
|
720
|
+
s,
|
|
721
|
+
JSON.stringify({ client_id: "abc", client_secret: s }),
|
|
722
|
+
'{"client_id":"abc","client_secret":"[REDACTED:client_secret]"}',
|
|
723
|
+
);
|
|
724
|
+
s = randomToken(32);
|
|
725
|
+
add(
|
|
726
|
+
"YAML token:",
|
|
727
|
+
s,
|
|
728
|
+
`auth:\n token: ${s}\n user: bob\n`,
|
|
729
|
+
"auth:\n token: [REDACTED:token]\n user: bob\n",
|
|
730
|
+
);
|
|
731
|
+
return cases;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
describe("scrubSecrets — structured credentials (audit probe)", () => {
|
|
735
|
+
for (const { shape, input, want, secret } of probeCases()) {
|
|
736
|
+
test(shape, () => {
|
|
737
|
+
expect(secret.length).toBeGreaterThan(8);
|
|
738
|
+
// Raw, and JSON-escaped as the line lands in session_logs.
|
|
739
|
+
for (const [text, expected] of [
|
|
740
|
+
[input, want],
|
|
741
|
+
[jsonBody(input), jsonBody(want)],
|
|
742
|
+
]) {
|
|
743
|
+
const out = scrubSecrets(text);
|
|
744
|
+
expect(out).not.toContain(secret);
|
|
745
|
+
expect(out).toContain("[REDACTED:");
|
|
746
|
+
expect(out).toBe(expected as string);
|
|
747
|
+
expect(scrubSecrets(out)).toBe(out);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
test("a JSON private_key value is consumed whole across \\n escapes", () => {
|
|
753
|
+
const tail = randomToken(24);
|
|
754
|
+
const input = JSON.stringify({ private_key: `${randomToken(16)}\n${tail}\n` });
|
|
755
|
+
expect(scrubSecrets(input)).toBe('{"private_key":"[REDACTED:private_key]"}');
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test("a sensitive key nested under a dotted path still counts", () => {
|
|
759
|
+
const s = randomToken(20);
|
|
760
|
+
expect(scrubSecrets(`config.SECRETS_ENCRYPTION_KEY=${s}`)).toBe(
|
|
761
|
+
"config.SECRETS_ENCRYPTION_KEY=[REDACTED:config.SECRETS_ENCRYPTION_KEY]",
|
|
762
|
+
);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test("isSensitiveKey normalizes case, camelCase, dots and dashes", () => {
|
|
766
|
+
for (const key of [
|
|
767
|
+
"apiKey",
|
|
768
|
+
"client_secret",
|
|
769
|
+
"x-api-key",
|
|
770
|
+
"slack.bot.token",
|
|
771
|
+
"secretAccessKey",
|
|
772
|
+
"SENTRY_DSN",
|
|
773
|
+
"DEPLOY_CREDENTIALS",
|
|
774
|
+
"passwd",
|
|
775
|
+
]) {
|
|
776
|
+
expect(isSensitiveKey(key)).toBe(true);
|
|
777
|
+
}
|
|
778
|
+
for (const key of [
|
|
779
|
+
"PWD",
|
|
780
|
+
"SORT_KEY",
|
|
781
|
+
"primaryKey",
|
|
782
|
+
"contextKey",
|
|
783
|
+
"max_tokens",
|
|
784
|
+
"token_count",
|
|
785
|
+
"nextPageToken",
|
|
786
|
+
"GOOGLE_APPLICATION_CREDENTIALS",
|
|
787
|
+
"MCP_BASE_URL",
|
|
788
|
+
]) {
|
|
789
|
+
expect(isSensitiveKey(key)).toBe(false);
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
describe("scrubSecrets — over-redaction guards", () => {
|
|
795
|
+
test("non-secret text stays byte-identical, raw and JSON-escaped", () => {
|
|
796
|
+
const sha = randomToken(40)
|
|
797
|
+
.toLowerCase()
|
|
798
|
+
.replace(/[^0-9a-f]/g, "0");
|
|
799
|
+
const png = `data:image/png;base64,${Buffer.from(randomToken(60)).toString("base64")}`;
|
|
800
|
+
const lines = [
|
|
801
|
+
"max_tokens=4096",
|
|
802
|
+
'"input_tokens": 1234',
|
|
803
|
+
"token_count: 12",
|
|
804
|
+
"PWD=/workspace",
|
|
805
|
+
"SORT_KEY=abc",
|
|
806
|
+
"primaryKey: id",
|
|
807
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: testing workflow syntax
|
|
808
|
+
"password: ${{ secrets.DB_PASSWORD }}",
|
|
809
|
+
"token: <your-token>",
|
|
810
|
+
"Authorization: Bearer [REDACTED:x]",
|
|
811
|
+
`commit ${sha}`,
|
|
812
|
+
crypto.randomUUID(),
|
|
813
|
+
`task: ${crypto.randomUUID()}`,
|
|
814
|
+
png,
|
|
815
|
+
"https://github.com/org/repo",
|
|
816
|
+
"const password = getPassword()",
|
|
817
|
+
// Code and prose that share a key name with a credential.
|
|
818
|
+
" token: string;",
|
|
819
|
+
" password: process.env.DB_PASSWORD",
|
|
820
|
+
"token = get_token()",
|
|
821
|
+
" password = pwd",
|
|
822
|
+
"Authorization: Bearer followed by the token",
|
|
823
|
+
'{"nextPageToken":"CAoQAA","secret":false,"token":null}',
|
|
824
|
+
"docker run -u 1000:1000 -u node:node img",
|
|
825
|
+
"echo $PW | docker login --password-stdin",
|
|
826
|
+
"http://localhost:3013/x?email=a@b.example",
|
|
827
|
+
"GOOGLE_APPLICATION_CREDENTIALS=/home/worker/.config/gcloud/sa.json",
|
|
828
|
+
" - uses: actions/checkout@v4",
|
|
829
|
+
"postgres://app:***@db.example/x",
|
|
830
|
+
];
|
|
831
|
+
for (const line of lines) {
|
|
832
|
+
expect(scrubSecrets(line)).toBe(line);
|
|
833
|
+
expect(scrubSecrets(jsonBody(line))).toBe(jsonBody(line));
|
|
834
|
+
}
|
|
835
|
+
const all = lines.join("\n");
|
|
836
|
+
expect(scrubSecrets(all)).toBe(all);
|
|
837
|
+
expect(scrubSecrets(jsonBody(all))).toBe(jsonBody(all));
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
// Each input is ~200 KB and built to make a naive pattern backtrack.
|
|
841
|
+
// Allow shared-runner headroom; a real ReDoS on these inputs takes seconds.
|
|
842
|
+
// retry: tolerate transient contention without weakening the per-input guard.
|
|
843
|
+
test(
|
|
844
|
+
"new key-context rules stay linear on 200 KB adversarial input",
|
|
845
|
+
() => {
|
|
846
|
+
const inputs = [
|
|
847
|
+
`-----BEGIN RSA PRIVATE KEY-----${"A".repeat(200_000)}`,
|
|
848
|
+
"-----BEGIN RSA PRIVATE KEY-----\n".repeat(6_000),
|
|
849
|
+
`"password": "${"\\\\".repeat(100_000)}`,
|
|
850
|
+
`\\"password\\": \\"${"\\\\".repeat(100_000)}`,
|
|
851
|
+
`"password": "${"a".repeat(200_000)}`,
|
|
852
|
+
`\\"password\\": \\"${"a".repeat(200_000)}`,
|
|
853
|
+
`"password": "`.repeat(14_000),
|
|
854
|
+
`token: ${"a".repeat(200_000)} x`,
|
|
855
|
+
"token: a\n".repeat(25_000),
|
|
856
|
+
`password = ${"a".repeat(200_000)} x`,
|
|
857
|
+
"Authorization: Bearer ".repeat(9_000),
|
|
858
|
+
`Authorization: Bearer ${"A".repeat(200_000)}[`,
|
|
859
|
+
"a".repeat(200_000),
|
|
860
|
+
`https://${"u".repeat(200_000)}`,
|
|
861
|
+
"a://a:".repeat(33_000),
|
|
862
|
+
" -u a:".repeat(33_000),
|
|
863
|
+
`curl -u ${"a".repeat(200_000)}`,
|
|
864
|
+
"a.".repeat(100_000),
|
|
865
|
+
"\\n".repeat(100_000),
|
|
866
|
+
`"${"a".repeat(63)}":`.repeat(3_000),
|
|
867
|
+
];
|
|
868
|
+
for (const input of inputs) {
|
|
869
|
+
const start = performance.now();
|
|
870
|
+
scrubSecrets(input);
|
|
871
|
+
expect(performance.now() - start).toBeLessThan(500);
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
{ retry: 2 },
|
|
875
|
+
);
|
|
876
|
+
});
|
|
@@ -344,6 +344,23 @@ describe("Phase 3 — session cost recompute golden fixtures", () => {
|
|
|
344
344
|
expect(auto.cost.costSource).toBe("unpriced");
|
|
345
345
|
});
|
|
346
346
|
|
|
347
|
+
// Boot-seeded rows only: grok rows project the models.dev xAI section.
|
|
348
|
+
test("grok runs price from the seeded xAI rates", async () => {
|
|
349
|
+
const body = await postCost({
|
|
350
|
+
sessionId: "grok-4-6",
|
|
351
|
+
provider: "grok",
|
|
352
|
+
model: "grok-4.6",
|
|
353
|
+
totalCostUsd: 0,
|
|
354
|
+
inputTokens: 2_000,
|
|
355
|
+
outputTokens: 300,
|
|
356
|
+
cacheReadTokens: 1_000,
|
|
357
|
+
});
|
|
358
|
+
const expected = (2_000 * 2 + 1_000 * 0.5 + 300 * 6) / 1_000_000;
|
|
359
|
+
|
|
360
|
+
expect(body.cost.costSource).toBe("pricing-table");
|
|
361
|
+
expectExact(body.cost.totalCostUsd, expected);
|
|
362
|
+
});
|
|
363
|
+
|
|
347
364
|
test("legacy cache writes without a TTL split keep the 5m class", async () => {
|
|
348
365
|
await seedModelRates("claude", "claude-legacy-cache-write", {
|
|
349
366
|
input: 5,
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { App, webApi } from "@slack/bolt";
|
|
3
|
+
import { sendKapsoText } from "../integrations/kapso/client";
|
|
4
|
+
import { installSlackEgressScrub } from "../slack/egress-scrub";
|
|
5
|
+
import type { ExecutorDependencies } from "../workflows/executors/base";
|
|
6
|
+
import { NotifyExecutor } from "../workflows/executors/notify";
|
|
7
|
+
|
|
8
|
+
// Built at runtime so no secret-shaped literal sits in the repo.
|
|
9
|
+
const SECRET = `ghp_${"Z".repeat(36)}`;
|
|
10
|
+
const REDACTED = "[REDACTED:";
|
|
11
|
+
|
|
12
|
+
type Captured = { method: string; body: string; params: URLSearchParams };
|
|
13
|
+
|
|
14
|
+
const captured: Captured[] = [];
|
|
15
|
+
const uploads: Uint8Array[] = [];
|
|
16
|
+
let server: ReturnType<typeof Bun.serve>;
|
|
17
|
+
let baseUrl = "";
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
installSlackEgressScrub();
|
|
21
|
+
server = Bun.serve({
|
|
22
|
+
port: 0,
|
|
23
|
+
async fetch(req) {
|
|
24
|
+
const { pathname } = new URL(req.url);
|
|
25
|
+
if (pathname.startsWith("/upload/")) {
|
|
26
|
+
// web-api posts the file as multipart form data under the `body` field.
|
|
27
|
+
const file = (await req.formData()).get("body") as Blob;
|
|
28
|
+
uploads.push(new Uint8Array(await file.arrayBuffer()));
|
|
29
|
+
return new Response("OK");
|
|
30
|
+
}
|
|
31
|
+
const method = pathname.replace(/^\/api\//, "");
|
|
32
|
+
const body = await req.text();
|
|
33
|
+
captured.push({ method, body, params: new URLSearchParams(body) });
|
|
34
|
+
if (method === "files.getUploadURLExternal") {
|
|
35
|
+
return Response.json({ ok: true, upload_url: `${baseUrl}upload/F1`, file_id: "F1" });
|
|
36
|
+
}
|
|
37
|
+
if (method === "files.completeUploadExternal") {
|
|
38
|
+
return Response.json({ ok: true, files: [{ id: "F1" }] });
|
|
39
|
+
}
|
|
40
|
+
return Response.json({ ok: true, ts: "1.000001", channel: "C1", messages: [] });
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
baseUrl = `http://localhost:${server.port}/`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterAll(() => {
|
|
47
|
+
server.stop(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
captured.length = 0;
|
|
52
|
+
uploads.length = 0;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function client(): InstanceType<typeof webApi.WebClient> {
|
|
56
|
+
return new webApi.WebClient("xoxb-test", {
|
|
57
|
+
slackApiUrl: `${baseUrl}api/`,
|
|
58
|
+
retryConfig: { retries: 0 },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function lastCall(method: string): Captured {
|
|
63
|
+
const call = captured.findLast((c) => c.method === method);
|
|
64
|
+
if (!call) throw new Error(`no ${method} call captured`);
|
|
65
|
+
return call;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function expectRedacted(text: string | null | undefined): void {
|
|
69
|
+
expect(text).toBeTruthy();
|
|
70
|
+
expect(text).not.toContain(SECRET);
|
|
71
|
+
expect(text).toContain(REDACTED);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("Slack client egress scrub", () => {
|
|
75
|
+
test("chat.postMessage scrubs text and keeps Block Kit structure", async () => {
|
|
76
|
+
await client().chat.postMessage({
|
|
77
|
+
channel: "C1",
|
|
78
|
+
text: `deploy key ${SECRET}`,
|
|
79
|
+
unfurl_links: false,
|
|
80
|
+
unfurl_media: false,
|
|
81
|
+
blocks: [
|
|
82
|
+
{ type: "section", text: { type: "mrkdwn", text: `token: ${SECRET} end` } },
|
|
83
|
+
{ type: "divider" },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
const call = lastCall("chat.postMessage");
|
|
87
|
+
expect(call.body).not.toContain(SECRET);
|
|
88
|
+
expectRedacted(call.params.get("text"));
|
|
89
|
+
const blocks = JSON.parse(call.params.get("blocks") ?? "null");
|
|
90
|
+
expect(Array.isArray(blocks)).toBe(true);
|
|
91
|
+
expect(blocks).toHaveLength(2);
|
|
92
|
+
expect(blocks[0].type).toBe("section");
|
|
93
|
+
expect(blocks[0].text.text).toStartWith("token: ");
|
|
94
|
+
expect(blocks[0].text.text).toEndWith(" end");
|
|
95
|
+
expectRedacted(blocks[0].text.text);
|
|
96
|
+
expect(blocks[1]).toEqual({ type: "divider" });
|
|
97
|
+
expect(call.params.get("channel")).toBe("C1");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("chat.update and chat.postEphemeral are scrubbed", async () => {
|
|
101
|
+
const c = client();
|
|
102
|
+
await c.chat.update({
|
|
103
|
+
channel: "C1",
|
|
104
|
+
ts: "1.0",
|
|
105
|
+
text: `edit ${SECRET}`,
|
|
106
|
+
unfurl_links: false,
|
|
107
|
+
unfurl_media: false,
|
|
108
|
+
});
|
|
109
|
+
await c.chat.postEphemeral({ channel: "C1", user: "U1", text: `psst ${SECRET}` });
|
|
110
|
+
expectRedacted(lastCall("chat.update").params.get("text"));
|
|
111
|
+
expectRedacted(lastCall("chat.postEphemeral").params.get("text"));
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("files.uploadV2 scrubs text content, comment and title", async () => {
|
|
115
|
+
await client().files.uploadV2({
|
|
116
|
+
channel_id: "C1",
|
|
117
|
+
content: `line one\nexport GH=${SECRET}\n`,
|
|
118
|
+
filename: "report.txt",
|
|
119
|
+
title: `report ${SECRET}`,
|
|
120
|
+
initial_comment: `see ${SECRET}`,
|
|
121
|
+
});
|
|
122
|
+
expect(uploads).toHaveLength(1);
|
|
123
|
+
const uploaded = new TextDecoder().decode(uploads[0]);
|
|
124
|
+
expect(uploaded).toStartWith("line one\n");
|
|
125
|
+
expectRedacted(uploaded);
|
|
126
|
+
const complete = lastCall("files.completeUploadExternal");
|
|
127
|
+
expect(complete.body).not.toContain(SECRET);
|
|
128
|
+
expectRedacted(complete.params.get("initial_comment"));
|
|
129
|
+
expectRedacted(complete.params.get("files"));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("binary uploads pass through byte-identical", async () => {
|
|
133
|
+
const bytes = Buffer.concat([Buffer.from([0, 255, 1, 254]), Buffer.from(SECRET)]);
|
|
134
|
+
await client().files.uploadV2({ channel_id: "C1", file: bytes, filename: "blob.bin" });
|
|
135
|
+
expect(uploads).toHaveLength(1);
|
|
136
|
+
expect(Buffer.from(uploads[0]).equals(bytes)).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a second install does not double-wrap", async () => {
|
|
140
|
+
const proto = webApi.WebClient.prototype;
|
|
141
|
+
const before = proto.apiCall;
|
|
142
|
+
installSlackEgressScrub();
|
|
143
|
+
expect(proto.apiCall).toBe(before);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("a Bolt App built after install scrubs app.client sends", async () => {
|
|
147
|
+
const app = new App({
|
|
148
|
+
token: "xoxb-test",
|
|
149
|
+
signingSecret: "test-signing-secret",
|
|
150
|
+
tokenVerificationEnabled: false,
|
|
151
|
+
clientOptions: { slackApiUrl: `${baseUrl}api/`, retryConfig: { retries: 0 } },
|
|
152
|
+
});
|
|
153
|
+
await app.client.chat.postMessage({
|
|
154
|
+
channel: "C1",
|
|
155
|
+
text: `bolt ${SECRET}`,
|
|
156
|
+
unfurl_links: false,
|
|
157
|
+
unfurl_media: false,
|
|
158
|
+
});
|
|
159
|
+
expectRedacted(lastCall("chat.postMessage").params.get("text"));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("WhatsApp and notify egress scrub", () => {
|
|
164
|
+
const originalFetch = globalThis.fetch;
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
globalThis.fetch = originalFetch;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("sendKapsoText scrubs the message body", async () => {
|
|
170
|
+
let sent = "";
|
|
171
|
+
globalThis.fetch = (async (_url: string, init: RequestInit) => {
|
|
172
|
+
sent = init.body as string;
|
|
173
|
+
return Response.json({ messages: [{ id: "wamid.X" }] });
|
|
174
|
+
}) as typeof fetch;
|
|
175
|
+
const result = await sendKapsoText({
|
|
176
|
+
apiBaseUrl: "https://kapso.invalid",
|
|
177
|
+
apiKey: "k",
|
|
178
|
+
phoneNumberId: "1",
|
|
179
|
+
to: "2",
|
|
180
|
+
body: `hola ${SECRET}`,
|
|
181
|
+
});
|
|
182
|
+
expect(result.ok).toBe(true);
|
|
183
|
+
const body = JSON.parse(sent) as { text: { body: string } };
|
|
184
|
+
expect(body.text.body).toStartWith("hola ");
|
|
185
|
+
expectRedacted(body.text.body);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("notify executor scrubs the message before posting and in its output", async () => {
|
|
189
|
+
const posted: string[] = [];
|
|
190
|
+
const deps = {
|
|
191
|
+
db: {
|
|
192
|
+
postMessage: (_channel: string, _agent: string | null, content: string) => {
|
|
193
|
+
posted.push(content);
|
|
194
|
+
return { id: "m1" };
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
eventBus: { emit: () => {}, on: () => {}, off: () => {} },
|
|
198
|
+
interpolate: (template: string, ctx: Record<string, unknown>) =>
|
|
199
|
+
template.replace("{{v}}", String(ctx.v)),
|
|
200
|
+
} as unknown as ExecutorDependencies;
|
|
201
|
+
const result = await new NotifyExecutor(deps).run({
|
|
202
|
+
config: { channel: "swarm", target: "chan", template: "value {{v}}" },
|
|
203
|
+
context: { v: SECRET },
|
|
204
|
+
meta: {
|
|
205
|
+
runId: "00000000-0000-0000-0000-000000000001",
|
|
206
|
+
stepId: "00000000-0000-0000-0000-000000000002",
|
|
207
|
+
nodeId: "n",
|
|
208
|
+
workflowId: "00000000-0000-0000-0000-000000000003",
|
|
209
|
+
dryRun: false,
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
expect(result.status).toBe("success");
|
|
213
|
+
expectRedacted(posted[0]);
|
|
214
|
+
expectRedacted((result.output as { message: string }).message);
|
|
215
|
+
});
|
|
216
|
+
});
|