@agenthippo-ai/agenthippo 0.1.1 → 1.110.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/agentpack/index.js +2 -1
- package/out/agentpack/paths.js +23 -0
- package/out/analytics/dashboard/fleetDisplayUtils.js +3 -0
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +293 -82
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +23 -25
- package/out/analytics/sessionEvents.js +109 -22
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +49 -15
- package/out/auth/onboardingPrompt.js +112 -0
- package/out/auth/provider.js +9 -22
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +54 -4
- package/out/auth-broker/server.js +152 -20
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/ask.js +3 -0
- package/out/cli/commands/chat.js +3 -0
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +67 -0
- package/out/cli/commands/deploy.js +50 -10
- package/out/cli/commands/gateway.js +58 -0
- package/out/cli/commands/idp.js +85 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +332 -0
- package/out/cli/commands/serve.js +1165 -82
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +42 -80
- package/out/cli/data-plane/broker.js +78 -0
- package/out/cli/data-plane/connector-host.js +121 -0
- package/out/cli/data-plane/egress.js +189 -0
- package/out/cli/data-plane/gateway.js +218 -0
- package/out/cli/data-plane/index.js +152 -0
- package/out/cli/data-plane/registry.js +89 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +161 -0
- package/out/cli/deploy/blueprints/registry.js +84 -0
- package/out/cli/deploy/layers/identity.js +172 -0
- package/out/cli/deploy-init.js +1182 -347
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +338 -0
- package/out/cli/index.js +192 -23
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +45 -3
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +44 -3
- package/out/cli/serve-hardening.js +139 -3
- package/out/cli/state-sync.js +16 -15
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +47 -8
- package/out/engines/codex.js +125 -22
- package/out/engines/custom.js +54 -12
- package/out/evidence/sessionShip.js +610 -0
- package/out/extension.js +13 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/outboundSend.js +6 -6
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +40 -9
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/live-probe/connector.mjs +8 -0
- package/out/live-probe/env-report.mjs +8 -0
- package/out/mcp.js +115 -67
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +309 -0
- package/out/pack/lease-schema.js +270 -0
- package/out/pack/lease.js +640 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +131 -0
- package/out/runner/protocol.js +108 -0
- package/out/runner/remoteEngine.js +130 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +570 -0
- package/out/sandbox/processDriver.js +315 -0
- package/out/sandbox/sandboxRunner.js +191 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -118
- package/out/scripts/analytics/setup-analytics.ps1 +37 -13
- package/out/scripts/analytics/setup-analytics.sh +93 -13
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +12 -111
- package/out/shared/providers.js +2 -2
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +198 -34
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +112 -9
- package/out/test/blueprint-registry.test.js +105 -0
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +455 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/credential-context.test.js +23 -28
- package/out/test/custom-env-layering.test.js +151 -0
- package/out/test/data-plane-engine-wiring.test.js +250 -0
- package/out/test/data-plane.test.js +311 -0
- package/out/test/deploy-init.test.js +295 -4
- package/out/test/egress-broker.test.js +172 -0
- package/out/test/fleetDisplayUtils.test.js +4 -0
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +217 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/idp-init.test.js +151 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +169 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/mcp-obo.test.js +52 -27
- package/out/test/mcp.test.js +69 -3
- package/out/test/model-routing.test.js +133 -0
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +395 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/serve-gateway-turn.test.js +267 -0
- package/out/test/serve-hardening.test.js +125 -6
- package/out/test/serveClient.test.js +38 -5
- package/out/test/sessionShip.test.js +560 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/state-sync-peer.test.js +18 -5
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +73 -20
- package/package.json +1 -1
|
@@ -63,6 +63,49 @@ suite('serve-hardening: resolveStateDir', () => {
|
|
|
63
63
|
assert.ok(r.dir.startsWith(os.tmpdir()));
|
|
64
64
|
});
|
|
65
65
|
});
|
|
66
|
+
suite('serve-hardening: resolveAnalyticsSessionsDir', () => {
|
|
67
|
+
// getAgentideHome() reads process.env directly (not injectable), so tests that need it to
|
|
68
|
+
// resolve deterministically save/restore the real env vars around themselves.
|
|
69
|
+
const saved = {};
|
|
70
|
+
setup(() => {
|
|
71
|
+
saved.AGENT_HOME_DIR = process.env.AGENT_HOME_DIR;
|
|
72
|
+
saved.AGENTHIPPO_HOME = process.env.AGENTHIPPO_HOME;
|
|
73
|
+
delete process.env.AGENT_HOME_DIR;
|
|
74
|
+
delete process.env.AGENTHIPPO_HOME;
|
|
75
|
+
});
|
|
76
|
+
teardown(() => {
|
|
77
|
+
if (saved.AGENT_HOME_DIR === undefined) {
|
|
78
|
+
delete process.env.AGENT_HOME_DIR;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
process.env.AGENT_HOME_DIR = saved.AGENT_HOME_DIR;
|
|
82
|
+
}
|
|
83
|
+
if (saved.AGENTHIPPO_HOME === undefined) {
|
|
84
|
+
delete process.env.AGENTHIPPO_HOME;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
process.env.AGENTHIPPO_HOME = saved.AGENTHIPPO_HOME;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
test('AGENTHIPPO_HOME set → uses getAgentideHome(), not stateDir (stays discoverable to Spotlight/DuckDB)', () => {
|
|
91
|
+
process.env.AGENTHIPPO_HOME = '/data/.agent-hippo';
|
|
92
|
+
const dir = (0, serve_hardening_1.resolveAnalyticsSessionsDir)('/data/.agent-hippo/serve-state', false, { AGENTHIPPO_HOME: '/data/.agent-hippo' });
|
|
93
|
+
assert.strictEqual(dir, path.join('/data/.agent-hippo', 'analytics', 'sessions'));
|
|
94
|
+
});
|
|
95
|
+
test('AGENT_HOME_DIR set → same precedence as AGENTHIPPO_HOME', () => {
|
|
96
|
+
process.env.AGENT_HOME_DIR = '/data/.agent-hippo';
|
|
97
|
+
const dir = (0, serve_hardening_1.resolveAnalyticsSessionsDir)('/data/.agent-hippo/serve-state', false, { AGENT_HOME_DIR: '/data/.agent-hippo' });
|
|
98
|
+
assert.strictEqual(dir, path.join('/data/.agent-hippo', 'analytics', 'sessions'));
|
|
99
|
+
});
|
|
100
|
+
test('no home-dir env var, explicit non-ephemeral --state-dir → uses stateDir (never regress durability for discoverability)', () => {
|
|
101
|
+
const dir = (0, serve_hardening_1.resolveAnalyticsSessionsDir)('/mnt/durable-vol', false, {});
|
|
102
|
+
assert.strictEqual(dir, path.join('/mnt/durable-vol', 'analytics', 'sessions'));
|
|
103
|
+
});
|
|
104
|
+
test('no home-dir env var, ephemeral state (nothing durable configured) → falls back to getAgentideHome() default', () => {
|
|
105
|
+
const dir = (0, serve_hardening_1.resolveAnalyticsSessionsDir)(path.join(os.tmpdir(), 'agenthippo-server'), true, {});
|
|
106
|
+
assert.strictEqual(dir, path.join(os.homedir(), '.agent-hippo', 'analytics', 'sessions'));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
66
109
|
suite('serve-hardening: resolveBindHost', () => {
|
|
67
110
|
test('defaults to loopback', () => {
|
|
68
111
|
assert.strictEqual((0, serve_hardening_1.resolveBindHost)(undefined, {}), serve_hardening_1.DEFAULT_BIND_HOST);
|
|
@@ -124,9 +167,40 @@ suite('serve-hardening: RateLimiter', () => {
|
|
|
124
167
|
});
|
|
125
168
|
});
|
|
126
169
|
suite('serve-hardening: resolveUserId', () => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
170
|
+
/**
|
|
171
|
+
* BREAKING, and deliberately so. `x-agent-user` used to be the FIRST trusted alias, so on any
|
|
172
|
+
* deployment that had not pinned AGENTHIPPO_IDENTITY_HEADER, any HTTP client could send
|
|
173
|
+
* `x-agent-user: victim@example.com` and be namespaced into that user's session state, attach
|
|
174
|
+
* to their conversation, and be issued a data-plane turn nonce scoped to them. No platform
|
|
175
|
+
* strips it — Cloud Run IAP strips `X-Goog-*` and forwards this one untouched.
|
|
176
|
+
*/
|
|
177
|
+
test('x-agent-user is NOT trusted by default — it is not stripped by any platform', () => {
|
|
178
|
+
assert.strictEqual((0, serve_hardening_1.resolveUserId)({ 'x-agent-user': 'victim@example.com' }, undefined, {}), undefined);
|
|
179
|
+
});
|
|
180
|
+
test('x-agent-user IS trusted when the deployment pins it (the Caddy forward_auth flow)', () => {
|
|
181
|
+
const env = { AGENTHIPPO_IDENTITY_HEADER: 'x-agent-user' };
|
|
182
|
+
assert.strictEqual((0, serve_hardening_1.resolveUserId)({ 'x-agent-user': 'alice@corp.com' }, { user_id: 'bob' }, env), 'alice@corp.com');
|
|
183
|
+
});
|
|
184
|
+
test('an unpinned deployment is told which env var to set, and why', () => {
|
|
185
|
+
const advice = (0, serve_hardening_1.identityHeaderAdvice)({});
|
|
186
|
+
assert.ok(advice.length > 0);
|
|
187
|
+
assert.ok(advice.every((l) => /AGENTHIPPO_IDENTITY_HEADER/.test(l)));
|
|
188
|
+
assert.ok(advice.some((l) => /x-agent-user/.test(l)), 'the removal must be explained, not silent');
|
|
189
|
+
assert.deepStrictEqual((0, serve_hardening_1.identityHeaderAdvice)({ AGENTHIPPO_IDENTITY_HEADER: 'x-agent-user' }), [], 'a pinned deployment is quiet');
|
|
190
|
+
});
|
|
191
|
+
test('advice severity follows reachability, so the loud case stays loud', () => {
|
|
192
|
+
const loopback = (0, serve_hardening_1.identityHeaderAdvice)({}, '127.0.0.1');
|
|
193
|
+
assert.strictEqual(loopback.length, 1, 'a local run gets one short note, not a lecture');
|
|
194
|
+
assert.ok(loopback.every((l) => /AGENTHIPPO_IDENTITY_HEADER/.test(l)));
|
|
195
|
+
assert.deepStrictEqual((0, serve_hardening_1.identityHeaderAdvice)({}, 'localhost').length, 1);
|
|
196
|
+
assert.deepStrictEqual((0, serve_hardening_1.identityHeaderAdvice)({}, '::1').length, 1);
|
|
197
|
+
const exposed = (0, serve_hardening_1.identityHeaderAdvice)({}, '0.0.0.0');
|
|
198
|
+
assert.ok(exposed.length > loopback.length, 'an exposed bind must say strictly more');
|
|
199
|
+
assert.ok(exposed.some((l) => /reachable beyond this machine/.test(l)));
|
|
200
|
+
assert.ok(exposed.every((l) => /AGENTHIPPO_IDENTITY_HEADER/.test(l)));
|
|
201
|
+
// Pinning silences both, exposed or not.
|
|
202
|
+
const pinned = { AGENTHIPPO_IDENTITY_HEADER: 'x-goog-authenticated-user-email' };
|
|
203
|
+
assert.deepStrictEqual((0, serve_hardening_1.identityHeaderAdvice)(pinned, '0.0.0.0'), []);
|
|
130
204
|
});
|
|
131
205
|
test('Databricks X-Forwarded-Email', () => {
|
|
132
206
|
assert.strictEqual((0, serve_hardening_1.resolveUserId)({ 'x-forwarded-email': 'priya@meridian.com' }, undefined), 'priya@meridian.com');
|
|
@@ -138,12 +212,20 @@ suite('serve-hardening: resolveUserId', () => {
|
|
|
138
212
|
test('Cloudflare Access header', () => {
|
|
139
213
|
assert.strictEqual((0, serve_hardening_1.resolveUserId)({ 'cf-access-authenticated-user-email': 'susan@meridian.com' }, undefined), 'susan@meridian.com');
|
|
140
214
|
});
|
|
141
|
-
test('
|
|
142
|
-
|
|
215
|
+
test('body.user_id is ignored unless explicitly opted in (§10 hole 3)', () => {
|
|
216
|
+
// Safe-by-convention was the hole: any deployment that forgot to pin a header let
|
|
217
|
+
// every caller name an arbitrary victim via the client-controlled body.
|
|
218
|
+
assert.strictEqual((0, serve_hardening_1.resolveUserId)({}, { user_id: 'bob' }, {}), undefined);
|
|
219
|
+
});
|
|
220
|
+
test('AGENTHIPPO_ALLOW_BODY_IDENTITY=1 restores the dev-convenience body fallback', () => {
|
|
221
|
+
const env = { AGENTHIPPO_ALLOW_BODY_IDENTITY: '1' };
|
|
222
|
+
assert.strictEqual((0, serve_hardening_1.resolveUserId)({}, { user_id: 'bob' }, env), 'bob');
|
|
223
|
+
// A trusted platform header still wins over the body even when opted in.
|
|
224
|
+
assert.strictEqual((0, serve_hardening_1.resolveUserId)({ 'x-forwarded-email': 'alice@corp.com' }, { user_id: 'bob' }, env), 'alice@corp.com');
|
|
143
225
|
});
|
|
144
226
|
test('undefined when nothing present', () => {
|
|
145
227
|
assert.strictEqual((0, serve_hardening_1.resolveUserId)({}, undefined), undefined);
|
|
146
|
-
assert.strictEqual((0, serve_hardening_1.resolveUserId)({}, { user_id: ' ' }), undefined);
|
|
228
|
+
assert.strictEqual((0, serve_hardening_1.resolveUserId)({}, { user_id: ' ' }, { AGENTHIPPO_ALLOW_BODY_IDENTITY: '1' }), undefined);
|
|
147
229
|
});
|
|
148
230
|
test('AGENTHIPPO_IDENTITY_HEADER pins trust to one header — spoofed aliases are ignored', () => {
|
|
149
231
|
const env = { AGENTHIPPO_IDENTITY_HEADER: 'x-goog-authenticated-user-email' };
|
|
@@ -297,3 +379,40 @@ suite('serve-hardening: readBodyWithCap', () => {
|
|
|
297
379
|
assert.strictEqual(serve_hardening_1.DEFAULT_MAX_BODY_BYTES, 1024 * 1024);
|
|
298
380
|
});
|
|
299
381
|
});
|
|
382
|
+
suite('serve-hardening: resolveServeSessionKey (secure-obo-design §3.4 Phase 0)', () => {
|
|
383
|
+
const PINNED = { AGENTHIPPO_IDENTITY_HEADER: 'x-forwarded-email' };
|
|
384
|
+
test('verified user + session_id → user-namespaced key', () => {
|
|
385
|
+
const r = (0, serve_hardening_1.resolveServeSessionKey)('chat-1', 'alice@corp.com', PINNED);
|
|
386
|
+
assert.strictEqual(r.sessionKey, `u${(0, serve_hardening_1.userNamespaceHash)('alice@corp.com')}--chat-1`);
|
|
387
|
+
assert.strictEqual(r.rejected, undefined);
|
|
388
|
+
});
|
|
389
|
+
test('two users presenting the SAME session_id can never share a key (the defect this closes)', () => {
|
|
390
|
+
const a = (0, serve_hardening_1.resolveServeSessionKey)('chat-1', 'alice@corp.com', PINNED);
|
|
391
|
+
const b = (0, serve_hardening_1.resolveServeSessionKey)('chat-1', 'bob@corp.com', PINNED);
|
|
392
|
+
assert.notStrictEqual(a.sessionKey, b.sessionKey);
|
|
393
|
+
});
|
|
394
|
+
test('verified user without session_id → stable per-user default key', () => {
|
|
395
|
+
const r1 = (0, serve_hardening_1.resolveServeSessionKey)(undefined, 'alice@corp.com', PINNED);
|
|
396
|
+
const r2 = (0, serve_hardening_1.resolveServeSessionKey)('', 'alice@corp.com', PINNED);
|
|
397
|
+
assert.strictEqual(r1.sessionKey, r2.sessionKey);
|
|
398
|
+
assert.strictEqual(r1.sessionKey, `u${(0, serve_hardening_1.userNamespaceHash)('alice@corp.com')}--default`);
|
|
399
|
+
});
|
|
400
|
+
test('pinned identity + session_id but NO verified user → fail closed', () => {
|
|
401
|
+
const r = (0, serve_hardening_1.resolveServeSessionKey)('chat-1', undefined, PINNED);
|
|
402
|
+
assert.strictEqual(r.rejected, serve_hardening_1.SESSION_WITHOUT_USER_REASON);
|
|
403
|
+
});
|
|
404
|
+
test('unpinned (local dev) keeps raw session_id behavior', () => {
|
|
405
|
+
const r = (0, serve_hardening_1.resolveServeSessionKey)('chat-1', undefined, {});
|
|
406
|
+
assert.strictEqual(r.sessionKey, 'chat-1');
|
|
407
|
+
assert.strictEqual(r.rejected, undefined);
|
|
408
|
+
});
|
|
409
|
+
test('unpinned, no user, no session_id → legacy anon bucket', () => {
|
|
410
|
+
const r = (0, serve_hardening_1.resolveServeSessionKey)(undefined, undefined, {});
|
|
411
|
+
assert.strictEqual(r.sessionKey, 'anon-unknown');
|
|
412
|
+
});
|
|
413
|
+
test('the key never embeds the raw user id (emails are not path segments)', () => {
|
|
414
|
+
const r = (0, serve_hardening_1.resolveServeSessionKey)('s', 'alice@corp.com', PINNED);
|
|
415
|
+
assert.ok(!r.sessionKey.includes('alice@corp.com'));
|
|
416
|
+
assert.ok(!r.sessionKey.includes('@'));
|
|
417
|
+
});
|
|
418
|
+
});
|
|
@@ -98,11 +98,12 @@ suite('ServeClient', () => {
|
|
|
98
98
|
test('normalizeServeBaseUrl strips trailing slashes', () => {
|
|
99
99
|
assert.strictEqual((0, serveClient_js_1.normalizeServeBaseUrl)('http://127.0.0.1:8787/'), 'http://127.0.0.1:8787');
|
|
100
100
|
});
|
|
101
|
-
test('buildServeAuthHeaders
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
test('buildServeAuthHeaders sends the bearer token and NO self-asserted identity', () => {
|
|
102
|
+
// `X-Agent-User` used to ride along here, and the serve side trusts `x-agent-user` as its
|
|
103
|
+
// first identity alias. On a deployment that does not pin AGENTHIPPO_IDENTITY_HEADER, a
|
|
104
|
+
// client could name someone else and be namespaced into that user's session/state and
|
|
105
|
+
// handed a data-plane nonce scoped to them. The bearer already carries a verified identity.
|
|
106
|
+
assert.deepStrictEqual((0, serveClient_js_1.buildServeAuthHeaders)({ accessToken: 'tok', userEmail: 'you@acme.com' }), { Authorization: 'Bearer tok' });
|
|
106
107
|
});
|
|
107
108
|
test('streamRemoteServeChat sends Authorization when auth provided', async () => {
|
|
108
109
|
const mock = await startMockServeServer([
|
|
@@ -173,4 +174,36 @@ suite('ServeClient', () => {
|
|
|
173
174
|
});
|
|
174
175
|
});
|
|
175
176
|
});
|
|
177
|
+
test('streamRemoteServeChat surfaces fail-closed credential rejection as HTTP error (not silent success)', async () => {
|
|
178
|
+
// Mirrors serve.ts after the fix: warrant denial happens BEFORE SSE writeHead(200),
|
|
179
|
+
// so the client sees a real non-2xx JSON error instead of 200 + raw body fallthrough.
|
|
180
|
+
const server = http.createServer((_req, res) => {
|
|
181
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
182
|
+
res.end(JSON.stringify({
|
|
183
|
+
error: 'OBO data token present without a verified user in request scope (fail-closed: token_without_verified_user)',
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
186
|
+
await new Promise((resolve, reject) => {
|
|
187
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
188
|
+
try {
|
|
189
|
+
const address = server.address();
|
|
190
|
+
if (!address || typeof address === 'string') {
|
|
191
|
+
throw new Error('bad address');
|
|
192
|
+
}
|
|
193
|
+
const emitter = createCaptureEmitter();
|
|
194
|
+
const result = await (0, serveClient_js_1.streamRemoteServeChat)({ baseUrl: `http://127.0.0.1:${address.port}`, message: 'ping' }, emitter);
|
|
195
|
+
assert.strictEqual(result.ok, false);
|
|
196
|
+
assert.match(result.error ?? '', /token_without_verified_user/);
|
|
197
|
+
assert.deepStrictEqual(emitter.events.map((e) => e.type), ['error', 'done']);
|
|
198
|
+
resolve();
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
reject(err);
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
server.close();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
176
209
|
});
|