vellum 0.2.1 → 0.2.7
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 +15 -2
- package/bun.lock +71 -100
- package/package.json +5 -3
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +305 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twilio-config.test.ts +221 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +71 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +100 -2
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +303 -0
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-conflict-gate.test.ts +28 -25
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/__tests__/twilio-webhook-urls.test.ts +162 -0
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +8 -8
- package/src/calls/twilio-provider.ts +13 -9
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/twilio-webhook-urls.ts +50 -0
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +270 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +34 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +165 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -5
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +205 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +32 -4
- package/src/daemon/ipc-contract.ts +156 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +75 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-conflict-gate.ts +18 -109
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/conflict-intent.ts +114 -0
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/job-handlers/conflict.ts +23 -1
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/gateway-client.ts +36 -0
- package/src/runtime/http-server.ts +166 -22
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +125 -88
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +293 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +207 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +269 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
sanitizeHeaders,
|
|
4
|
+
sanitizeUrl,
|
|
5
|
+
createSafeLogEntry,
|
|
6
|
+
stripQueryString,
|
|
7
|
+
buildDecisionTrace,
|
|
8
|
+
buildCredentialRefTrace,
|
|
9
|
+
} from '../logging.js';
|
|
10
|
+
import type { PolicyDecision } from '../types.js';
|
|
11
|
+
import type { CredentialInjectionTemplate } from '../../../credentials/policy-types.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// sanitizeHeaders
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
describe('sanitizeHeaders', () => {
|
|
18
|
+
test('redacts sensitive keys', () => {
|
|
19
|
+
const headers = {
|
|
20
|
+
'Authorization': 'secret-value',
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'X-API-Key': 'another-secret',
|
|
23
|
+
};
|
|
24
|
+
const result = sanitizeHeaders(headers, ['authorization', 'x-api-key']);
|
|
25
|
+
expect(result['Authorization']).toBe('[REDACTED]');
|
|
26
|
+
expect(result['Content-Type']).toBe('application/json');
|
|
27
|
+
expect(result['X-API-Key']).toBe('[REDACTED]');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('case-insensitive matching', () => {
|
|
31
|
+
const headers = { 'authorization': 'bearer xyz' };
|
|
32
|
+
const result = sanitizeHeaders(headers, ['Authorization']);
|
|
33
|
+
expect(result['authorization']).toBe('[REDACTED]');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('preserves non-sensitive headers', () => {
|
|
37
|
+
const headers = { 'Accept': 'text/html', 'Host': 'example.com' };
|
|
38
|
+
const result = sanitizeHeaders(headers, ['authorization']);
|
|
39
|
+
expect(result).toEqual(headers);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('handles empty headers', () => {
|
|
43
|
+
const result = sanitizeHeaders({}, ['authorization']);
|
|
44
|
+
expect(result).toEqual({});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('handles empty sensitive keys', () => {
|
|
48
|
+
const headers = { 'Authorization': 'bearer xyz' };
|
|
49
|
+
const result = sanitizeHeaders(headers, []);
|
|
50
|
+
expect(result['Authorization']).toBe('bearer xyz');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// sanitizeUrl
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
describe('sanitizeUrl', () => {
|
|
59
|
+
test('redacts sensitive query params from absolute URL', () => {
|
|
60
|
+
const result = sanitizeUrl('https://api.example.com/v1?api_key=secret123&format=json', ['api_key']);
|
|
61
|
+
expect(result).toContain('api_key=%5BREDACTED%5D');
|
|
62
|
+
expect(result).toContain('format=json');
|
|
63
|
+
expect(result).not.toContain('secret123');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('redacts sensitive query params from relative path', () => {
|
|
67
|
+
const result = sanitizeUrl('/v1/search?token=abc&q=hello', ['token']);
|
|
68
|
+
expect(result).toContain('token=%5BREDACTED%5D');
|
|
69
|
+
expect(result).toContain('q=hello');
|
|
70
|
+
expect(result).not.toContain('abc');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('returns URL unchanged when no sensitive params', () => {
|
|
74
|
+
const url = 'https://api.example.com/v1?format=json';
|
|
75
|
+
expect(sanitizeUrl(url, ['api_key'])).toBe(url);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('returns URL unchanged when sensitiveParams is empty', () => {
|
|
79
|
+
const url = 'https://api.example.com/v1?api_key=secret';
|
|
80
|
+
expect(sanitizeUrl(url, [])).toBe(url);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('returns URL unchanged when no query string', () => {
|
|
84
|
+
expect(sanitizeUrl('https://api.example.com/v1', ['api_key']))
|
|
85
|
+
.toBe('https://api.example.com/v1');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('case-insensitive param matching', () => {
|
|
89
|
+
const result = sanitizeUrl('https://api.example.com/v1?API_KEY=secret', ['api_key']);
|
|
90
|
+
expect(result).not.toContain('secret');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('strips query string entirely for unparseable URLs', () => {
|
|
94
|
+
// Malformed URL that URL constructor can't parse
|
|
95
|
+
const result = sanitizeUrl('http://[invalid:url?key=secret', ['key']);
|
|
96
|
+
expect(result).not.toContain('secret');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// createSafeLogEntry
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
describe('createSafeLogEntry', () => {
|
|
105
|
+
test('sanitizes both URL and headers', () => {
|
|
106
|
+
const req = {
|
|
107
|
+
method: 'GET',
|
|
108
|
+
url: '/api?token=secret',
|
|
109
|
+
headers: { 'Authorization': 'Bearer xyz', 'Accept': 'application/json' },
|
|
110
|
+
};
|
|
111
|
+
const result = createSafeLogEntry(req, ['authorization', 'token']);
|
|
112
|
+
expect(result.method).toBe('GET');
|
|
113
|
+
expect(result.url).not.toContain('secret');
|
|
114
|
+
expect(result.headers['Authorization']).toBe('[REDACTED]');
|
|
115
|
+
expect(result.headers['Accept']).toBe('application/json');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// stripQueryString
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
describe('stripQueryString', () => {
|
|
124
|
+
test('strips query from path', () => {
|
|
125
|
+
expect(stripQueryString('/api/v1?key=value')).toBe('/api/v1');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('returns path unchanged when no query', () => {
|
|
129
|
+
expect(stripQueryString('/api/v1')).toBe('/api/v1');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('handles empty path', () => {
|
|
133
|
+
expect(stripQueryString('')).toBe('');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('handles query-only', () => {
|
|
137
|
+
expect(stripQueryString('?key=value')).toBe('');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// buildDecisionTrace
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
describe('buildDecisionTrace', () => {
|
|
146
|
+
test('matched decision', () => {
|
|
147
|
+
const decision: PolicyDecision = {
|
|
148
|
+
kind: 'matched',
|
|
149
|
+
credentialId: 'cred-1',
|
|
150
|
+
template: {
|
|
151
|
+
hostPattern: '*.example.com',
|
|
152
|
+
injectionType: 'header',
|
|
153
|
+
headerName: 'Authorization',
|
|
154
|
+
valuePrefix: 'Bearer ',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const trace = buildDecisionTrace('api.example.com', 443, '/api?key=secret', 'https', decision);
|
|
158
|
+
expect(trace.host).toBe('api.example.com');
|
|
159
|
+
expect(trace.port).toBe(443);
|
|
160
|
+
expect(trace.path).toBe('/api');
|
|
161
|
+
expect(trace.scheme).toBe('https');
|
|
162
|
+
expect(trace.decisionKind).toBe('matched');
|
|
163
|
+
expect(trace.candidateCount).toBe(1);
|
|
164
|
+
expect(trace.selectedPattern).toBe('*.example.com');
|
|
165
|
+
expect(trace.selectedCredentialId).toBe('cred-1');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('ambiguous decision', () => {
|
|
169
|
+
const decision: PolicyDecision = {
|
|
170
|
+
kind: 'ambiguous',
|
|
171
|
+
candidates: [
|
|
172
|
+
{ credentialId: 'cred-1', template: { hostPattern: '*.example.com', injectionType: 'header' } as CredentialInjectionTemplate },
|
|
173
|
+
{ credentialId: 'cred-2', template: { hostPattern: '*.example.com', injectionType: 'header' } as CredentialInjectionTemplate },
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
const trace = buildDecisionTrace('api.example.com', null, '/', 'https', decision);
|
|
177
|
+
expect(trace.decisionKind).toBe('ambiguous');
|
|
178
|
+
expect(trace.candidateCount).toBe(2);
|
|
179
|
+
expect(trace.selectedPattern).toBeNull();
|
|
180
|
+
expect(trace.selectedCredentialId).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('missing decision', () => {
|
|
184
|
+
const decision: PolicyDecision = { kind: 'missing' };
|
|
185
|
+
const trace = buildDecisionTrace('unknown.com', null, '/', 'https', decision);
|
|
186
|
+
expect(trace.decisionKind).toBe('missing');
|
|
187
|
+
expect(trace.candidateCount).toBe(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('unauthenticated decision', () => {
|
|
191
|
+
const decision: PolicyDecision = { kind: 'unauthenticated' };
|
|
192
|
+
const trace = buildDecisionTrace('example.com', null, '/', 'http', decision);
|
|
193
|
+
expect(trace.decisionKind).toBe('unauthenticated');
|
|
194
|
+
expect(trace.candidateCount).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('ask_missing_credential decision', () => {
|
|
198
|
+
const decision: PolicyDecision = {
|
|
199
|
+
kind: 'ask_missing_credential',
|
|
200
|
+
target: { hostname: 'api.example.com', port: null, path: '/', scheme: 'https' },
|
|
201
|
+
matchingPatterns: ['*.example.com', 'api.example.com'],
|
|
202
|
+
};
|
|
203
|
+
const trace = buildDecisionTrace('api.example.com', null, '/', 'https', decision);
|
|
204
|
+
expect(trace.decisionKind).toBe('ask_missing_credential');
|
|
205
|
+
expect(trace.candidateCount).toBe(2);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('ask_unauthenticated decision', () => {
|
|
209
|
+
const decision: PolicyDecision = {
|
|
210
|
+
kind: 'ask_unauthenticated',
|
|
211
|
+
target: { hostname: 'unknown.com', port: null, path: '/', scheme: 'https' },
|
|
212
|
+
};
|
|
213
|
+
const trace = buildDecisionTrace('unknown.com', null, '/', 'https', decision);
|
|
214
|
+
expect(trace.decisionKind).toBe('ask_unauthenticated');
|
|
215
|
+
expect(trace.candidateCount).toBe(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('strips query string from path to avoid leaking secrets', () => {
|
|
219
|
+
const decision: PolicyDecision = { kind: 'unauthenticated' };
|
|
220
|
+
const trace = buildDecisionTrace('example.com', null, '/api?secret=abc', 'https', decision);
|
|
221
|
+
expect(trace.path).toBe('/api');
|
|
222
|
+
expect(trace.path).not.toContain('secret');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// buildCredentialRefTrace
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
describe('buildCredentialRefTrace', () => {
|
|
231
|
+
test('builds trace with all fields', () => {
|
|
232
|
+
const trace = buildCredentialRefTrace(
|
|
233
|
+
['my-api-key', 'unknown-ref'],
|
|
234
|
+
['uuid-1'],
|
|
235
|
+
['unknown-ref'],
|
|
236
|
+
);
|
|
237
|
+
expect(trace.rawRefs).toEqual(['my-api-key', 'unknown-ref']);
|
|
238
|
+
expect(trace.resolvedIds).toEqual(['uuid-1']);
|
|
239
|
+
expect(trace.unresolvedRefs).toEqual(['unknown-ref']);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('handles empty arrays', () => {
|
|
243
|
+
const trace = buildCredentialRefTrace([], [], []);
|
|
244
|
+
expect(trace.rawRefs).toEqual([]);
|
|
245
|
+
expect(trace.resolvedIds).toEqual([]);
|
|
246
|
+
expect(trace.unresolvedRefs).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { evaluateRequest, evaluateRequestWithApproval } from '../policy.js';
|
|
3
|
+
import type { CredentialInjectionTemplate } from '../../../credentials/policy-types.js';
|
|
4
|
+
|
|
5
|
+
function makeTemplate(overrides: Partial<CredentialInjectionTemplate> = {}): CredentialInjectionTemplate {
|
|
6
|
+
return {
|
|
7
|
+
hostPattern: '*.example.com',
|
|
8
|
+
injectionType: 'header',
|
|
9
|
+
headerName: 'Authorization',
|
|
10
|
+
valuePrefix: 'Bearer ',
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// evaluateRequest
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
describe('evaluateRequest', () => {
|
|
20
|
+
test('returns unauthenticated when no credential IDs', () => {
|
|
21
|
+
const result = evaluateRequest('api.example.com', '/', [], new Map());
|
|
22
|
+
expect(result.kind).toBe('unauthenticated');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('returns missing when credential has no templates', () => {
|
|
26
|
+
const result = evaluateRequest('api.example.com', '/', ['cred-1'], new Map());
|
|
27
|
+
expect(result.kind).toBe('missing');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('returns missing when no template matches the host', () => {
|
|
31
|
+
const templates = new Map<string, CredentialInjectionTemplate[]>();
|
|
32
|
+
templates.set('cred-1', [makeTemplate({ hostPattern: '*.other.com' })]);
|
|
33
|
+
const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
|
|
34
|
+
expect(result.kind).toBe('missing');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('returns matched for exact host match', () => {
|
|
38
|
+
const tpl = makeTemplate({ hostPattern: 'api.example.com' });
|
|
39
|
+
const templates = new Map<string, CredentialInjectionTemplate[]>();
|
|
40
|
+
templates.set('cred-1', [tpl]);
|
|
41
|
+
const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
|
|
42
|
+
expect(result.kind).toBe('matched');
|
|
43
|
+
if (result.kind === 'matched') {
|
|
44
|
+
expect(result.credentialId).toBe('cred-1');
|
|
45
|
+
expect(result.template).toBe(tpl);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('returns matched for wildcard host match', () => {
|
|
50
|
+
const tpl = makeTemplate({ hostPattern: '*.example.com' });
|
|
51
|
+
const templates = new Map<string, CredentialInjectionTemplate[]>();
|
|
52
|
+
templates.set('cred-1', [tpl]);
|
|
53
|
+
const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
|
|
54
|
+
expect(result.kind).toBe('matched');
|
|
55
|
+
if (result.kind === 'matched') {
|
|
56
|
+
expect(result.credentialId).toBe('cred-1');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('prefers exact match over wildcard for same credential', () => {
|
|
61
|
+
const wildcard = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-Wildcard' });
|
|
62
|
+
const exact = makeTemplate({ hostPattern: 'api.example.com', headerName: 'X-Exact' });
|
|
63
|
+
const templates = new Map<string, CredentialInjectionTemplate[]>();
|
|
64
|
+
templates.set('cred-1', [wildcard, exact]);
|
|
65
|
+
const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
|
|
66
|
+
expect(result.kind).toBe('matched');
|
|
67
|
+
if (result.kind === 'matched') {
|
|
68
|
+
expect(result.template.headerName).toBe('X-Exact');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('returns ambiguous for same-specificity tie within one credential', () => {
|
|
73
|
+
const tpl1 = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-One' });
|
|
74
|
+
const tpl2 = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-Two' });
|
|
75
|
+
const templates = new Map<string, CredentialInjectionTemplate[]>();
|
|
76
|
+
templates.set('cred-1', [tpl1, tpl2]);
|
|
77
|
+
const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
|
|
78
|
+
expect(result.kind).toBe('ambiguous');
|
|
79
|
+
if (result.kind === 'ambiguous') {
|
|
80
|
+
expect(result.candidates).toHaveLength(2);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('returns ambiguous for cross-credential match', () => {
|
|
85
|
+
const tpl1 = makeTemplate({ hostPattern: '*.example.com' });
|
|
86
|
+
const tpl2 = makeTemplate({ hostPattern: '*.example.com' });
|
|
87
|
+
const templates = new Map<string, CredentialInjectionTemplate[]>();
|
|
88
|
+
templates.set('cred-1', [tpl1]);
|
|
89
|
+
templates.set('cred-2', [tpl2]);
|
|
90
|
+
const result = evaluateRequest('api.example.com', '/', ['cred-1', 'cred-2'], templates);
|
|
91
|
+
expect(result.kind).toBe('ambiguous');
|
|
92
|
+
if (result.kind === 'ambiguous') {
|
|
93
|
+
expect(result.candidates).toHaveLength(2);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('skips query-type injection templates', () => {
|
|
98
|
+
const tpl = makeTemplate({
|
|
99
|
+
hostPattern: '*.example.com',
|
|
100
|
+
injectionType: 'query',
|
|
101
|
+
queryParamName: 'api_key',
|
|
102
|
+
headerName: undefined,
|
|
103
|
+
});
|
|
104
|
+
const templates = new Map<string, CredentialInjectionTemplate[]>();
|
|
105
|
+
templates.set('cred-1', [tpl]);
|
|
106
|
+
const result = evaluateRequest('api.example.com', '/', ['cred-1'], templates);
|
|
107
|
+
expect(result.kind).toBe('missing');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('wildcard with includeApexForWildcard matches bare domain', () => {
|
|
111
|
+
const tpl = makeTemplate({ hostPattern: '*.example.com' });
|
|
112
|
+
const templates = new Map<string, CredentialInjectionTemplate[]>();
|
|
113
|
+
templates.set('cred-1', [tpl]);
|
|
114
|
+
const result = evaluateRequest('example.com', '/', ['cred-1'], templates);
|
|
115
|
+
expect(result.kind).toBe('matched');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// evaluateRequestWithApproval
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
describe('evaluateRequestWithApproval', () => {
|
|
124
|
+
test('passes through matched decisions', () => {
|
|
125
|
+
const tpl = makeTemplate({ hostPattern: 'api.example.com' });
|
|
126
|
+
const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
|
|
127
|
+
sessionTemplates.set('cred-1', [tpl]);
|
|
128
|
+
const result = evaluateRequestWithApproval(
|
|
129
|
+
'api.example.com', null, '/',
|
|
130
|
+
['cred-1'], sessionTemplates, [],
|
|
131
|
+
);
|
|
132
|
+
expect(result.kind).toBe('matched');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('passes through ambiguous decisions', () => {
|
|
136
|
+
const tpl1 = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-One' });
|
|
137
|
+
const tpl2 = makeTemplate({ hostPattern: '*.example.com', headerName: 'X-Two' });
|
|
138
|
+
const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
|
|
139
|
+
sessionTemplates.set('cred-1', [tpl1, tpl2]);
|
|
140
|
+
const result = evaluateRequestWithApproval(
|
|
141
|
+
'api.example.com', null, '/',
|
|
142
|
+
['cred-1'], sessionTemplates, [],
|
|
143
|
+
);
|
|
144
|
+
expect(result.kind).toBe('ambiguous');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('returns ask_missing_credential when known templates match', () => {
|
|
148
|
+
const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
|
|
149
|
+
const allKnown = [makeTemplate({ hostPattern: '*.example.com' })];
|
|
150
|
+
const result = evaluateRequestWithApproval(
|
|
151
|
+
'api.example.com', 443, '/api',
|
|
152
|
+
['cred-1'], sessionTemplates, allKnown,
|
|
153
|
+
);
|
|
154
|
+
expect(result.kind).toBe('ask_missing_credential');
|
|
155
|
+
if (result.kind === 'ask_missing_credential') {
|
|
156
|
+
expect(result.target.hostname).toBe('api.example.com');
|
|
157
|
+
expect(result.target.port).toBe(443);
|
|
158
|
+
expect(result.target.path).toBe('/api');
|
|
159
|
+
expect(result.matchingPatterns).toContain('*.example.com');
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('deduplicates matching patterns', () => {
|
|
164
|
+
const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
|
|
165
|
+
const allKnown = [
|
|
166
|
+
makeTemplate({ hostPattern: '*.example.com', headerName: 'X-One' }),
|
|
167
|
+
makeTemplate({ hostPattern: '*.example.com', headerName: 'X-Two' }),
|
|
168
|
+
];
|
|
169
|
+
const result = evaluateRequestWithApproval(
|
|
170
|
+
'api.example.com', null, '/',
|
|
171
|
+
['cred-1'], sessionTemplates, allKnown,
|
|
172
|
+
);
|
|
173
|
+
expect(result.kind).toBe('ask_missing_credential');
|
|
174
|
+
if (result.kind === 'ask_missing_credential') {
|
|
175
|
+
expect(result.matchingPatterns).toEqual(['*.example.com']);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('returns ask_unauthenticated when no known templates match and no credentials', () => {
|
|
180
|
+
const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
|
|
181
|
+
const result = evaluateRequestWithApproval(
|
|
182
|
+
'unknown.example.com', null, '/',
|
|
183
|
+
[], sessionTemplates, [],
|
|
184
|
+
);
|
|
185
|
+
expect(result.kind).toBe('ask_unauthenticated');
|
|
186
|
+
if (result.kind === 'ask_unauthenticated') {
|
|
187
|
+
expect(result.target.hostname).toBe('unknown.example.com');
|
|
188
|
+
expect(result.target.scheme).toBe('https');
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('returns ask_unauthenticated for unknown host even with known templates for other hosts', () => {
|
|
193
|
+
const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
|
|
194
|
+
const allKnown = [makeTemplate({ hostPattern: '*.other.com' })];
|
|
195
|
+
const result = evaluateRequestWithApproval(
|
|
196
|
+
'unknown.example.com', null, '/',
|
|
197
|
+
[], sessionTemplates, allKnown,
|
|
198
|
+
);
|
|
199
|
+
expect(result.kind).toBe('ask_unauthenticated');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('skips query templates when scanning allKnownTemplates', () => {
|
|
203
|
+
const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
|
|
204
|
+
const allKnown = [makeTemplate({
|
|
205
|
+
hostPattern: '*.example.com',
|
|
206
|
+
injectionType: 'query',
|
|
207
|
+
queryParamName: 'key',
|
|
208
|
+
headerName: undefined,
|
|
209
|
+
})];
|
|
210
|
+
const result = evaluateRequestWithApproval(
|
|
211
|
+
'api.example.com', null, '/',
|
|
212
|
+
['cred-1'], sessionTemplates, allKnown,
|
|
213
|
+
);
|
|
214
|
+
// Query templates are skipped, so no pattern matches → ask_unauthenticated
|
|
215
|
+
// But credentialIds is non-empty so base is 'missing' → then no known header templates → ask_unauthenticated
|
|
216
|
+
// Actually: credentialIds=['cred-1'] but sessionTemplates is empty → base='missing'
|
|
217
|
+
// allKnown only has query type → no header matches → falls through to ask_unauthenticated
|
|
218
|
+
// Wait: base is 'missing' (not unauthenticated), and uniquePatterns.length===0
|
|
219
|
+
// For 'missing' with no matching patterns, the function returns ask_unauthenticated
|
|
220
|
+
expect(result.kind).toBe('ask_unauthenticated');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('uses provided scheme parameter', () => {
|
|
224
|
+
const sessionTemplates = new Map<string, CredentialInjectionTemplate[]>();
|
|
225
|
+
const result = evaluateRequestWithApproval(
|
|
226
|
+
'unknown.example.com', 80, '/',
|
|
227
|
+
[], sessionTemplates, [], 'http',
|
|
228
|
+
);
|
|
229
|
+
expect(result.kind).toBe('ask_unauthenticated');
|
|
230
|
+
if (result.kind === 'ask_unauthenticated') {
|
|
231
|
+
expect(result.target.scheme).toBe('http');
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { routeConnection } from '../router.js';
|
|
3
|
+
import type { CredentialInjectionTemplate } from '../../../credentials/policy-types.js';
|
|
4
|
+
|
|
5
|
+
function makeTemplate(overrides: Partial<CredentialInjectionTemplate> = {}): CredentialInjectionTemplate {
|
|
6
|
+
return {
|
|
7
|
+
hostPattern: '*.example.com',
|
|
8
|
+
injectionType: 'header',
|
|
9
|
+
headerName: 'Authorization',
|
|
10
|
+
valuePrefix: 'Bearer ',
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('routeConnection', () => {
|
|
16
|
+
test('tunnels when no credentials', () => {
|
|
17
|
+
const result = routeConnection('api.example.com', 443, [], new Map());
|
|
18
|
+
expect(result.action).toBe('tunnel');
|
|
19
|
+
expect(result.reason).toBe('tunnel:no_credentials');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('tunnels when credentials exist but no template matches', () => {
|
|
23
|
+
const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
|
|
24
|
+
templates.set('cred-1', [makeTemplate({ hostPattern: '*.other.com' })]);
|
|
25
|
+
const result = routeConnection('api.example.com', 443, ['cred-1'], templates);
|
|
26
|
+
expect(result.action).toBe('tunnel');
|
|
27
|
+
expect(result.reason).toBe('tunnel:no_rewrite');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('tunnels when credential has no templates in map', () => {
|
|
31
|
+
const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
|
|
32
|
+
const result = routeConnection('api.example.com', 443, ['cred-1'], templates);
|
|
33
|
+
expect(result.action).toBe('tunnel');
|
|
34
|
+
expect(result.reason).toBe('tunnel:no_rewrite');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('MITMs when wildcard template matches', () => {
|
|
38
|
+
const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
|
|
39
|
+
templates.set('cred-1', [makeTemplate({ hostPattern: '*.example.com' })]);
|
|
40
|
+
const result = routeConnection('api.example.com', 443, ['cred-1'], templates);
|
|
41
|
+
expect(result.action).toBe('mitm');
|
|
42
|
+
expect(result.reason).toBe('mitm:credential_injection');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('MITMs when exact template matches', () => {
|
|
46
|
+
const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
|
|
47
|
+
templates.set('cred-1', [makeTemplate({ hostPattern: 'api.example.com' })]);
|
|
48
|
+
const result = routeConnection('api.example.com', 443, ['cred-1'], templates);
|
|
49
|
+
expect(result.action).toBe('mitm');
|
|
50
|
+
expect(result.reason).toBe('mitm:credential_injection');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('MITMs when any credential matches (first wins)', () => {
|
|
54
|
+
const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
|
|
55
|
+
templates.set('cred-1', [makeTemplate({ hostPattern: '*.other.com' })]);
|
|
56
|
+
templates.set('cred-2', [makeTemplate({ hostPattern: '*.example.com' })]);
|
|
57
|
+
const result = routeConnection('api.example.com', 443, ['cred-1', 'cred-2'], templates);
|
|
58
|
+
expect(result.action).toBe('mitm');
|
|
59
|
+
expect(result.reason).toBe('mitm:credential_injection');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('wildcard matches bare apex domain with includeApexForWildcard', () => {
|
|
63
|
+
const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
|
|
64
|
+
templates.set('cred-1', [makeTemplate({ hostPattern: '*.example.com' })]);
|
|
65
|
+
const result = routeConnection('example.com', 443, ['cred-1'], templates);
|
|
66
|
+
expect(result.action).toBe('mitm');
|
|
67
|
+
expect(result.reason).toBe('mitm:credential_injection');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('case-insensitive matching', () => {
|
|
71
|
+
const templates = new Map<string, readonly CredentialInjectionTemplate[]>();
|
|
72
|
+
templates.set('cred-1', [makeTemplate({ hostPattern: '*.EXAMPLE.COM' })]);
|
|
73
|
+
const result = routeConnection('API.example.com', 443, ['cred-1'], templates);
|
|
74
|
+
expect(result.action).toBe('mitm');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -298,12 +298,21 @@ function extractFirstMatch(text: string, regex: RegExp, captureGroup = 1): strin
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
function extractHtmlMetadata(html: string): { title?: string; description?: string } {
|
|
301
|
-
|
|
301
|
+
// Only search the <head> section (or first 50KB) to avoid catastrophic
|
|
302
|
+
// regex backtracking on large HTML documents.
|
|
303
|
+
// Strip <script> blocks first so that a literal "</head>" inside a script
|
|
304
|
+
// doesn't cause a false match that truncates the search region prematurely.
|
|
305
|
+
const candidate = html.slice(0, 200_000);
|
|
306
|
+
const stripped = candidate.replace(/<script[\s>][\s\S]*?<\/script>/gi, '');
|
|
307
|
+
const headEnd = stripped.search(/<\/head[\s>]/i);
|
|
308
|
+
const searchRegion = headEnd > 0 ? stripped.slice(0, headEnd + 10) : stripped.slice(0, 50_000);
|
|
309
|
+
|
|
310
|
+
const title = extractFirstMatch(searchRegion, /<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
302
311
|
const description =
|
|
303
|
-
extractFirstMatch(
|
|
304
|
-
?? extractFirstMatch(
|
|
305
|
-
?? extractFirstMatch(
|
|
306
|
-
?? extractFirstMatch(
|
|
312
|
+
extractFirstMatch(searchRegion, /<meta\s+[^>]*name=(['"])description\1[^>]*content=(['"])([\s\S]*?)\2[^>]*>/i, 3)
|
|
313
|
+
?? extractFirstMatch(searchRegion, /<meta\s+[^>]*content=(['"])([\s\S]*?)\1[^>]*name=(['"])description\3[^>]*>/i, 2)
|
|
314
|
+
?? extractFirstMatch(searchRegion, /<meta\s+[^>]*property=(['"])og:description\1[^>]*content=(['"])([\s\S]*?)\2[^>]*>/i, 3)
|
|
315
|
+
?? extractFirstMatch(searchRegion, /<meta\s+[^>]*content=(['"])([\s\S]*?)\1[^>]*property=(['"])og:description\3[^>]*>/i, 2);
|
|
307
316
|
|
|
308
317
|
return { title, description };
|
|
309
318
|
}
|
|
@@ -437,7 +446,10 @@ export async function executeWebFetch(
|
|
|
437
446
|
const safeRequestedUrl = sanitizeUrlForOutput(parsedUrl);
|
|
438
447
|
|
|
439
448
|
const controller = new AbortController();
|
|
440
|
-
const timeoutHandle = setTimeout(() =>
|
|
449
|
+
const timeoutHandle = setTimeout(() => {
|
|
450
|
+
log.warn({ url: safeRequestedUrl, timeoutSeconds }, 'Web fetch timeout fired, aborting');
|
|
451
|
+
controller.abort();
|
|
452
|
+
}, timeoutSeconds * 1000);
|
|
441
453
|
|
|
442
454
|
try {
|
|
443
455
|
log.debug({ url: safeRequestedUrl, timeoutSeconds, maxChars, startIndex, rawMode }, 'Fetching webpage');
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import './playbook-delete.js';
|
|
1
|
+
export { executePlaybookCreate } from './playbook-create.js';
|
|
2
|
+
export { executePlaybookList } from './playbook-list.js';
|
|
3
|
+
export { executePlaybookUpdate } from './playbook-update.js';
|
|
4
|
+
export { executePlaybookDelete } from './playbook-delete.js';
|