negotium 0.3.13 → 0.4.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 +2 -6
- package/dist/agent-helpers.js +328 -185
- package/dist/agent-helpers.js.map +9 -8
- package/dist/{chunk-0ynjwr50.js → chunk-zq2tcq4k.js} +12 -29
- package/dist/{chunk-0ynjwr50.js.map → chunk-zq2tcq4k.js.map} +4 -4
- package/dist/hosted-agent.js +207 -64
- package/dist/hosted-agent.js.map +8 -7
- package/dist/main.js +753 -886
- package/dist/main.js.map +13 -13
- package/dist/mcp-factories.js +300 -468
- package/dist/mcp-factories.js.map +9 -10
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +2 -2
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/agents/codex-provider.ts +33 -8
- package/dist/runtime/src/agents/codex-vault-hook-bridge.ts +192 -0
- package/dist/runtime/src/agents/codex-vault-hook.mjs +48 -0
- package/dist/runtime/src/agents/execution-host.ts +1 -14
- package/dist/runtime/src/agents/public-helpers.ts +0 -9
- package/dist/runtime/src/agents/vault-tool-policy.ts +9 -51
- package/dist/runtime/src/mcp/factories/index.ts +1 -8
- package/dist/runtime/src/mcp/factories/vault.ts +6 -104
- package/dist/runtime/src/mcp/vault-server.ts +4 -20
- package/dist/runtime/src/prompts/sessions/_shared-tools.md +1 -1
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/agents/codex-vault-hook-bridge.d.ts +27 -0
- package/dist/types/packages/core/src/agents/execution-host.d.ts +0 -2
- package/dist/types/packages/core/src/agents/public-helpers.d.ts +0 -1
- package/dist/types/packages/core/src/agents/vault-tool-policy.d.ts +1 -14
- package/dist/types/packages/core/src/mcp/factories/index.d.ts +1 -3
- package/dist/types/packages/core/src/mcp/factories/vault.d.ts +4 -13
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/dist/runtime/src/mcp/factories/vault-host.ts +0 -8
- package/dist/runtime/src/mcp/vault-http.ts +0 -235
- package/dist/runtime/src/mcp/vault-run.ts +0 -154
- package/dist/types/packages/core/src/mcp/factories/vault-host.d.ts +0 -7
- package/dist/types/packages/core/src/mcp/vault-http.d.ts +0 -23
- package/dist/types/packages/core/src/mcp/vault-run.d.ts +0 -19
|
@@ -1,44 +1,21 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { executeVaultRun, type VaultRunRequest } from "#mcp/vault-run";
|
|
5
|
-
import { mcpError, mcpOk } from "../mcp-helpers";
|
|
6
|
-
import type { VaultCredentialHost } from "./vault-host";
|
|
2
|
+
import type { VaultEntry } from "#storage/vault";
|
|
3
|
+
import { mcpOk } from "../mcp-helpers";
|
|
7
4
|
|
|
8
5
|
export interface VaultMcpContext {
|
|
9
6
|
userId?: string;
|
|
10
|
-
listOnly?: boolean;
|
|
11
|
-
httpOnly?: boolean;
|
|
12
|
-
cwd?: string;
|
|
13
7
|
}
|
|
14
8
|
|
|
15
|
-
export interface
|
|
16
|
-
|
|
17
|
-
userId: string,
|
|
18
|
-
request: VaultRunRequest,
|
|
19
|
-
host: VaultCredentialHost,
|
|
20
|
-
): ReturnType<typeof executeVaultRun>;
|
|
21
|
-
http?(
|
|
22
|
-
userId: string,
|
|
23
|
-
request: VaultHttpRequest,
|
|
24
|
-
host: VaultCredentialHost,
|
|
25
|
-
): ReturnType<typeof executeVaultHttpRequest>;
|
|
9
|
+
export interface VaultMcpHost {
|
|
10
|
+
list(userId: string): readonly VaultEntry[];
|
|
26
11
|
}
|
|
27
12
|
|
|
28
|
-
export function createVaultMcpServer(
|
|
29
|
-
context: VaultMcpContext,
|
|
30
|
-
host: VaultCredentialHost,
|
|
31
|
-
executors: VaultMcpExecutors = {},
|
|
32
|
-
): McpServer {
|
|
13
|
+
export function createVaultMcpServer(context: VaultMcpContext, host: VaultMcpHost): McpServer {
|
|
33
14
|
const server = new McpServer({ name: "vault", version: "1.0.0" });
|
|
34
|
-
const run = executors.run ?? executeVaultRun;
|
|
35
|
-
const http = executors.http ?? executeVaultHttpRequest;
|
|
36
15
|
|
|
37
16
|
server.tool(
|
|
38
17
|
"vault_list",
|
|
39
|
-
|
|
40
|
-
? "List the user's Vault keys and descriptions without exposing values. Use vault_http_request for HTTPS APIs that need a credential."
|
|
41
|
-
: "List the user's Vault keys and descriptions without exposing values. Use vault_http_request for APIs and vault_run for shell/CLI work that needs a credential.",
|
|
18
|
+
"List the user's Vault keys and descriptions without exposing values. Use {{KEY}} placeholders directly in supported transient tool inputs.",
|
|
42
19
|
{},
|
|
43
20
|
() => {
|
|
44
21
|
if (!context.userId) return mcpOk("(vault unavailable: no user context)");
|
|
@@ -51,80 +28,5 @@ export function createVaultMcpServer(
|
|
|
51
28
|
},
|
|
52
29
|
);
|
|
53
30
|
|
|
54
|
-
if (!context.listOnly && !context.httpOnly) {
|
|
55
|
-
server.tool(
|
|
56
|
-
"vault_run",
|
|
57
|
-
"Run a shell command containing {{KEY}} references inside Otium's credential broker. Expanded command input never reaches the model/provider, and stdout/stderr are redacted before return. Prefer vault_http_request for HTTP APIs.",
|
|
58
|
-
{
|
|
59
|
-
command: z
|
|
60
|
-
.string()
|
|
61
|
-
.min(1)
|
|
62
|
-
.max(64 * 1024),
|
|
63
|
-
timeout_ms: z.number().int().min(1_000).max(600_000).optional(),
|
|
64
|
-
max_output_bytes: z
|
|
65
|
-
.number()
|
|
66
|
-
.int()
|
|
67
|
-
.min(1_024)
|
|
68
|
-
.max(2 * 1024 * 1024)
|
|
69
|
-
.optional(),
|
|
70
|
-
},
|
|
71
|
-
async ({ command, timeout_ms, max_output_bytes }) => {
|
|
72
|
-
if (!context.userId) return mcpError("Vault unavailable: no user context");
|
|
73
|
-
const result = await run(
|
|
74
|
-
context.userId,
|
|
75
|
-
{
|
|
76
|
-
command,
|
|
77
|
-
timeoutMs: timeout_ms,
|
|
78
|
-
maxOutputBytes: max_output_bytes,
|
|
79
|
-
cwd: context.cwd,
|
|
80
|
-
},
|
|
81
|
-
host,
|
|
82
|
-
);
|
|
83
|
-
return result.error && result.exitCode === null
|
|
84
|
-
? mcpError(result.error)
|
|
85
|
-
: mcpOk(JSON.stringify(result, null, 2));
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (!context.listOnly)
|
|
91
|
-
server.tool(
|
|
92
|
-
"vault_http_request",
|
|
93
|
-
"Make an HTTPS request with {{KEY}} references resolved inside Otium. Put secrets in headers or body, never in the URL. The expanded request is not returned; the response is redacted before the model sees it.",
|
|
94
|
-
{
|
|
95
|
-
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
|
|
96
|
-
url: z.string().url().describe("Absolute HTTPS URL without Vault placeholders"),
|
|
97
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
98
|
-
body: z.string().optional(),
|
|
99
|
-
timeout_ms: z.number().int().min(1_000).max(120_000).optional(),
|
|
100
|
-
max_response_bytes: z
|
|
101
|
-
.number()
|
|
102
|
-
.int()
|
|
103
|
-
.min(1_024)
|
|
104
|
-
.max(1024 * 1024)
|
|
105
|
-
.optional(),
|
|
106
|
-
},
|
|
107
|
-
async ({ method, url, headers, body, timeout_ms, max_response_bytes }) => {
|
|
108
|
-
if (!context.userId) return mcpError("Vault unavailable: no user context");
|
|
109
|
-
const result = await http(
|
|
110
|
-
context.userId,
|
|
111
|
-
{
|
|
112
|
-
method,
|
|
113
|
-
url,
|
|
114
|
-
headers,
|
|
115
|
-
body,
|
|
116
|
-
timeoutMs: timeout_ms,
|
|
117
|
-
maxResponseBytes: max_response_bytes,
|
|
118
|
-
},
|
|
119
|
-
host,
|
|
120
|
-
);
|
|
121
|
-
return result.error ? mcpError(result.error) : mcpOk(JSON.stringify(result, null, 2));
|
|
122
|
-
},
|
|
123
|
-
);
|
|
124
|
-
|
|
125
31
|
return server;
|
|
126
32
|
}
|
|
127
|
-
|
|
128
|
-
export type { VaultHttpRequest, VaultHttpResult } from "#mcp/vault-http";
|
|
129
|
-
export type { VaultRunRequest, VaultRunResult } from "#mcp/vault-run";
|
|
130
|
-
export type { VaultCredentialHost } from "./vault-host";
|
|
@@ -1,28 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./stdio-protect";
|
|
3
|
-
import { createVaultMcpServer, type
|
|
3
|
+
import { createVaultMcpServer, type VaultMcpHost } from "#mcp/factories/vault";
|
|
4
4
|
import { connectStdio, parseUserIdArg } from "#mcp/mcp-helpers";
|
|
5
|
-
import {
|
|
6
|
-
import { redactVaultSecrets, vaultList, vaultSubstituteDetailed } from "#storage/vault";
|
|
5
|
+
import { vaultList } from "#storage/vault";
|
|
7
6
|
|
|
8
7
|
const args = process.argv.slice(2);
|
|
9
|
-
const host:
|
|
8
|
+
const host: VaultMcpHost = {
|
|
10
9
|
list: vaultList,
|
|
11
|
-
substitute: vaultSubstituteDetailed,
|
|
12
|
-
redact: redactVaultSecrets,
|
|
13
|
-
log(level, details, message) {
|
|
14
|
-
logger[level](details, message);
|
|
15
|
-
},
|
|
16
10
|
};
|
|
17
11
|
|
|
18
|
-
await connectStdio(
|
|
19
|
-
createVaultMcpServer(
|
|
20
|
-
{
|
|
21
|
-
userId: parseUserIdArg(args),
|
|
22
|
-
listOnly: args.includes("--list-only=true"),
|
|
23
|
-
httpOnly: args.includes("--http-only=true"),
|
|
24
|
-
cwd: process.cwd(),
|
|
25
|
-
},
|
|
26
|
-
host,
|
|
27
|
-
),
|
|
28
|
-
);
|
|
12
|
+
await connectStdio(createVaultMcpServer({ userId: parseUserIdArg(args) }, host));
|
|
@@ -11,7 +11,7 @@ Use a tool only when it is actually available; otherwise say so instead of prete
|
|
|
11
11
|
- Voice: user voice arrives transcribed; fix misheard proper nouns from context.
|
|
12
12
|
- Skills: when a task looks unfamiliar, slow, or error-prone, `skill_query` first; save or update a reusable solution with `skill_save`.
|
|
13
13
|
- Memory: when a Memory section is injected, use it for past context; `wiki_query` for deeper recall.
|
|
14
|
-
- Vault: use `{{KEY}}` directly in
|
|
14
|
+
- Vault: use `{{KEY}}` directly in supported transient tool inputs for every provider; never ask the user to paste secrets into chat.
|
|
15
15
|
- Background shell: use background-bash only for independent commands expected to outlive the current turn (typically over 2 minutes). Run ordinary builds, tests, and commands needed for the next step inline and wait for them; do not background work merely to avoid waiting. Results are injected automatically, so do not poll unless live output is required.
|
|
16
16
|
- Scheduled tasks: manage with `cron-manager` tools; scripts must already exist (`cron_list_scripts`). Jobs in one topic share a Cron conversation, so `cron_reset` clears the topic's whole Cron context, not one job.
|
|
17
17
|
- Heavy work (large files, video encode, big crawls, browser automation): check `get_system_health` first and back off under resource pressure.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const NEGOTIUM_VERSION = "0.
|
|
1
|
+
export const NEGOTIUM_VERSION = "0.4.0";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface CodexPreToolUseInput {
|
|
2
|
+
hook_event_name?: string;
|
|
3
|
+
tool_name: string;
|
|
4
|
+
tool_input: unknown;
|
|
5
|
+
}
|
|
6
|
+
export type CodexPreToolUseOutput = Record<string, unknown>;
|
|
7
|
+
export interface CodexVaultHookOperations {
|
|
8
|
+
referencesSensitiveStorage(value: unknown): boolean;
|
|
9
|
+
substitute(userId: string, value: string): string;
|
|
10
|
+
}
|
|
11
|
+
export declare function evaluateCodexVaultPreToolUse(input: CodexPreToolUseInput, userId: string, operations: CodexVaultHookOperations): CodexPreToolUseOutput;
|
|
12
|
+
export interface CodexVaultHookBridge {
|
|
13
|
+
codexPathOverride: string;
|
|
14
|
+
hooks: {
|
|
15
|
+
PreToolUse: Array<{
|
|
16
|
+
matcher: string;
|
|
17
|
+
hooks: Array<{
|
|
18
|
+
type: string;
|
|
19
|
+
command: string;
|
|
20
|
+
timeout: number;
|
|
21
|
+
statusMessage: string;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
};
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export declare function createCodexVaultHookBridge(userId: string): Promise<CodexVaultHookBridge>;
|
|
@@ -12,7 +12,6 @@ export interface AgentExecutionHost {
|
|
|
12
12
|
redactVaultSecrets(userId: string, value: string): string;
|
|
13
13
|
substituteVaultSecrets(userId: string, value: string): string;
|
|
14
14
|
referencesRuntimeSecretStorage(value: unknown): boolean;
|
|
15
|
-
shouldRedirectVaultTool(userId: string, toolName: string, input: unknown): boolean;
|
|
16
15
|
claudeCodeExecutablePath(): string | undefined;
|
|
17
16
|
codexAuthFilePath(): string;
|
|
18
17
|
transformQueryOptions?(opts: AgentQueryOptions): AgentQueryOptions;
|
|
@@ -27,7 +26,6 @@ export declare function hostedMcpServers(opts: AgentQueryOptions): Record<string
|
|
|
27
26
|
export declare function redactHostedSecrets(userId: string, value: string): string;
|
|
28
27
|
export declare function substituteHostedSecrets(userId: string, value: string): string;
|
|
29
28
|
export declare function referencesHostedSecretStorage(value: unknown): boolean;
|
|
30
|
-
export declare function shouldRedirectHostedVaultTool(userId: string, toolName: string, input: unknown): boolean;
|
|
31
29
|
export declare function transformHostedQueryOptions(opts: AgentQueryOptions): AgentQueryOptions;
|
|
32
30
|
export declare function hostedClaudeCodeExecutablePath(): string | undefined;
|
|
33
31
|
export declare function hostedCodexAuthFilePath(): string;
|
|
@@ -14,4 +14,3 @@ export { createSelfConfigCore, DEFAULT_SELF_CONFIG_PRODUCT, type SelfConfigAgent
|
|
|
14
14
|
export { resolveTaskEventScope, type TaskEventHost, type TaskEventScope, withTaskSnapshots, } from "./task-events";
|
|
15
15
|
export { buildNumberedDiffSummary, classifyShellToolName, formatToolUse, type NumberedDiffSummary, summarizeDisplayText, summarizeShellCommand, summarizeToolInput, type ToolCallSummaryInput, type ToolCallSummaryValue, } from "./tool-format";
|
|
16
16
|
export { cleanupTopicRollouts, cleanupTopicRolloutsFromEntries, createTopicLogMaintenance, type PurgeSessionRef, type PurgeTopicLogsOptions, purgeTopicLogs, type RotateTopicLogsOptions, type RotateTopicLogsResult, rotateTopicLogs, type TopicConversationEntry, type TopicLogMaintenance, type TopicLogMaintenanceHost, } from "./topic-cleanup";
|
|
17
|
-
export { createVaultToolPolicy, isVaultBrokerTool, referencesRuntimeSecretStorage, shouldRedirectVaultTool, VAULT_BROKER_REDIRECT_ERROR, type VaultToolPolicy, type VaultToolPolicyHost, } from "./vault-tool-policy";
|
|
@@ -1,15 +1,2 @@
|
|
|
1
1
|
export declare function shouldSubstituteVaultToolInput(toolName: string): boolean;
|
|
2
|
-
export declare
|
|
3
|
-
export interface VaultToolPolicyHost {
|
|
4
|
-
isSensitivePath(path: string): boolean;
|
|
5
|
-
valueReferencesVaultKey(userId: string, value: unknown): boolean;
|
|
6
|
-
}
|
|
7
|
-
export interface VaultToolPolicy {
|
|
8
|
-
isVaultBrokerTool(toolName: string): boolean;
|
|
9
|
-
referencesRuntimeSecretStorage(value: unknown): boolean;
|
|
10
|
-
shouldRedirectVaultTool(userId: string, toolName: string, input: unknown): boolean;
|
|
11
|
-
}
|
|
12
|
-
export declare function createVaultToolPolicy(host: VaultToolPolicyHost): VaultToolPolicy;
|
|
13
|
-
export declare const isVaultBrokerTool: (toolName: string) => boolean;
|
|
14
|
-
export declare const referencesRuntimeSecretStorage: (value: unknown) => boolean;
|
|
15
|
-
export declare const shouldRedirectVaultTool: (userId: string, toolName: string, input: unknown) => boolean;
|
|
2
|
+
export declare function referencesRuntimeSecretStorage(value: unknown): boolean;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export { parseSessionCommContext, type SessionCommContext, type SessionCommContextDefaults, } from "../session-comm/context";
|
|
2
2
|
export { createSessionTargetCatalog, type SessionTarget, type SessionTargetCatalog, type SessionTargetCatalogHost, type SessionTopicEntry, type SessionTopicRow, type ValidateSessionTargetResult, } from "../session-comm/topic-catalog";
|
|
3
|
-
export { executeVaultHttpRequest } from "../vault-http";
|
|
4
|
-
export { executeVaultRun } from "../vault-run";
|
|
5
3
|
export { createWikiMcpServer, type WikiMcpContext, type WikiMcpHost, type WikiSurface, type WikiTopicBrief, } from "../wiki-server";
|
|
6
4
|
export { type AgentHealthMcpContext, createAgentHealthMcpServer, } from "./agent-health";
|
|
7
5
|
export { type CompactionLogMcpContext, createCompactionLogMcpServer, } from "./compaction-log";
|
|
@@ -11,4 +9,4 @@ export { type McpStdioProtectionTarget, protectMcpStdio, } from "./stdio-protect
|
|
|
11
9
|
export { createSystemHealthMcpServer, defaultSystemHealthMcpHost, type SystemHealthMcpHost, type SystemHealthSnapshot, } from "./system-health";
|
|
12
10
|
export { createTaskMcpServer, defaultTaskMcpHost, type TaskMcpContext, type TaskMcpHost, } from "./task";
|
|
13
11
|
export { createTokenStatsMcpServer, defaultTokenStatsMcpHost, type TokenStatsMcpContext, type TokenStatsMcpHost, type TokenStatsSnapshot, } from "./token-stats";
|
|
14
|
-
export { createVaultMcpServer, type
|
|
12
|
+
export { createVaultMcpServer, type VaultMcpContext, type VaultMcpHost, } from "./vault";
|
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import {
|
|
3
|
-
import { executeVaultRun, type VaultRunRequest } from "../vault-run";
|
|
4
|
-
import type { VaultCredentialHost } from "./vault-host";
|
|
2
|
+
import type { VaultEntry } from "../../storage/vault";
|
|
5
3
|
export interface VaultMcpContext {
|
|
6
4
|
userId?: string;
|
|
7
|
-
listOnly?: boolean;
|
|
8
|
-
httpOnly?: boolean;
|
|
9
|
-
cwd?: string;
|
|
10
5
|
}
|
|
11
|
-
export interface
|
|
12
|
-
|
|
13
|
-
http?(userId: string, request: VaultHttpRequest, host: VaultCredentialHost): ReturnType<typeof executeVaultHttpRequest>;
|
|
6
|
+
export interface VaultMcpHost {
|
|
7
|
+
list(userId: string): readonly VaultEntry[];
|
|
14
8
|
}
|
|
15
|
-
export declare function createVaultMcpServer(context: VaultMcpContext, host:
|
|
16
|
-
export type { VaultHttpRequest, VaultHttpResult } from "../vault-http";
|
|
17
|
-
export type { VaultRunRequest, VaultRunResult } from "../vault-run";
|
|
18
|
-
export type { VaultCredentialHost } from "./vault-host";
|
|
9
|
+
export declare function createVaultMcpServer(context: VaultMcpContext, host: VaultMcpHost): McpServer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const NEGOTIUM_VERSION = "0.
|
|
1
|
+
export declare const NEGOTIUM_VERSION = "0.4.0";
|
package/package.json
CHANGED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { VaultEntry, VaultSubstitutionResult } from "#storage/vault";
|
|
2
|
-
|
|
3
|
-
export interface VaultCredentialHost {
|
|
4
|
-
list(userId: string): readonly VaultEntry[];
|
|
5
|
-
substitute(userId: string, text: string): VaultSubstitutionResult;
|
|
6
|
-
redact(userId: string, text: string): string;
|
|
7
|
-
log?(level: "info" | "warn", details: Record<string, unknown>, message: string): void;
|
|
8
|
-
}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
import type { VaultCredentialHost } from "#mcp/factories/vault-host";
|
|
2
|
-
|
|
3
|
-
const SAFE_RESPONSE_HEADERS = new Set([
|
|
4
|
-
"content-type",
|
|
5
|
-
"content-length",
|
|
6
|
-
"location",
|
|
7
|
-
"retry-after",
|
|
8
|
-
"x-ratelimit-limit",
|
|
9
|
-
"x-ratelimit-remaining",
|
|
10
|
-
"x-ratelimit-reset",
|
|
11
|
-
]);
|
|
12
|
-
|
|
13
|
-
const FORBIDDEN_REQUEST_HEADERS = new Set([
|
|
14
|
-
"connection",
|
|
15
|
-
"content-length",
|
|
16
|
-
"host",
|
|
17
|
-
"proxy-authorization",
|
|
18
|
-
"transfer-encoding",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
export interface VaultHttpRequest {
|
|
22
|
-
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
23
|
-
url: string;
|
|
24
|
-
headers?: Record<string, string>;
|
|
25
|
-
body?: string;
|
|
26
|
-
timeoutMs?: number;
|
|
27
|
-
maxResponseBytes?: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface VaultHttpResult {
|
|
31
|
-
ok: boolean;
|
|
32
|
-
status?: number;
|
|
33
|
-
statusText?: string;
|
|
34
|
-
headers?: Record<string, string>;
|
|
35
|
-
body?: string;
|
|
36
|
-
truncated?: boolean;
|
|
37
|
-
error?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function substituteObject(
|
|
41
|
-
userId: string,
|
|
42
|
-
values: Record<string, string>,
|
|
43
|
-
host: VaultCredentialHost,
|
|
44
|
-
): { values: Record<string, string>; usedKeys: string[] } {
|
|
45
|
-
const used = new Set<string>();
|
|
46
|
-
const substituted: Record<string, string> = {};
|
|
47
|
-
for (const [key, value] of Object.entries(values)) {
|
|
48
|
-
if (FORBIDDEN_REQUEST_HEADERS.has(key.toLowerCase())) {
|
|
49
|
-
throw new Error(`Header "${key}" is not allowed`);
|
|
50
|
-
}
|
|
51
|
-
const result = host.substitute(userId, value);
|
|
52
|
-
for (const usedKey of result.usedKeys) used.add(usedKey);
|
|
53
|
-
substituted[key] = result.text;
|
|
54
|
-
}
|
|
55
|
-
return { values: substituted, usedKeys: [...used] };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function safeResponseHeaders(
|
|
59
|
-
userId: string,
|
|
60
|
-
headers: Headers,
|
|
61
|
-
host: VaultCredentialHost,
|
|
62
|
-
): Record<string, string> {
|
|
63
|
-
const output: Record<string, string> = {};
|
|
64
|
-
for (const [key, value] of headers.entries()) {
|
|
65
|
-
if (!SAFE_RESPONSE_HEADERS.has(key.toLowerCase())) continue;
|
|
66
|
-
output[key] = host.redact(userId, value);
|
|
67
|
-
}
|
|
68
|
-
return output;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function readBoundedBody(
|
|
72
|
-
response: Response,
|
|
73
|
-
maxBytes: number,
|
|
74
|
-
): Promise<{ bytes: Uint8Array; truncated: boolean }> {
|
|
75
|
-
if (!response.body) return { bytes: new Uint8Array(), truncated: false };
|
|
76
|
-
const reader = response.body.getReader();
|
|
77
|
-
const chunks: Uint8Array[] = [];
|
|
78
|
-
let keptBytes = 0;
|
|
79
|
-
let truncated = false;
|
|
80
|
-
try {
|
|
81
|
-
while (true) {
|
|
82
|
-
const next = await reader.read();
|
|
83
|
-
if (next.done) break;
|
|
84
|
-
const remaining = maxBytes - keptBytes;
|
|
85
|
-
if (remaining <= 0) {
|
|
86
|
-
truncated = true;
|
|
87
|
-
await reader.cancel();
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
const visible = next.value.subarray(0, remaining);
|
|
91
|
-
chunks.push(visible);
|
|
92
|
-
keptBytes += visible.byteLength;
|
|
93
|
-
if (visible.byteLength < next.value.byteLength) {
|
|
94
|
-
truncated = true;
|
|
95
|
-
await reader.cancel();
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} finally {
|
|
100
|
-
reader.releaseLock();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const bytes = new Uint8Array(keptBytes);
|
|
104
|
-
let offset = 0;
|
|
105
|
-
for (const chunk of chunks) {
|
|
106
|
-
bytes.set(chunk, offset);
|
|
107
|
-
offset += chunk.byteLength;
|
|
108
|
-
}
|
|
109
|
-
return { bytes, truncated };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Execute an HTTPS request inside the credential boundary. The caller supplies
|
|
114
|
-
* only {{KEY}} references; expanded headers/body never leave this function.
|
|
115
|
-
*/
|
|
116
|
-
export async function executeVaultHttpRequest(
|
|
117
|
-
userId: string,
|
|
118
|
-
request: VaultHttpRequest,
|
|
119
|
-
host: VaultCredentialHost,
|
|
120
|
-
fetchImpl: typeof fetch = fetch,
|
|
121
|
-
): Promise<VaultHttpResult> {
|
|
122
|
-
let parsedUrl: URL;
|
|
123
|
-
try {
|
|
124
|
-
parsedUrl = new URL(request.url);
|
|
125
|
-
} catch {
|
|
126
|
-
return { ok: false, error: "url must be an absolute HTTPS URL" };
|
|
127
|
-
}
|
|
128
|
-
if (parsedUrl.protocol !== "https:") {
|
|
129
|
-
return {
|
|
130
|
-
ok: false,
|
|
131
|
-
error: "Vault credentials may only be sent over HTTPS",
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (parsedUrl.username || parsedUrl.password || /\{\{[^}]+\}\}/.test(request.url)) {
|
|
135
|
-
return {
|
|
136
|
-
ok: false,
|
|
137
|
-
error: "Keep Vault placeholders out of URLs; put credentials in headers or body",
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
let headers: Record<string, string>;
|
|
142
|
-
let headerKeys: string[];
|
|
143
|
-
try {
|
|
144
|
-
const substituted = substituteObject(userId, request.headers ?? {}, host);
|
|
145
|
-
headers = substituted.values;
|
|
146
|
-
headerKeys = substituted.usedKeys;
|
|
147
|
-
} catch (error) {
|
|
148
|
-
return {
|
|
149
|
-
ok: false,
|
|
150
|
-
error: error instanceof Error ? error.message : String(error),
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const bodyResult =
|
|
155
|
-
request.body === undefined
|
|
156
|
-
? { text: undefined, usedKeys: [] as string[] }
|
|
157
|
-
: host.substitute(userId, request.body);
|
|
158
|
-
const usedKeys = [...new Set([...headerKeys, ...bodyResult.usedKeys])].sort();
|
|
159
|
-
if (usedKeys.length === 0) {
|
|
160
|
-
return {
|
|
161
|
-
ok: false,
|
|
162
|
-
error: "No valid Vault placeholder was found in headers or body",
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
if (request.method === "GET" && request.body !== undefined) {
|
|
166
|
-
return {
|
|
167
|
-
ok: false,
|
|
168
|
-
error: "GET requests cannot include a credential-bearing body",
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const controller = new AbortController();
|
|
173
|
-
const timeoutMs = Math.min(Math.max(request.timeoutMs ?? 30_000, 1_000), 120_000);
|
|
174
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
175
|
-
const startedAt = Date.now();
|
|
176
|
-
try {
|
|
177
|
-
const response = await fetchImpl(parsedUrl, {
|
|
178
|
-
method: request.method,
|
|
179
|
-
headers,
|
|
180
|
-
body: request.method === "GET" ? undefined : bodyResult.text,
|
|
181
|
-
redirect: "manual",
|
|
182
|
-
signal: controller.signal,
|
|
183
|
-
});
|
|
184
|
-
const maxBytes = Math.min(Math.max(request.maxResponseBytes ?? 256 * 1024, 1_024), 1024 * 1024);
|
|
185
|
-
const { bytes, truncated } = await readBoundedBody(response, maxBytes);
|
|
186
|
-
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
187
|
-
const textual =
|
|
188
|
-
contentType.startsWith("text/") ||
|
|
189
|
-
contentType.includes("json") ||
|
|
190
|
-
contentType.includes("xml") ||
|
|
191
|
-
contentType.includes("javascript") ||
|
|
192
|
-
contentType === "";
|
|
193
|
-
const body = textual
|
|
194
|
-
? host.redact(userId, new TextDecoder().decode(bytes))
|
|
195
|
-
: `[binary response omitted: ${bytes.byteLength} bytes, ${contentType || "unknown content type"}]`;
|
|
196
|
-
|
|
197
|
-
host.log?.(
|
|
198
|
-
"info",
|
|
199
|
-
{
|
|
200
|
-
userId,
|
|
201
|
-
vaultKeys: usedKeys,
|
|
202
|
-
host: parsedUrl.hostname,
|
|
203
|
-
method: request.method,
|
|
204
|
-
status: response.status,
|
|
205
|
-
durationMs: Date.now() - startedAt,
|
|
206
|
-
},
|
|
207
|
-
"vault credential used",
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
ok: response.ok,
|
|
212
|
-
status: response.status,
|
|
213
|
-
statusText: response.statusText,
|
|
214
|
-
headers: safeResponseHeaders(userId, response.headers, host),
|
|
215
|
-
body,
|
|
216
|
-
truncated,
|
|
217
|
-
};
|
|
218
|
-
} catch (error) {
|
|
219
|
-
const raw = error instanceof Error ? error.message : String(error);
|
|
220
|
-
host.log?.(
|
|
221
|
-
"warn",
|
|
222
|
-
{
|
|
223
|
-
userId,
|
|
224
|
-
vaultKeys: usedKeys,
|
|
225
|
-
host: parsedUrl.hostname,
|
|
226
|
-
method: request.method,
|
|
227
|
-
durationMs: Date.now() - startedAt,
|
|
228
|
-
},
|
|
229
|
-
"vault credential request failed",
|
|
230
|
-
);
|
|
231
|
-
return { ok: false, error: host.redact(userId, raw) };
|
|
232
|
-
} finally {
|
|
233
|
-
clearTimeout(timeout);
|
|
234
|
-
}
|
|
235
|
-
}
|