@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895

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.
Files changed (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +1 -2
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  5. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
  6. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  8. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  9. package/package.json +2 -3
  10. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  11. package/src/__tests__/local-standalone.test.ts +5 -36
  12. package/src/__tests__/managed-integration.test.ts +112 -91
  13. package/src/__tests__/managed-reconnect.test.ts +2 -2
  14. package/src/__tests__/transport.test.ts +23 -27
  15. package/src/cli.ts +1 -1
  16. package/src/index.ts +8 -88
  17. package/src/main.ts +228 -340
  18. package/src/paths.ts +4 -20
  19. package/src/server.ts +52 -469
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  21. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  22. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  23. package/src/__tests__/command-executor.test.ts +0 -1879
  24. package/src/__tests__/command-validator.test.ts +0 -1405
  25. package/src/__tests__/command-workspace.test.ts +0 -1050
  26. package/src/__tests__/grant-store.test.ts +0 -689
  27. package/src/__tests__/http-executor.test.ts +0 -1336
  28. package/src/__tests__/http-policy.test.ts +0 -1069
  29. package/src/__tests__/local-materializers.test.ts +0 -860
  30. package/src/__tests__/local-token-refresh.test.ts +0 -361
  31. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  32. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  33. package/src/__tests__/managed-materializers.test.ts +0 -1028
  34. package/src/__tests__/managed-rejection.test.ts +0 -43
  35. package/src/__tests__/toolstore.test.ts +0 -773
  36. package/src/audit/store.ts +0 -188
  37. package/src/commands/auth-adapters.ts +0 -169
  38. package/src/commands/egress-hooks.ts +0 -203
  39. package/src/commands/executor.ts +0 -1155
  40. package/src/commands/output-scan.ts +0 -157
  41. package/src/commands/profiles.ts +0 -286
  42. package/src/commands/validator.ts +0 -702
  43. package/src/commands/workspace.ts +0 -550
  44. package/src/grants/index.ts +0 -17
  45. package/src/grants/persistent-store.ts +0 -309
  46. package/src/grants/rpc-handlers.ts +0 -293
  47. package/src/grants/temporary-store.ts +0 -289
  48. package/src/http/audit.ts +0 -84
  49. package/src/http/executor.ts +0 -684
  50. package/src/http/path-template.ts +0 -245
  51. package/src/http/policy.ts +0 -238
  52. package/src/http/response-filter.ts +0 -233
  53. package/src/managed-errors.ts +0 -9
  54. package/src/managed-lazy-getters.ts +0 -106
  55. package/src/managed-main.ts +0 -822
  56. package/src/materializers/local-oauth-lookup.ts +0 -98
  57. package/src/materializers/local-token-refresh.ts +0 -287
  58. package/src/materializers/local.ts +0 -316
  59. package/src/materializers/managed-platform.ts +0 -295
  60. package/src/subjects/local.ts +0 -177
  61. package/src/subjects/managed.ts +0 -311
  62. package/src/subjects/policy.ts +0 -79
  63. package/src/toolstore/integrity.ts +0 -94
  64. package/src/toolstore/manifest.ts +0 -154
  65. package/src/toolstore/publish.ts +0 -571
@@ -1,188 +0,0 @@
1
- /**
2
- * CES audit record persistence.
3
- *
4
- * Persists token-free audit record summaries to `audit.jsonl` inside
5
- * the CES-private data root. Each line is a self-contained JSON object
6
- * conforming to the `AuditRecordSummary` schema from `@vellumai/service-contracts`.
7
- *
8
- * Design principles:
9
- * - **Append-only**: Records are appended one per line. The file is never
10
- * rewritten or truncated during normal operation.
11
- * - **Token-free**: Audit records must never contain raw secrets, auth
12
- * tokens, raw headers, or raw response bodies. Only sanitized summaries
13
- * (method, URL template, status code, credential handle, grant ID) are
14
- * persisted.
15
- * - **Fail-open for reads**: If the file is corrupt or missing, reads
16
- * return an empty array rather than throwing. Writes still throw on I/O
17
- * failure so callers know when persistence is broken.
18
- * - **Bounded reads**: The `list` method supports limit and cursor-based
19
- * pagination to avoid reading the entire log into memory.
20
- */
21
-
22
- import {
23
- appendFileSync,
24
- existsSync,
25
- mkdirSync,
26
- readFileSync,
27
- } from "node:fs";
28
- import { dirname, join } from "node:path";
29
-
30
- import type { AuditRecordSummary } from "@vellumai/service-contracts/credential-rpc";
31
-
32
- import { getCesAuditDir } from "../paths.js";
33
-
34
- // ---------------------------------------------------------------------------
35
- // Constants
36
- // ---------------------------------------------------------------------------
37
-
38
- const AUDIT_FILENAME = "audit.jsonl";
39
-
40
- // ---------------------------------------------------------------------------
41
- // Store implementation
42
- // ---------------------------------------------------------------------------
43
-
44
- export class AuditStore {
45
- private readonly filePath: string;
46
-
47
- constructor(auditDir?: string) {
48
- const dir = auditDir ?? getCesAuditDir();
49
- this.filePath = join(dir, AUDIT_FILENAME);
50
- }
51
-
52
- // -----------------------------------------------------------------------
53
- // Public API
54
- // -----------------------------------------------------------------------
55
-
56
- /**
57
- * Ensure the parent directory exists. Safe to call multiple times.
58
- */
59
- init(): void {
60
- const dir = dirname(this.filePath);
61
- if (!existsSync(dir)) {
62
- mkdirSync(dir, { recursive: true });
63
- }
64
- }
65
-
66
- /**
67
- * Append a token-free audit record summary to the log.
68
- *
69
- * Throws on I/O failure (callers should handle gracefully — audit
70
- * persistence failure must not block the execution pipeline).
71
- */
72
- append(record: AuditRecordSummary): void {
73
- const dir = dirname(this.filePath);
74
- if (!existsSync(dir)) {
75
- mkdirSync(dir, { recursive: true });
76
- }
77
-
78
- const line = JSON.stringify(record) + "\n";
79
- appendFileSync(this.filePath, line, { mode: 0o600 });
80
- }
81
-
82
- /**
83
- * List audit records with optional filtering and pagination.
84
- *
85
- * Records are returned in reverse-chronological order (newest first).
86
- *
87
- * @param options.sessionId - Filter by session ID.
88
- * @param options.credentialHandle - Filter by credential handle.
89
- * @param options.grantId - Filter by grant ID.
90
- * @param options.limit - Maximum number of records to return (default: 50).
91
- * @param options.cursor - Opaque cursor from a previous response to
92
- * continue pagination. The cursor is the 0-based line offset encoded
93
- * as a string.
94
- *
95
- * @returns An object with `records` and `nextCursor`. `nextCursor` is
96
- * null when there are no more results.
97
- */
98
- list(options?: {
99
- sessionId?: string;
100
- credentialHandle?: string;
101
- grantId?: string;
102
- limit?: number;
103
- cursor?: string;
104
- }): { records: AuditRecordSummary[]; nextCursor: string | null } {
105
- const limit = options?.limit ?? 50;
106
-
107
- const allRecords = this.readAll();
108
-
109
- // Reverse for newest-first ordering
110
- allRecords.reverse();
111
-
112
- // Apply filters
113
- let filtered = allRecords;
114
- if (options?.sessionId) {
115
- filtered = filtered.filter((r) => r.sessionId === options.sessionId);
116
- }
117
- if (options?.credentialHandle) {
118
- filtered = filtered.filter(
119
- (r) => r.credentialHandle === options.credentialHandle,
120
- );
121
- }
122
- if (options?.grantId) {
123
- filtered = filtered.filter((r) => r.grantId === options.grantId);
124
- }
125
-
126
- // Apply cursor-based pagination
127
- const offset = options?.cursor ? parseInt(options.cursor, 10) : 0;
128
- const startIdx = isNaN(offset) ? 0 : offset;
129
-
130
- const page = filtered.slice(startIdx, startIdx + limit);
131
- const hasMore = startIdx + limit < filtered.length;
132
- const nextCursor = hasMore ? String(startIdx + limit) : null;
133
-
134
- return { records: page, nextCursor };
135
- }
136
-
137
- /**
138
- * Return the total number of records in the log.
139
- *
140
- * Returns 0 if the file does not exist or is unreadable.
141
- */
142
- count(): number {
143
- return this.readAll().length;
144
- }
145
-
146
- // -----------------------------------------------------------------------
147
- // Internals
148
- // -----------------------------------------------------------------------
149
-
150
- /**
151
- * Read all records from the JSONL file, skipping malformed lines.
152
- *
153
- * Returns an empty array if the file is missing or unreadable.
154
- */
155
- private readAll(): AuditRecordSummary[] {
156
- if (!existsSync(this.filePath)) return [];
157
-
158
- let raw: string;
159
- try {
160
- raw = readFileSync(this.filePath, "utf-8");
161
- } catch {
162
- return [];
163
- }
164
-
165
- const records: AuditRecordSummary[] = [];
166
- const lines = raw.split("\n");
167
-
168
- for (const line of lines) {
169
- const trimmed = line.trim();
170
- if (trimmed.length === 0) continue;
171
-
172
- try {
173
- const parsed = JSON.parse(trimmed) as AuditRecordSummary;
174
- // Minimal validation — must have auditId and timestamp
175
- if (
176
- typeof parsed.auditId === "string" &&
177
- typeof parsed.timestamp === "string"
178
- ) {
179
- records.push(parsed);
180
- }
181
- } catch {
182
- // Skip malformed lines
183
- }
184
- }
185
-
186
- return records;
187
- }
188
- }
@@ -1,169 +0,0 @@
1
- /**
2
- * CES auth adapter definitions for secure command profiles.
3
- *
4
- * Auth adapters describe how credentials are materialised into a command's
5
- * execution environment. Each adapter type has different security properties
6
- * and cleanup requirements.
7
- *
8
- * v1 adapter set:
9
- *
10
- * - `env_var` — Inject credential as an environment variable.
11
- * Lifetime: process scope. Cleaned up on exit.
12
- * - `temp_file` — Write credential to a temporary file and pass
13
- * the path via an env var. File is deleted after
14
- * command exits.
15
- * - `credential_process` — Spawn a helper process that prints the credential
16
- * to stdout (AWS credential_process pattern). The
17
- * helper runs inside CES and is never exposed to the
18
- * subprocess directly.
19
- */
20
-
21
- // ---------------------------------------------------------------------------
22
- // Auth adapter type discriminator
23
- // ---------------------------------------------------------------------------
24
-
25
- export const AuthAdapterType = {
26
- /** Inject credential value as an environment variable. */
27
- EnvVar: "env_var",
28
- /** Write credential to a temp file and set a path env var. */
29
- TempFile: "temp_file",
30
- /**
31
- * Spawn a credential helper process (AWS credential_process-style).
32
- * The helper stdout is captured and injected as an env var.
33
- */
34
- CredentialProcess: "credential_process",
35
- } as const;
36
-
37
- export type AuthAdapterType =
38
- (typeof AuthAdapterType)[keyof typeof AuthAdapterType];
39
-
40
- /** All valid auth adapter type strings. */
41
- export const AUTH_ADAPTER_TYPES: readonly AuthAdapterType[] = Object.values(
42
- AuthAdapterType,
43
- ) as AuthAdapterType[];
44
-
45
- // ---------------------------------------------------------------------------
46
- // Auth adapter config shapes
47
- // ---------------------------------------------------------------------------
48
-
49
- /**
50
- * Inject a credential directly as an environment variable.
51
- *
52
- * Example: `GH_TOKEN=<secret>` with `envVarName: "GH_TOKEN"`.
53
- */
54
- export interface EnvVarAdapterConfig {
55
- type: typeof AuthAdapterType.EnvVar;
56
- /** Environment variable name where the credential value is injected. */
57
- envVarName: string;
58
- /**
59
- * Optional prefix prepended to the raw credential value before injection
60
- * (e.g. "Bearer " for OAuth tokens).
61
- */
62
- valuePrefix?: string;
63
- }
64
-
65
- /**
66
- * Write the credential to a temporary file and set an env var to the path.
67
- *
68
- * Example: `GOOGLE_APPLICATION_CREDENTIALS=/tmp/ces-xxx/svc.json`.
69
- * The temp file is created in a CES-managed ephemeral directory and deleted
70
- * after the command exits.
71
- */
72
- export interface TempFileAdapterConfig {
73
- type: typeof AuthAdapterType.TempFile;
74
- /** Environment variable name pointing to the temp file path. */
75
- envVarName: string;
76
- /** File extension for the temp file (e.g. ".json", ".pem"). */
77
- fileExtension?: string;
78
- /**
79
- * File mode (octal) for the temp file. Defaults to 0o600 (owner-only
80
- * read/write). Must be <= 0o600.
81
- */
82
- fileMode?: number;
83
- }
84
-
85
- /**
86
- * Spawn a credential helper process, capture its stdout, and inject the
87
- * result as an env var.
88
- *
89
- * Example: AWS `credential_process` that emits JSON with temporary keys.
90
- * The helper command runs inside the CES process and is never exposed to
91
- * the child command.
92
- */
93
- export interface CredentialProcessAdapterConfig {
94
- type: typeof AuthAdapterType.CredentialProcess;
95
- /** The helper command to run (e.g. "aws-vault exec <profile> --json"). */
96
- helperCommand: string;
97
- /** Environment variable name where the helper's stdout is injected. */
98
- envVarName: string;
99
- /** Timeout in milliseconds for the helper process. Defaults to 10000. */
100
- timeoutMs?: number;
101
- }
102
-
103
- /**
104
- * Discriminated union of all auth adapter configurations.
105
- */
106
- export type AuthAdapterConfig =
107
- | EnvVarAdapterConfig
108
- | TempFileAdapterConfig
109
- | CredentialProcessAdapterConfig;
110
-
111
- // ---------------------------------------------------------------------------
112
- // Validation helpers
113
- // ---------------------------------------------------------------------------
114
-
115
- /**
116
- * Returns true if the given string is a valid auth adapter type.
117
- */
118
- export function isValidAuthAdapterType(value: string): value is AuthAdapterType {
119
- return (AUTH_ADAPTER_TYPES as readonly string[]).includes(value);
120
- }
121
-
122
- /**
123
- * Validate an auth adapter config shape. Returns a list of error messages
124
- * (empty array = valid).
125
- */
126
- export function validateAuthAdapterConfig(
127
- config: AuthAdapterConfig,
128
- ): string[] {
129
- const errors: string[] = [];
130
-
131
- if (!isValidAuthAdapterType(config.type)) {
132
- errors.push(
133
- `Unknown auth adapter type "${config.type}". Valid types: ${AUTH_ADAPTER_TYPES.join(", ")}`,
134
- );
135
- return errors;
136
- }
137
-
138
- if (!config.envVarName || config.envVarName.trim().length === 0) {
139
- errors.push(`Auth adapter "${config.type}" requires a non-empty envVarName`);
140
- }
141
-
142
- switch (config.type) {
143
- case AuthAdapterType.TempFile:
144
- if (
145
- config.fileMode !== undefined &&
146
- (config.fileMode > 0o600 || (config.fileMode & 0o077) !== 0)
147
- ) {
148
- errors.push(
149
- `temp_file adapter fileMode must be <= 0600 (owner-only) with no group/other bits, got ${config.fileMode.toString(8)}`,
150
- );
151
- }
152
- break;
153
-
154
- case AuthAdapterType.CredentialProcess:
155
- if (!config.helperCommand || config.helperCommand.trim().length === 0) {
156
- errors.push(
157
- `credential_process adapter requires a non-empty helperCommand`,
158
- );
159
- }
160
- if (config.timeoutMs !== undefined && config.timeoutMs <= 0) {
161
- errors.push(
162
- `credential_process adapter timeoutMs must be positive, got ${config.timeoutMs}`,
163
- );
164
- }
165
- break;
166
- }
167
-
168
- return errors;
169
- }
@@ -1,203 +0,0 @@
1
- /**
2
- * CES egress proxy hooks.
3
- *
4
- * Provides a lightweight `SessionStartHooks` implementation for the
5
- * credential execution service. Unlike the assistant's session-manager
6
- * (which wires credential resolution, MITM interception, and policy
7
- * decisions), CES only needs to enforce the manifest's
8
- * `allowedNetworkTargets` allowlist. No credential injection or CA
9
- * setup happens at the proxy layer — CES injects credentials through
10
- * auth adapters in the command environment.
11
- *
12
- * The proxy server is a plain HTTP CONNECT proxy that:
13
- * - Allows connections matching the session's `allowedTargets` (host, port, protocol)
14
- * - Blocks all other outbound connections
15
- */
16
-
17
- import { request as httpRequest, createServer, type IncomingMessage, type ServerResponse } from "node:http";
18
- import { request as httpsRequest } from "node:https";
19
- import { connect, type Socket } from "node:net";
20
-
21
- import type { AllowedTarget, ManagedSession, SessionStartHooks } from "@vellumai/egress-proxy";
22
-
23
- // ---------------------------------------------------------------------------
24
- // Host-pattern matching
25
- // ---------------------------------------------------------------------------
26
-
27
- /**
28
- * Match a hostname against a glob pattern.
29
- *
30
- * Supported patterns:
31
- * - Exact match: `"api.github.com"` matches `"api.github.com"`
32
- * - Wildcard subdomain: `"*.github.com"` matches `"api.github.com"`,
33
- * `"foo.bar.github.com"`, and also `"github.com"` (apex)
34
- *
35
- * Note: `"*"` (match-everything) is intentionally NOT supported. The
36
- * manifest validator rejects overbroad patterns at registration time.
37
- */
38
- function matchesHostPattern(hostname: string, pattern: string): boolean {
39
- if (pattern === hostname) return true;
40
- if (pattern.startsWith("*.")) {
41
- const suffix = pattern.slice(1); // e.g. ".github.com"
42
- const apex = pattern.slice(2); // e.g. "github.com"
43
- return hostname.endsWith(suffix) || hostname === apex;
44
- }
45
- return false;
46
- }
47
-
48
- /**
49
- * Check if a request target is allowed by any of the provided allowed targets.
50
- *
51
- * Validates host, port, and protocol against each allowed target entry.
52
- * - Host must match the glob pattern.
53
- * - If the target specifies `ports`, the request port must be in the list.
54
- * - If the target specifies `protocols`, the request protocol must be in the list.
55
- */
56
- function isTargetAllowed(
57
- hostname: string,
58
- port: number,
59
- protocol: "http" | "https",
60
- allowedTargets: AllowedTarget[],
61
- ): boolean {
62
- for (const target of allowedTargets) {
63
- if (!matchesHostPattern(hostname, target.host)) continue;
64
- if (target.ports && target.ports.length > 0 && !target.ports.includes(port)) continue;
65
- if (target.protocols && target.protocols.length > 0 && !target.protocols.includes(protocol)) continue;
66
- return true;
67
- }
68
- return false;
69
- }
70
-
71
- // ---------------------------------------------------------------------------
72
- // CONNECT tunnel handler
73
- // ---------------------------------------------------------------------------
74
-
75
- /**
76
- * Parse a CONNECT target of the form `host:port`.
77
- */
78
- function parseConnectTarget(
79
- url: string | undefined,
80
- ): { host: string; port: number } | null {
81
- if (!url) return null;
82
- const colonIdx = url.lastIndexOf(":");
83
- if (colonIdx <= 0) return null;
84
- let host = url.slice(0, colonIdx);
85
- const portStr = url.slice(colonIdx + 1);
86
- if (!host || !portStr) return null;
87
- const port = Number(portStr);
88
- if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
89
- if (host.startsWith("[") && host.endsWith("]")) {
90
- host = host.slice(1, -1);
91
- if (!host) return null;
92
- }
93
- return { host, port };
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // Hooks factory
98
- // ---------------------------------------------------------------------------
99
-
100
- /**
101
- * Build `SessionStartHooks` for CES egress enforcement.
102
- *
103
- * The created proxy server enforces the `allowedTargets` from the
104
- * session's config. If no `allowedTargets` are configured, all
105
- * connections are blocked (fail-closed).
106
- */
107
- export function buildCesEgressHooks(): SessionStartHooks {
108
- return {
109
- // No CA setup needed — CES does not do MITM interception
110
- createServer: async (managed: ManagedSession) => {
111
- const allowedTargets = managed.config.allowedTargets ?? [];
112
-
113
- const server = createServer((req: IncomingMessage, res: ServerResponse) => {
114
- // Plain HTTP proxy requests — parse the absolute URL and check host/port/protocol
115
- if (req.url && req.method) {
116
- try {
117
- const target = new URL(req.url);
118
- const protocol = target.protocol === "https:" ? "https" : "http" as const;
119
- const port = target.port
120
- ? Number(target.port)
121
- : protocol === "https" ? 443 : 80;
122
- if (!isTargetAllowed(target.hostname, port, protocol, allowedTargets)) {
123
- res.writeHead(403, { "Content-Type": "text/plain" });
124
- res.end(`Blocked by CES egress policy: ${target.hostname}:${port} (${protocol}) is not in the allowed targets list`);
125
- return;
126
- }
127
-
128
- // Forward the request using the appropriate protocol
129
- const doRequest = target.protocol === "https:" ? httpsRequest : httpRequest;
130
- const proxyReq = doRequest(
131
- req.url,
132
- {
133
- method: req.method,
134
- headers: { ...req.headers, host: target.host },
135
- },
136
- (proxyRes) => {
137
- res.writeHead(proxyRes.statusCode ?? 502, proxyRes.headers);
138
- proxyRes.pipe(res);
139
- },
140
- );
141
-
142
- proxyReq.on("error", () => {
143
- if (!res.headersSent) {
144
- res.writeHead(502, { "Content-Type": "text/plain" });
145
- }
146
- res.end("Proxy connection error");
147
- });
148
-
149
- req.pipe(proxyReq);
150
- } catch {
151
- res.writeHead(400, { "Content-Type": "text/plain" });
152
- res.end("Bad request");
153
- }
154
- return;
155
- }
156
-
157
- res.writeHead(400, { "Content-Type": "text/plain" });
158
- res.end("Bad request");
159
- });
160
-
161
- // Handle CONNECT for HTTPS tunnelling
162
- server.on("connect", (req: IncomingMessage, clientSocket: Socket, head: Buffer) => {
163
- const target = parseConnectTarget(req.url);
164
- if (!target) {
165
- clientSocket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
166
- clientSocket.destroy();
167
- return;
168
- }
169
-
170
- // CONNECT is used for HTTPS tunnelling — assume "https" protocol
171
- if (!isTargetAllowed(target.host, target.port, "https", allowedTargets)) {
172
- clientSocket.write(
173
- "HTTP/1.1 403 Forbidden\r\n" +
174
- "Content-Type: text/plain\r\n\r\n" +
175
- `Blocked by CES egress policy: ${target.host}:${target.port} (https) is not in the allowed targets list`,
176
- );
177
- clientSocket.destroy();
178
- return;
179
- }
180
-
181
- // Tunnel to the allowed target
182
- const upstream = connect(target.port, target.host, () => {
183
- clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
184
- if (head.length > 0) {
185
- upstream.write(head);
186
- }
187
- upstream.pipe(clientSocket);
188
- clientSocket.pipe(upstream);
189
- });
190
-
191
- upstream.on("error", () => {
192
- clientSocket.destroy();
193
- });
194
-
195
- clientSocket.on("error", () => {
196
- upstream.destroy();
197
- });
198
- });
199
-
200
- return server;
201
- },
202
- };
203
- }