aki-pro-max 2.3.3
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/.env.example +34 -0
- package/AICOWORKER-NATIVE-TOOLS.md +60 -0
- package/CLAUDE-LIVE-TOOLS-EVIDENCE.json +94 -0
- package/FULL-TRACE-EVIDENCE.json +109 -0
- package/ISSUE-1-REMOTE.json +1 -0
- package/ISSUE-2-POSTREVIEW.json +1 -0
- package/ISSUE-2-REMOTE.json +1 -0
- package/KEY-ROTATION-EVIDENCE.json +9 -0
- package/LICENSE +21 -0
- package/PMN-9ROUTER-FINAL.md +13 -0
- package/RAPID-BASIL-RECONCILIATION.json +10 -0
- package/README.md +232 -0
- package/RELEASE-ARCHIVE.json +15 -0
- package/SECURITY-RECONCILIATION.json +30 -0
- package/SECURITY.md +16 -0
- package/TEST-ISSUES12-FINAL.txt +0 -0
- package/TEST-ISSUES12-HARNESS.txt +0 -0
- package/VERIFICATION-REPORT.md +58 -0
- package/VERIFIER-ISSUES12-FINAL.txt +0 -0
- package/VERIFY-RELEASE-ISSUES12-FINAL.txt +0 -0
- package/VERIFY-RELEASE-ISSUES12-HARNESS.txt +0 -0
- package/bin/aki-pro-max.js +98 -0
- package/docs/ADMIN-GUI-CONTRACT.md +29 -0
- package/docs/ARCHITECTURE.md +109 -0
- package/docs/CAPABILITY-MATRIX.md +44 -0
- package/docs/CORRELATION-DESIGN.md +226 -0
- package/docs/FAIL-CLOSED-ISSUE-HARNESS.md +21 -0
- package/docs/WEB-SESSION-TRANSPORT-DESIGN.md +423 -0
- package/docs/assets/control-plane.jpg +0 -0
- package/gitleaks-report-all.json +1 -0
- package/gitleaks-report-latest.json +1 -0
- package/gitleaks-report.json +1 -0
- package/package.json +33 -0
- package/scripts/eventual-tool-loop.mjs +55 -0
- package/scripts/install-local.ps1 +35 -0
- package/scripts/live-eventual-multitool.mjs +18 -0
- package/scripts/upgrade-admin-v232.mjs +33 -0
- package/scripts/verify-issue-closure.mjs +81 -0
- package/scripts/verify-release.mjs +31 -0
- package/src/admin-auth.mjs +94 -0
- package/src/admin.mjs +133 -0
- package/src/canonical.mjs +23 -0
- package/src/config.mjs +88 -0
- package/src/correlation-store.mjs +120 -0
- package/src/errors.mjs +18 -0
- package/src/index.mjs +4 -0
- package/src/openai-response.mjs +72 -0
- package/src/openai.mjs +104 -0
- package/src/postman-events.mjs +43 -0
- package/src/postman-request.mjs +49 -0
- package/src/schema.mjs +35 -0
- package/src/server.mjs +73 -0
- package/src/session-store.mjs +48 -0
- package/src/sse.mjs +13 -0
- package/src/transport.mjs +90 -0
- package/src/web-session-events.mjs +358 -0
- package/src/web-session-request.mjs +280 -0
- package/test/9router-executor.integration.test.mjs +207 -0
- package/test/admin-auth.test.mjs +47 -0
- package/test/admin.test.mjs +68 -0
- package/test/config.test.mjs +14 -0
- package/test/contract.test.mjs +14 -0
- package/test/correlation-store.test.mjs +19 -0
- package/test/correlation.integration.test.mjs +48 -0
- package/test/eventual-tool-loop.test.mjs +42 -0
- package/test/fixtures/text.json +8 -0
- package/test/fixtures/tool.json +7 -0
- package/test/fixtures/web-session-observed-done.json +12 -0
- package/test/fixtures/web-session-tool-fragments.json +14 -0
- package/test/full-ingress/alias-loader.mjs +22 -0
- package/test/full-ingress/run-full-ingress.mjs +207 -0
- package/test/full-ingress/seed-9router.mjs +36 -0
- package/test/helpers.mjs +9 -0
- package/test/issue-closure-harness.test.mjs +49 -0
- package/test/model-thinking.test.mjs +20 -0
- package/test/protocol.test.mjs +16 -0
- package/test/request.test.mjs +10 -0
- package/test/session-store.test.mjs +20 -0
- package/test/web-session-builder.test.mjs +124 -0
- package/test/web-session-events.test.mjs +241 -0
- package/test/web-session-integration.test.mjs +103 -0
- package/test/web-session-tools.test.mjs +52 -0
- package/version.json +8 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { once } from 'node:events';
|
|
4
|
+
import { createProviderServer } from '../src/server.mjs';
|
|
5
|
+
|
|
6
|
+
// This suite exercises the real 9router DefaultExecutor and model parser checked out
|
|
7
|
+
// at the pinned commit. Only the Postman-facing fetch inside the facade is injected.
|
|
8
|
+
const routerRoot = new URL('../../../tmp/9router-pinned-17c4cc7/', import.meta.url);
|
|
9
|
+
const { DefaultExecutor } = await import(new URL('open-sse/executors/default.js', routerRoot));
|
|
10
|
+
const { parseModel } = await import(new URL('open-sse/services/model.js', routerRoot));
|
|
11
|
+
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
const sseResponse = frames => new Response(new ReadableStream({
|
|
14
|
+
start(controller) {
|
|
15
|
+
for (const frame of frames) controller.enqueue(encoder.encode(`event: message\ndata: ${JSON.stringify(frame)}\n\n`));
|
|
16
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
17
|
+
controller.close();
|
|
18
|
+
},
|
|
19
|
+
}), { status: 200, headers: { 'content-type': 'text/event-stream' } });
|
|
20
|
+
|
|
21
|
+
const textFrames = [
|
|
22
|
+
{ eventType: 'conversation', data: { id: 'conv_fixture' } },
|
|
23
|
+
{ eventType: 'textChunk', data: { textContent: 'hello' } },
|
|
24
|
+
];
|
|
25
|
+
const toolFrames = [
|
|
26
|
+
{ eventType: 'conversation', data: { id: 'conv_tool' } },
|
|
27
|
+
{ eventType: 'toolCallChunk', data: { id: 'call_fixture', index: 0, name: 'openai__lookup', toolCallGroupId: 'group_fixture' } },
|
|
28
|
+
{ eventType: 'toolCallChunk', data: { id: 'call_fixture', index: 0, arguments: '{"q":' } },
|
|
29
|
+
{ eventType: 'toolCallChunk', data: { id: 'call_fixture', index: 0, arguments: '"x"}' } },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const config = {
|
|
33
|
+
host: '127.0.0.1', port: 0, apiKey: 'router-to-facade-key', upstreamToken: 'fixture-upstream-token',
|
|
34
|
+
workspaceId: 'ws_fixture', upstreamOrigin: 'https://fixture.invalid', chatPath: '/chat', appVersion: 'integration/0.1',
|
|
35
|
+
models: ['fixture-model'], timeoutMs: 1000, maxBodyBytes: 1024 * 1024, maxEventBytes: 1024 * 1024,
|
|
36
|
+
maxOutputBytes: 1024 * 1024, maxToolArgumentBytes: 1024 * 1024, maxTools: 64, maxConcurrent: 8,
|
|
37
|
+
exposeHealthDetails: false, correlationEnabled: true, principalKey: 'router-test-principal', credentialSlotId: 'router-client',
|
|
38
|
+
maxToolResultBytes: 1024 * 1024, correlationPendingTtlMs: 1000, correlationTerminalTtlMs: 1000, correlationInflightTtlMs: 1000,
|
|
39
|
+
correlationMaxGroups: 100, correlationMaxCalls: 100, correlationMaxBytes: 1024 * 1024,
|
|
40
|
+
};
|
|
41
|
+
const log = { debug() {}, info() {}, error() {} };
|
|
42
|
+
const provider = 'openai-compatible-chat-fixture';
|
|
43
|
+
const credentialsFor = baseUrl => ({
|
|
44
|
+
apiKey: 'router-to-facade-key',
|
|
45
|
+
providerSpecificData: { baseUrl: `${baseUrl}/v1`, apiType: 'chat' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
async function withFacade(upstreamFixture, fn, configOverride = {}) {
|
|
49
|
+
const captures = [];
|
|
50
|
+
const server = createProviderServer({ ...config, ...configOverride }, {
|
|
51
|
+
logger: { error() {} },
|
|
52
|
+
fetchImpl: async (url, init) => {
|
|
53
|
+
captures.push({ url: String(url), init, body: JSON.parse(init.body) });
|
|
54
|
+
return upstreamFixture(captures.at(-1));
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
server.listen(0, '127.0.0.1');
|
|
58
|
+
await once(server, 'listening');
|
|
59
|
+
const { port } = server.address();
|
|
60
|
+
try { await fn(`http://127.0.0.1:${port}`, captures); }
|
|
61
|
+
finally { await new Promise(resolve => server.close(resolve)); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function routedModel(value = `${provider}/fixture-model`) {
|
|
65
|
+
const parsed = parseModel(value);
|
|
66
|
+
assert.equal(parsed.provider, provider);
|
|
67
|
+
assert.equal(parsed.model, 'fixture-model');
|
|
68
|
+
return parsed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const baseBody = { model: `${provider}/fixture-model`, messages: [{ role: 'user', content: 'hi' }], stream: false };
|
|
72
|
+
|
|
73
|
+
async function execute(executor, baseUrl, body, signal = new AbortController().signal) {
|
|
74
|
+
const parsed = routedModel(body.model);
|
|
75
|
+
const forwarded = { ...body, model: parsed.model };
|
|
76
|
+
return executor.execute({ model: parsed.model, body: forwarded, stream: forwarded.stream === true, credentials: credentialsFor(baseUrl), signal, log });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
test('pinned 9router executor builds exact URL/auth and strips route prefix for JSON', async () => {
|
|
80
|
+
await withFacade(() => sseResponse(textFrames), async (baseUrl, captures) => {
|
|
81
|
+
const executor = new DefaultExecutor(provider);
|
|
82
|
+
const { response, url, transformedBody, headers } = await execute(executor, baseUrl, baseBody);
|
|
83
|
+
assert.equal(url, `${baseUrl}/v1/chat/completions`);
|
|
84
|
+
assert.equal(headers.Authorization, 'Bearer router-to-facade-key');
|
|
85
|
+
assert.equal(transformedBody.model, 'fixture-model');
|
|
86
|
+
assert.equal(response.status, 200);
|
|
87
|
+
const result = await response.json();
|
|
88
|
+
assert.equal(result.model, 'fixture-model');
|
|
89
|
+
assert.equal(result.choices[0].message.content, 'hello');
|
|
90
|
+
assert.equal(captures.length, 1);
|
|
91
|
+
assert.equal(captures[0].url, 'https://fixture.invalid/chat');
|
|
92
|
+
assert.equal(captures[0].init.headers['x-access-token'], 'fixture-upstream-token');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('pinned 9router executor preserves OpenAI SSE chunks and DONE from facade', async () => {
|
|
97
|
+
await withFacade(() => sseResponse(textFrames), async (baseUrl) => {
|
|
98
|
+
const executor = new DefaultExecutor(provider);
|
|
99
|
+
const { response } = await execute(executor, baseUrl, { ...baseBody, stream: true });
|
|
100
|
+
assert.match(response.headers.get('content-type'), /text\/event-stream/);
|
|
101
|
+
const wire = await response.text();
|
|
102
|
+
assert.match(wire, /"content":"hello"/);
|
|
103
|
+
assert.match(wire, /"finish_reason":"stop"/);
|
|
104
|
+
assert.match(wire, /data: \[DONE\]/);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('pinned 9router executor consumes explicit web_session text JSON and SSE from the facade', async () => {
|
|
109
|
+
const webConfig = { transportStrategy: 'web_session', webSessionCookie: 'dummy-cookie-value', webSessionSubdomain: 'fixture-team', selectedUpstreamModel: 'GPT_56_SOL', webSessionObservedModels: ['gpt-5.6-sol'] };
|
|
110
|
+
const upstreamFixture = () => sseResponse([
|
|
111
|
+
{ eventType: 'conversation', data: { id: 'web-conversation' } },
|
|
112
|
+
{ eventType: 'textChunk', data: { textContent: 'web-session-ok', metadata: { model: 'gpt-5.6-sol' } } },
|
|
113
|
+
]);
|
|
114
|
+
await withFacade(upstreamFixture, async baseUrl => {
|
|
115
|
+
const executor = new DefaultExecutor(provider);
|
|
116
|
+
let result = await execute(executor, baseUrl, baseBody);
|
|
117
|
+
assert.equal(result.response.status, 200); assert.equal((await result.response.json()).choices[0].message.content, 'web-session-ok');
|
|
118
|
+
result = await execute(executor, baseUrl, { ...baseBody, stream: true });
|
|
119
|
+
assert.equal(result.response.status, 200); const wire = await result.response.text(); assert.match(wire, /web-session-ok/); assert.match(wire, /\[DONE\]/);
|
|
120
|
+
}, webConfig);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('pinned 9router executor completes native role=tool roundtrip and consumed replay is 409', async () => {
|
|
124
|
+
let sequence = 0;
|
|
125
|
+
await withFacade(() => sequence++ === 0 ? sseResponse(toolFrames) : sseResponse([{ eventType:'conversation',data:{id:'conv_tool'} },{ eventType:'textChunk',data:{textContent:'native final'} }]), async (baseUrl, captures) => {
|
|
126
|
+
const executor = new DefaultExecutor(provider);
|
|
127
|
+
const tool = { type: 'function', function: { name: 'lookup', description: 'lookup', parameters: { type: 'object', properties: { q: { type: 'string' } }, required: ['q'], additionalProperties: false } } };
|
|
128
|
+
const first = await execute(executor, baseUrl, { ...baseBody, tools: [tool] });
|
|
129
|
+
const toolReply = await first.response.json(); const assistant=toolReply.choices[0].message; const call=assistant.tool_calls[0];
|
|
130
|
+
assert.match(call.id,/^call_pmn_/); assert.notEqual(call.id,'call_fixture');
|
|
131
|
+
const secondBody = { ...baseBody, tools: [tool], messages: [{ role:'user',content:'hi' },assistant,{ role:'tool',tool_call_id:call.id,content:'lookup-result' }] };
|
|
132
|
+
const second=await execute(executor,baseUrl,secondBody); assert.equal(second.response.status,200); assert.equal((await second.response.json()).choices[0].message.content,'native final');
|
|
133
|
+
const replay=await execute(executor,baseUrl,secondBody); assert.equal(replay.response.status,409); assert.equal((await replay.response.json()).error.code,'tool_call_already_consumed');
|
|
134
|
+
assert.equal(captures.length,2); assert.equal(captures[1].body.input.chatType,'TOOL_RESPONSE'); assert.equal(captures[1].body.input.toolCallGroupId,'group_fixture'); assert.equal(captures[1].body.input.toolResponses[0].toolCallId,'call_fixture');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
test('pinned 9router executor completes web_session tool call -> result -> final against injected mock', async () => {
|
|
138
|
+
const webConfig = { transportStrategy: 'web_session', webSessionCookie: 'dummy-cookie-value', webSessionSubdomain: 'fixture-team', selectedUpstreamModel: 'GPT_56_SOL', webSessionObservedModels: ['gpt-5.6-sol'] };
|
|
139
|
+
let sequence=0;
|
|
140
|
+
const upstreamFixture=()=>sequence++===0?sseResponse([
|
|
141
|
+
{eventType:'conversation',data:{id:'web-tool-conv'}},
|
|
142
|
+
{eventType:'toolCallChunk',data:{id:'web-up-call',index:0,name:'openai__lookup',arguments:'{"q":"x"}',metadata:{model:'gpt-5.6-sol'}}},
|
|
143
|
+
]):sseResponse([{eventType:'conversation',data:{id:'web-tool-conv'}},{eventType:'textChunk',data:{textContent:'web tool final',metadata:{model:'gpt-5.6-sol'}}}]);
|
|
144
|
+
await withFacade(upstreamFixture,async baseUrl=>{
|
|
145
|
+
const executor=new DefaultExecutor(provider);const tool={type:'function',function:{name:'lookup',parameters:{type:'object',properties:{q:{type:'string'}},required:['q'],additionalProperties:false}}};
|
|
146
|
+
const first=await execute(executor,baseUrl,{...baseBody,tools:[tool]});const assistant=(await first.response.json()).choices[0].message;const second=await execute(executor,baseUrl,{...baseBody,tools:[tool],messages:[{role:'user',content:'hi'},assistant,{role:'tool',tool_call_id:assistant.tool_calls[0].id,content:'ok'}]});assert.equal((await second.response.json()).choices[0].message.content,'web tool final');
|
|
147
|
+
},webConfig);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('pinned 9router executor returns facade auth/model errors without live fallback', async () => {
|
|
151
|
+
await withFacade(() => sseResponse(textFrames), async (baseUrl) => {
|
|
152
|
+
const executor = new DefaultExecutor(provider);
|
|
153
|
+
const badCredentials = { apiKey: 'wrong', providerSpecificData: { baseUrl: `${baseUrl}/v1`, apiType: 'chat' } };
|
|
154
|
+
const auth = await executor.execute({ model: 'fixture-model', body: { ...baseBody, model: 'fixture-model' }, stream: false, credentials: badCredentials, signal: new AbortController().signal, log });
|
|
155
|
+
assert.equal(auth.response.status, 401);
|
|
156
|
+
assert.equal((await auth.response.json()).error.code, 'invalid_api_key');
|
|
157
|
+
const model = await executor.execute({ model: 'other', body: { ...baseBody, model: 'other' }, stream: false, credentials: credentialsFor(baseUrl), signal: new AbortController().signal, log });
|
|
158
|
+
assert.equal(model.response.status, 400);
|
|
159
|
+
assert.equal((await model.response.json()).error.code, 'model_not_allowed');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('pinned 9router executor sees post-header error without successful DONE', async () => {
|
|
164
|
+
await withFacade(() => sseResponse([
|
|
165
|
+
{ eventType: 'conversation', data: { id: 'conv_error' } },
|
|
166
|
+
{ eventType: 'textChunk', data: { textContent: 'partial' } },
|
|
167
|
+
{ eventType: 'error', data: { errorType: 'LLM_STREAM_ERROR', message: 'Bearer secret-value' } },
|
|
168
|
+
]), async (baseUrl) => {
|
|
169
|
+
const executor = new DefaultExecutor(provider);
|
|
170
|
+
const { response } = await execute(executor, baseUrl, { ...baseBody, stream: true });
|
|
171
|
+
assert.equal(response.status, 200);
|
|
172
|
+
const wire = await response.text();
|
|
173
|
+
assert.match(wire, /"content":"partial"/);
|
|
174
|
+
assert.match(wire, /"code":"upstream_stream_error"/);
|
|
175
|
+
assert.equal(wire.includes('secret-value'), false);
|
|
176
|
+
assert.equal(wire.includes('finish_reason":"stop"'), false);
|
|
177
|
+
assert.equal(wire.includes('[DONE]'), false);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('pinned 9router executor receives pre-header tool validation error as JSON', async () => {
|
|
182
|
+
await withFacade(() => sseResponse([{ eventType: 'error', errorType: 'TOOL_VALIDATION_ERROR', message: 'secret-value' }]), async (baseUrl) => {
|
|
183
|
+
const executor = new DefaultExecutor(provider);
|
|
184
|
+
const { response } = await execute(executor, baseUrl, baseBody);
|
|
185
|
+
assert.equal(response.status, 400);
|
|
186
|
+
const wire = await response.text();
|
|
187
|
+
const value = JSON.parse(wire);
|
|
188
|
+
assert.equal(value.error.code, 'upstream_tool_validation_error');
|
|
189
|
+
assert.equal(wire.includes('secret-value'), false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('pinned 9router executor propagates abort through facade to injected upstream', async () => {
|
|
194
|
+
let upstreamAborted = false;
|
|
195
|
+
await withFacade(capture => new Promise((resolve, reject) => {
|
|
196
|
+
capture.init.signal.addEventListener('abort', () => { upstreamAborted = true; reject(capture.init.signal.reason); }, { once: true });
|
|
197
|
+
}), async (baseUrl) => {
|
|
198
|
+
const executor = new DefaultExecutor(provider);
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const pending = execute(executor, baseUrl, baseBody, controller.signal);
|
|
201
|
+
setTimeout(() => controller.abort(new DOMException('integration abort', 'AbortError')), 30);
|
|
202
|
+
await assert.rejects(pending, error => error?.name === 'AbortError');
|
|
203
|
+
await new Promise(resolve => setTimeout(resolve, 30));
|
|
204
|
+
assert.equal(upstreamAborted, true);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { authStatus, discoverPostmanDesktop, saveAuthConfig, validateAuthInput } from '../src/admin-auth.mjs';
|
|
7
|
+
|
|
8
|
+
const valid = { sessionCookie: 'session-secret-1234', subdomain: 'team-a', workspaceId: 'workspace-1', selectedModel: 'GPT_56_SOL' };
|
|
9
|
+
|
|
10
|
+
test('auth input validates bounded fields without echo helpers', () => {
|
|
11
|
+
assert.deepEqual(validateAuthInput(valid), valid);
|
|
12
|
+
assert.throws(() => validateAuthInput({ ...valid, sessionCookie: 'bad;cookie' }), /unsupported/);
|
|
13
|
+
assert.throws(() => validateAuthInput({ ...valid, subdomain: 'Bad Host' }), /invalid/);
|
|
14
|
+
const status = authStatus({ transportStrategy: 'web_session', webSessionCookie: valid.sessionCookie, webSessionSubdomain: valid.subdomain, workspaceId: valid.workspaceId, selectedUpstreamModel: valid.selectedModel });
|
|
15
|
+
assert.equal(status.cookie.masked, '••••1234');
|
|
16
|
+
assert.equal(JSON.stringify(status).includes(valid.sessionCookie), false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('desktop discovery is bounded metadata-only and never imports session', async () => {
|
|
20
|
+
const root = await mkdtemp(join(tmpdir(), 'postman-discovery-'));
|
|
21
|
+
const roaming = join(root, 'roaming'); const local = join(root, 'local');
|
|
22
|
+
const fs = await import('node:fs/promises');
|
|
23
|
+
await Promise.all([fs.mkdir(join(roaming, 'Postman'), { recursive: true }), fs.mkdir(join(local, 'Postman'), { recursive: true })]);
|
|
24
|
+
await fs.writeFile(join(local, 'Postman', 'Postman.exe'), '');
|
|
25
|
+
const found = await discoverPostmanDesktop({ APPDATA: roaming, LOCALAPPDATA: local });
|
|
26
|
+
assert.equal(found.installed, true); assert.equal(found.profileDetected, true);
|
|
27
|
+
assert.equal(found.sessionAutoImportAvailable, false); assert.equal(found.manualCredentialRequired, true);
|
|
28
|
+
assert.equal(JSON.stringify(found).includes('session-secret'), false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('auth config failed pre-rename apply restores original file', async () => {
|
|
32
|
+
const root = await mkdtemp(join(tmpdir(), 'postman-auth-fault-')); const envFile = join(root, '.env');
|
|
33
|
+
const original = 'PROVIDER_PORT=8788\nPOSTMAN_SESSION_COOKIE=original\n'; await writeFile(envFile, original, 'utf8');
|
|
34
|
+
await assert.rejects(() => saveAuthConfig(valid, { envFile, beforeRename() { throw new Error('injected failure'); } }), /injected failure/);
|
|
35
|
+
assert.equal(await readFile(envFile, 'utf8'), original);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('auth config save creates backup and atomically preserves unrelated fields', async () => {
|
|
39
|
+
const root = await mkdtemp(join(tmpdir(), 'postman-auth-save-')); const envFile = join(root, '.env');
|
|
40
|
+
await writeFile(envFile, 'PROVIDER_PORT=8788\nUNRELATED=keep\nPOSTMAN_SESSION_COOKIE=old\n', 'utf8');
|
|
41
|
+
const result = await saveAuthConfig(valid, { envFile });
|
|
42
|
+
assert.equal(result.saved, true); assert.equal(result.restartRequired, true);
|
|
43
|
+
assert.equal(JSON.stringify(result).includes(valid.sessionCookie), false);
|
|
44
|
+
const updated = await readFile(envFile, 'utf8'); const backup = await readFile(result.backupFile, 'utf8');
|
|
45
|
+
assert.match(updated, /UNRELATED=keep/); assert.match(updated, /POSTMAN_SESSION_COOKIE=session-secret-1234/); assert.match(updated, /POSTMAN_WORKSPACE_SUBDOMAIN=team-a/);
|
|
46
|
+
assert.match(backup, /POSTMAN_SESSION_COOKIE=old/);
|
|
47
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { once } from 'node:events';
|
|
4
|
+
import http from 'node:http';
|
|
5
|
+
import { createProviderServer } from '../src/server.mjs';
|
|
6
|
+
import { adminStaticAssets, isAllowedAdminHost, isLoopbackAddress } from '../src/admin.mjs';
|
|
7
|
+
import { config, listen } from './helpers.mjs';
|
|
8
|
+
|
|
9
|
+
const secret = 'sk-pmn-test-secret-never-static';
|
|
10
|
+
|
|
11
|
+
async function withServer(fn) {
|
|
12
|
+
const server = createProviderServer({ ...config, apiKey: secret }, { fetchImpl: async () => { throw new Error('unexpected upstream'); }, logger: { error() {} } });
|
|
13
|
+
const base = await listen(server);
|
|
14
|
+
try { await fn(base, server); }
|
|
15
|
+
finally { server.close(); server.closeAllConnections?.(); await once(server, 'close'); }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('admin loopback and Host validators fail closed', () => {
|
|
19
|
+
for (const value of ['127.0.0.1', '127.4.5.6', '::1', '::ffff:127.0.0.2']) assert.equal(isLoopbackAddress(value), true, value);
|
|
20
|
+
for (const value of ['10.0.0.1', '192.168.1.2', '::2', '::ffff:10.0.0.1', undefined]) assert.equal(isLoopbackAddress(value), false, String(value));
|
|
21
|
+
for (const host of ['localhost:8788', '127.0.0.1:8788', '[::1]:8788']) assert.equal(isAllowedAdminHost(host, 8788), true, host);
|
|
22
|
+
for (const host of ['attacker.test:8788', 'localhost:9999', '127.0.0.2:8788', undefined]) assert.equal(isAllowedAdminHost(host, 8788), false, String(host));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('admin static assets never embed configured API key', () => {
|
|
26
|
+
for (const [name, value] of Object.entries(adminStaticAssets)) {
|
|
27
|
+
assert.equal(value.includes(secret), false, name);
|
|
28
|
+
assert.equal(value.includes('PROVIDER_API_KEY'), false, name);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('admin GUI/config are localhost-only and credentials require explicit POST', async () => {
|
|
33
|
+
await withServer(async base => {
|
|
34
|
+
const gui = await fetch(`${base}/admin`);
|
|
35
|
+
assert.equal(gui.status, 200);
|
|
36
|
+
const html = await gui.text();
|
|
37
|
+
assert.match(html, /Aki Pro Max <em>Control/);
|
|
38
|
+
assert.doesNotMatch(html, /Postman2API/);
|
|
39
|
+
assert.equal(html.includes(secret), false);
|
|
40
|
+
assert.match(gui.headers.get('content-security-policy'), /default-src 'self'/);
|
|
41
|
+
assert.equal(gui.headers.get('access-control-allow-origin'), null);
|
|
42
|
+
|
|
43
|
+
const configResponse = await fetch(`${base}/admin/config`);
|
|
44
|
+
assert.equal(configResponse.status, 200);
|
|
45
|
+
assert.equal(configResponse.headers.get('cache-control'), 'no-store');
|
|
46
|
+
const metadata = await configResponse.json();
|
|
47
|
+
assert.equal(metadata.apiKey, undefined);
|
|
48
|
+
assert.deepEqual(metadata.models, config.models);
|
|
49
|
+
|
|
50
|
+
const noGetSecret = await fetch(`${base}/admin/credentials`);
|
|
51
|
+
assert.equal(noGetSecret.status, 404);
|
|
52
|
+
const credentials = await fetch(`${base}/admin/credentials`, { method: 'POST', headers: { 'content-type': 'application/json', origin: base }, body: '{}' });
|
|
53
|
+
assert.equal(credentials.status, 200);
|
|
54
|
+
assert.equal((await credentials.json()).apiKey, secret);
|
|
55
|
+
const crossOrigin = await fetch(`${base}/admin/credentials`, { method: 'POST', headers: { 'content-type': 'application/json', origin: 'http://attacker.test' }, body: '{}' });
|
|
56
|
+
assert.equal(crossOrigin.status, 403);
|
|
57
|
+
|
|
58
|
+
const target = new URL(base);
|
|
59
|
+
const rawStatus = await new Promise((resolve, reject) => {
|
|
60
|
+
const request = http.request({ hostname: target.hostname, port: target.port, path: '/admin/config', method: 'GET', headers: { Host: `attacker.test:${target.port}` } }, response => {
|
|
61
|
+
response.resume(); response.on('end', () => resolve(response.statusCode));
|
|
62
|
+
});
|
|
63
|
+
request.on('error', reject); request.end();
|
|
64
|
+
});
|
|
65
|
+
assert.equal(rawStatus, 403);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from 'node:test';import assert from 'node:assert/strict';
|
|
2
|
+
import { loadConfig, authenticate, assertModel, upstreamUrl, ConcurrencyGate } from '../src/config.mjs';import { ProviderError } from '../src/errors.mjs';import { validateSchemaDefinition, validateValue } from '../src/schema.mjs';
|
|
3
|
+
const env={PROVIDER_API_KEY:'a',POSTMAN_ACCESS_TOKEN:'b',POSTMAN_WORKSPACE_ID:'w',POSTMAN_ORIGIN:'https://gateway.example.test',PROVIDER_MODELS:'m1,m2'};
|
|
4
|
+
test('loads exact HTTPS origin and explicit models',()=>{const c=loadConfig(env);assert.deepEqual(c.models,['m1','m2']);assert.equal(upstreamUrl(c).origin,'https://gateway.example.test');});
|
|
5
|
+
test('rejects origin paths HTTP and unknown models',()=>{assert.throws(()=>loadConfig({...env,POSTMAN_ORIGIN:'http://x.test'}));assert.throws(()=>loadConfig({...env,POSTMAN_ORIGIN:'https://x.test/path'}));assert.throws(()=>assertModel('other',{models:['m']}),/Unknown/);});
|
|
6
|
+
test('bearer is exact and concurrency releases',()=>{authenticate('Bearer abc','abc');assert.throws(()=>authenticate('Bearer ab','abc'),ProviderError);const g=new ConcurrencyGate(1),leave=g.enter();assert.throws(()=>g.enter(),/concurrency/);leave();assert.equal(g.active,0);});
|
|
7
|
+
test('schema subset validates ordinary object tools and rejects unsupported keywords/value',()=>{const s={type:'object',properties:{q:{type:'string'},limit:{type:'integer',minimum:1,maximum:10},mode:{type:'string',enum:['fast','safe']}},required:['q'],additionalProperties:false};validateSchemaDefinition(s);validateValue({q:'x',limit:2,mode:'safe'},s);assert.throws(()=>validateValue({},s));assert.throws(()=>validateSchemaDefinition({type:'string',pattern:'x'}),/Unsupported/);});
|
|
8
|
+
test('schema subset recursively validates patternProperties used by OpenClaw exec.env',()=>{const s={type:'object',patternProperties:{'^(.*)$':{type:'string'}}};validateSchemaDefinition(s);assert.throws(()=>validateSchemaDefinition({type:'object',patternProperties:[]}),/must be an object/);assert.throws(()=>validateSchemaDefinition({type:'object',patternProperties:{'.*':{type:'string',pattern:'x'}}}),/Unsupported JSON Schema keyword pattern/);});
|
|
9
|
+
test('schema definitions reject malformed supported keywords instead of silently under-validating',()=>{assert.throws(()=>validateSchemaDefinition({type:'object',required:'q'}),/array of strings/);assert.throws(()=>validateSchemaDefinition({type:'object',additionalProperties:{type:'string'}}),/booleans only/);assert.throws(()=>validateSchemaDefinition({type:'string',minLength:-1}),/non-negative integer/);assert.throws(()=>validateSchemaDefinition({type:'number',minimum:'0'}),/finite number/);assert.throws(()=>validateSchemaDefinition({type:'string',enum:[]}),/non-empty array/);});
|
|
10
|
+
test('const supports JSON object and array values',()=>{const objectConst={type:'object',const:{mode:'safe'}};validateSchemaDefinition(objectConst);validateValue({mode:'safe'},objectConst);assert.throws(()=>validateValue({mode:'fast'},objectConst));const arrayConst={type:'array',const:[1,2]};validateSchemaDefinition(arrayConst);validateValue([1,2],arrayConst);assert.throws(()=>validateValue([2,1],arrayConst));});
|
|
11
|
+
|
|
12
|
+
test('catalog metadata defaults remain backward compatible and explicit values load',()=>{const defaults=loadConfig(env);assert.equal(defaults.platform,'OPENAI_COMPATIBLE_ADAPTER');assert.equal(defaults.nativeToolsHash,undefined);assert.deepEqual(defaults.excludedTools,[]);const configured=loadConfig({...env,POSTMAN_PLATFORM:'DESKTOP_CUSTOM',POSTMAN_NATIVE_TOOLS_HASH:'catalog-v12:opaque-123',POSTMAN_EXCLUDED_TOOLS:'readFile,webSearch'});assert.equal(configured.platform,'DESKTOP_CUSTOM');assert.equal(configured.nativeToolsHash,'catalog-v12:opaque-123');assert.deepEqual(configured.excludedTools,['readFile','webSearch']);assert.ok(Object.isFrozen(configured.excludedTools));});
|
|
13
|
+
test('catalog metadata accepts exact documented bounds',()=>{const c=loadConfig({...env,POSTMAN_PLATFORM:'P'.repeat(64),POSTMAN_NATIVE_TOOLS_HASH:'H'.repeat(256),POSTMAN_EXCLUDED_TOOLS:Array.from({length:64},(_,i)=>{const prefix=`tool${i}`;return `${prefix}${'x'.repeat(128-prefix.length)}`;}).join(',')});assert.equal(c.platform.length,64);assert.equal(c.nativeToolsHash.length,256);assert.equal(c.excludedTools.length,64);assert.ok(c.excludedTools.every(value=>value.length===128));});
|
|
14
|
+
test('catalog metadata rejects malformed ambiguous and unbounded config',()=>{for(const patch of [{POSTMAN_PLATFORM:'bad value'},{POSTMAN_PLATFORM:'x'.repeat(65)},{POSTMAN_NATIVE_TOOLS_HASH:'hash\nforged'},{POSTMAN_NATIVE_TOOLS_HASH:'x'.repeat(257)},{POSTMAN_EXCLUDED_TOOLS:'readFile,readFile'},{POSTMAN_EXCLUDED_TOOLS:'readFile,,webSearch'},{POSTMAN_EXCLUDED_TOOLS:'bad tool'},{POSTMAN_EXCLUDED_TOOLS:'x'.repeat(129)},{POSTMAN_EXCLUDED_TOOLS:Array.from({length:65},(_,i)=>`tool${i}`).join(',')},{POSTMAN_PLATFORM:null},{POSTMAN_PLATFORM:42},{POSTMAN_NATIVE_TOOLS_HASH:{}},{POSTMAN_NATIVE_TOOLS_HASH:[]},{POSTMAN_EXCLUDED_TOOLS:null},{POSTMAN_EXCLUDED_TOOLS:['readFile']}])assert.throws(()=>loadConfig({...env,...patch}),/POSTMAN_(?:PLATFORM|NATIVE_TOOLS_HASH|EXCLUDED_TOOLS)/);});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from 'node:test';import assert from 'node:assert/strict';import { once } from 'node:events';
|
|
2
|
+
import { createProviderServer } from '../src/server.mjs';import { config,sseResponse,listen,request,json } from './helpers.mjs';
|
|
3
|
+
const textEvents=[{data:{eventType:'conversation',data:{id:'conv'}}},{data:{eventType:'textChunk',data:{textContent:'hello'}}},{data:{eventType:'usage',data:{inputTokens:2,outputTokens:1}}}];
|
|
4
|
+
const body={model:'fixture-model',messages:[{role:'user',content:'hi'}]};
|
|
5
|
+
async function withServer(fetchImpl,fn,cfg=config){const server=createProviderServer(cfg,{fetchImpl,logger:{error(){}}});const base=await listen(server);try{await fn(base,server);}finally{server.close();server.closeAllConnections?.();await once(server,'close');}}
|
|
6
|
+
test('health public models authenticated and chat JSON works',async()=>{let captured;await withServer(async(url,init)=>{captured={url:String(url),init};return sseResponse(textEvents);},async base=>{assert.equal((await json(await fetch(base+'/health'))).upstream_verified,false);assert.equal((await request(base,'/v1/models',{token:'bad'})).status,401);const models=await json(await request(base,'/v1/models'));assert.equal(models.data[0].id,'fixture-model');const result=await json(await request(base,'/v1/chat/completions',{method:'POST',body}));assert.equal(result.choices[0].message.content,'hello');assert.equal(captured.init.headers['x-access-token'],'upstream-secret');assert.equal(JSON.parse(captured.init.body).mandatoryContext.workspaceId,'ws_fixture');});});
|
|
7
|
+
test('stream emits chunks finish usage and DONE',async()=>{await withServer(async()=>sseResponse(textEvents),async base=>{const res=await request(base,'/v1/chat/completions',{method:'POST',body:{...body,stream:true}});assert.match(res.headers.get('content-type'),/text\/event-stream/);const value=await res.text();assert.match(value,/chat\.completion\.chunk/);assert.match(value,/"content":"hello"/);assert.match(value,/\[DONE\]/);});});
|
|
8
|
+
test('auth model invalid JSON and body limit errors',async()=>{await withServer(async()=>sseResponse(textEvents),async base=>{assert.equal((await request(base,'/v1/chat/completions',{method:'POST',token:'bad',body})).status,401);assert.equal((await request(base,'/v1/chat/completions',{method:'POST',body:{...body,model:'nope'}})).status,400);const bad=await fetch(base+'/v1/chat/completions',{method:'POST',headers:{authorization:'Bearer ingress-secret','content-type':'application/json'},body:'{'});assert.equal(bad.status,400);const large=await request(base,'/v1/chat/completions',{method:'POST',body:{...body,messages:[{role:'user',content:'x'.repeat(5000)}]}});assert.equal(large.status,413);});});
|
|
9
|
+
test('upstream HTTP statuses non-SSE malformed and incomplete map cleanly',async()=>{for(const [factory,status,code] of [[()=>new Response('x',{status:401}),401,'upstream_http_error'],[()=>new Response('x',{status:403}),403,'upstream_http_error'],[()=>new Response('x',{status:429,headers:{'retry-after':'2'}}),429,'upstream_http_error'],[()=>new Response('x',{status:500}),500,'upstream_http_error'],[()=>new Response('<html>',{status:200,headers:{'content-type':'text/html'}}),502,'upstream_content_type_error'],[()=>new Response(new ReadableStream({start(c){c.enqueue(new TextEncoder().encode('data: not-json\\n\\ndata: [DONE]\\n\\n'));c.close();}}),{status:200,headers:{'content-type':'text/event-stream'}}),502,'upstream_invalid_json'],[()=>sseResponse(textEvents,{done:false}),502,'upstream_incomplete_stream']])await withServer(async()=>factory(),async base=>{const res=await request(base,'/v1/chat/completions',{method:'POST',body});assert.equal(res.status,status);const value=await res.json();assert.equal(value.error.code,code);});});
|
|
10
|
+
test('concurrent requests remain isolated and enforce bound',async()=>{let releases=[];const slow=async(_u,init)=>new Promise((resolve,reject)=>{const abort=()=>reject(init.signal.reason);init.signal.addEventListener('abort',abort,{once:true});releases.push(()=>{init.signal.removeEventListener('abort',abort);resolve(sseResponse(textEvents));});});await withServer(slow,async base=>{const p1=request(base,'/v1/chat/completions',{method:'POST',body});while(!releases.length)await new Promise(r=>setTimeout(r,1));const p2=request(base,'/v1/chat/completions',{method:'POST',body});while(releases.length<2)await new Promise(r=>setTimeout(r,1));const p3=await request(base,'/v1/chat/completions',{method:'POST',body});assert.equal(p3.status,429);releases.splice(0).forEach(f=>f());const [a,b]=await Promise.all([p1,p2]);assert.equal((await a.json()).choices[0].message.content,'hello');assert.equal((await b.json()).choices[0].message.content,'hello');});});
|
|
11
|
+
test('client abort propagates to injected fetch',async()=>{let aborted=false;await withServer(async(_u,init)=>new Promise((resolve,reject)=>init.signal.addEventListener('abort',()=>{aborted=true;reject(init.signal.reason);},{once:true})),async base=>{const c=new AbortController();const p=request(base,'/v1/chat/completions',{method:'POST',body,signal:c.signal}).catch(()=>null);setTimeout(()=>c.abort(),20);await p;await new Promise(r=>setTimeout(r,30));assert.equal(aborted,true);});});
|
|
12
|
+
test('pre-header source-backed failures return sanitized JSON and no success body',async()=>{for(const [failure,status,code] of [[{data:{result:'failure',message:'Bearer top-secret raw body'}},502,'upstream_gateway_failure'],[{data:{eventType:'error',errorType:'TOOL_VALIDATION_ERROR',message:'top-secret'}},400,'upstream_tool_validation_error'],[{data:{eventType:'usage',data:{usageState:'BLOCKED',blockedUntil:'2026-09-06T19:17:37.905Z'}}},400,'upstream_credit_blocked']])await withServer(async()=>sseResponse([failure]),async base=>{const res=await request(base,'/v1/chat/completions',{method:'POST',body});assert.equal(res.status,status);const wire=await res.text();const value=JSON.parse(wire);assert.equal(value.error.code,code);assert.equal(wire.includes('top-secret'),false);assert.equal(wire.includes('blockedUntil'),false);assert.equal(wire.includes('chat.completion'),false);});});
|
|
13
|
+
test('post-header stream failure emits error frame then closes without finish or DONE',async()=>{await withServer(async()=>sseResponse([{data:{eventType:'conversation',data:{id:'conv'}}},{data:{eventType:'textChunk',data:{textContent:'partial'}}},{data:{eventType:'error',data:{errorType:'LLM_STREAM_ERROR',message:'Bearer top-secret'}}}]),async base=>{const res=await request(base,'/v1/chat/completions',{method:'POST',body:{...body,stream:true}});assert.equal(res.status,200);const wire=await res.text();assert.match(wire,/"content":"partial"/);assert.match(wire,/"code":"upstream_stream_error"/);assert.equal(wire.includes('top-secret'),false);assert.equal(wire.includes('finish_reason":"stop"'),false);assert.equal(wire.includes('[DONE]'),false);});});
|
|
14
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { CorrelationStore } from '../src/correlation-store.mjs';
|
|
4
|
+
|
|
5
|
+
const binding={tools:[{type:'function',function:{name:'lookup',description:'lookup',parameters:{type:'object'}}}],toolChoice:'auto'};
|
|
6
|
+
const transcript=[{role:'user',content:'hi'},{role:'assistant',content:null,tool_calls:[]}];
|
|
7
|
+
const calls=(n=2)=>Array.from({length:n},(_,i)=>({upstreamToolCallId:`up-${i}`,upstreamGroupId:n>1?'grp':null,originalName:'lookup',argumentsJson:'{}',schemaHash:'schema'}));
|
|
8
|
+
function admit(store,overrides={}){return store.admit({principalId:'P-A',workspaceId:'ws-A',model:'fixture-model',conversationId:'conv',upstreamGroupId:'grp',transcript,toolsBinding:binding,catalogMetadata:{platform:'OPENAI_COMPATIBLE_ADAPTER',excludedTools:[]},calls:calls(),...overrides});}
|
|
9
|
+
const results=g=>g.calls.map((c,i)=>({toolCallId:c.publicToolCallId,content:`R${i}`}));
|
|
10
|
+
const claim=(store,g,overrides={})=>store.claim({principalId:'P-A',results:results(g),workspaceId:'ws-A',model:'fixture-model',transcript,toolsBinding:binding,catalogMetadata:{platform:'OPENAI_COMPATIBLE_ADAPTER',excludedTools:[]},...overrides});
|
|
11
|
+
|
|
12
|
+
test('opaque IDs are random-sized and hide upstream identifiers',()=>{const store=new CorrelationStore();const group=admit(store);for(const call of group.calls){assert.match(call.publicToolCallId,/^call_pmn_[A-Za-z0-9_-]{32}$/);assert.equal(call.publicToolCallId.includes(call.upstreamToolCallId),false);}assert.notEqual(group.calls[0].publicToolCallId,group.calls[1].publicToolCallId);});
|
|
13
|
+
test('complete set claims once; partial duplicate mixed and cross-principal do not mutate',()=>{const store=new CorrelationStore();const a=admit(store),b=admit(store,{conversationId:'conv2',upstreamGroupId:'grp2'});assert.throws(()=>claim(store,a,{results:results(a).slice(0,1)}),e=>e.code==='tool_result_incomplete');assert.throws(()=>claim(store,a,{results:[results(a)[0],results(a)[0],results(a)[1]]}),e=>e.code==='tool_result_group_mismatch');assert.throws(()=>claim(store,a,{results:[results(a)[0],results(b)[1]]}),e=>e.code==='tool_result_group_mismatch');assert.throws(()=>store.claim({principalId:'P-B',results:results(a),workspaceId:'ws-A',model:'fixture-model',transcript,toolsBinding:binding}),e=>e.code==='unknown_tool_call');assert.equal(a.state,'PENDING');claim(store,a);assert.equal(a.state,'INFLIGHT');assert.throws(()=>claim(store,a),e=>e.code==='continuation_inflight');});
|
|
14
|
+
test('model workspace tools history and conflicting replay are bound',()=>{for(const [field,value,code] of [['model','other','continuation_model_mismatch'],['workspaceId','ws-B','continuation_workspace_mismatch'],['transcript',[{role:'user',content:'bye'}],'continuation_history_mismatch'],['toolsBinding',{...binding,toolChoice:'none'},'continuation_tools_mismatch']]){const store=new CorrelationStore();const group=admit(store);assert.throws(()=>claim(store,group,{[field]:value}),e=>e.code===code);assert.equal(group.state,'PENDING');}const store=new CorrelationStore();const group=admit(store);claim(store,group);store.complete(group);assert.throws(()=>claim(store,group),e=>e.code==='tool_call_already_consumed');const changed=results(group);changed[0]={...changed[0],content:'different'};assert.throws(()=>claim(store,group,{results:changed}),e=>e.code==='continuation_conflict');});
|
|
15
|
+
test('expiry tombstones, inflight uncertainty, terminal cleanup and no active eviction',()=>{let now=0;const store=new CorrelationStore({pendingTtlMs:10,inflightTtlMs:10,terminalTtlMs:10,maxGroups:1,now:()=>now});const first=admit(store);assert.throws(()=>admit(store,{conversationId:'other'}),e=>e.code==='correlation_capacity_exceeded');assert.ok(store.inspect('P-A',first.calls[0].publicToolCallId));now=11;store.sweep();assert.equal(first.state,'EXPIRED');assert.throws(()=>claim(store,first),e=>e.code==='tool_call_expired');now=22;store.sweep();assert.equal(store.inspect('P-A',first.calls[0].publicToolCallId),null);const second=admit(store);claim(store,second);now=33;store.sweep();assert.equal(second.state,'UNCERTAIN');assert.throws(()=>claim(store,second),e=>e.code==='continuation_outcome_uncertain');});
|
|
16
|
+
test('singular ungrouped groups are admitted; multiple ungrouped calls reject',()=>{const store=new CorrelationStore();const one=admit(store,{upstreamGroupId:null,calls:calls(1)});assert.equal(one.calls.length,1);assert.throws(()=>admit(store,{upstreamGroupId:null,calls:calls(2).map(c=>({...c,upstreamGroupId:null}))}),e=>e.code==='upstream_ungrouped_tool_batch');});
|
|
17
|
+
test('50 concurrent claims produce exactly one winner under single-process CAS',async()=>{const store=new CorrelationStore();const group=admit(store);const attempts=await Promise.all(Array.from({length:50},async()=>{try{return claim(store,group).dispatchAttemptId;}catch(e){return e.code;}}));assert.equal(attempts.filter(v=>typeof v==='string'&&!v.startsWith('continuation_')).length,1);assert.equal(attempts.filter(v=>v==='continuation_inflight').length,49);});
|
|
18
|
+
|
|
19
|
+
test('catalog metadata is bound across continuation and mismatch does not mutate',()=>{const store=new CorrelationStore();const metadata={platform:'CUSTOM',nativeToolsHash:'catalog:1',excludedTools:['readFile']};const group=admit(store,{catalogMetadata:metadata});metadata.excludedTools.push('mutated-after-admit');assert.deepEqual(group.catalogMetadata.excludedTools,['readFile']);assert.throws(()=>claim(store,group,{catalogMetadata:{platform:'CUSTOM',nativeToolsHash:'catalog:2',excludedTools:['readFile']}}),error=>error.code==='continuation_catalog_mismatch');assert.equal(group.state,'PENDING');const accepted=claim(store,group,{catalogMetadata:{platform:'CUSTOM',nativeToolsHash:'catalog:1',excludedTools:['readFile']}});assert.equal(accepted.group.state,'INFLIGHT');});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createProviderServer } from '../src/server.mjs';
|
|
4
|
+
import { config as baseConfig, listen, request, sseResponse } from './helpers.mjs';
|
|
5
|
+
|
|
6
|
+
const tool={type:'function',function:{name:'lookup',description:'lookup',parameters:{type:'object',properties:{q:{type:'string'}},required:['q'],additionalProperties:false}}};
|
|
7
|
+
const config={...baseConfig,correlationEnabled:true,principalKey:'test-principal-key',credentialSlotId:'client-a',maxToolResultBytes:1024,correlationPendingTtlMs:1000,correlationTerminalTtlMs:1000,correlationInflightTtlMs:1000,correlationMaxGroups:10,correlationMaxCalls:20,correlationMaxBytes:1024*1024};
|
|
8
|
+
const frames=(conv='conv-1',group='grp-1')=>[
|
|
9
|
+
{event:'message',data:{eventType:'conversation',data:{id:conv}}},
|
|
10
|
+
{event:'message',data:{eventType:'toolCallChunk',data:{id:`${conv}-up-a`,index:0,name:'openai__lookup',toolCallGroupId:group}}},
|
|
11
|
+
{event:'message',data:{eventType:'toolCallChunk',data:{id:`${conv}-up-a`,index:0,arguments:'{"q":"x"}'}}},
|
|
12
|
+
{event:'message',data:{eventType:'toolCallChunk',data:{id:`${conv}-up-b`,index:1,name:'openai__lookup',toolCallGroupId:group}}},
|
|
13
|
+
{event:'message',data:{eventType:'toolCallChunk',data:{id:`${conv}-up-b`,index:1,arguments:'{"q":"y"}'}}},
|
|
14
|
+
];
|
|
15
|
+
async function withServer(fetchImpl,fn,overrides={}){const server=createProviderServer({...config,...overrides},{fetchImpl,logger:{error(){}}});const base=await listen(server);try{await fn(base);}finally{await new Promise(r=>server.close(r));}}
|
|
16
|
+
const initial={model:'fixture-model',messages:[{role:'user',content:'hi'}],tools:[tool],stream:false};
|
|
17
|
+
const continuation=(message,contents=['X','Y'],extra={})=>({model:'fixture-model',messages:[{role:'user',content:'hi'},message,...message.tool_calls.map((call,i)=>({role:'tool',tool_call_id:call.id,content:contents[i]}))],tools:[tool],stream:false,...extra});
|
|
18
|
+
|
|
19
|
+
test('grouped native continuation remaps IDs and sends exact source-backed TOOL_RESPONSE',async()=>{const captures=[];let n=0;await withServer(async(_url,init)=>{captures.push(JSON.parse(init.body));if(n++===0)return sseResponse(frames());return sseResponse([{data:{eventType:'conversation',data:{id:'conv-1'}}},{data:{eventType:'textChunk',data:{textContent:'final'}}}]);},async base=>{const first=await request(base,'/v1/chat/completions',{method:'POST',body:initial});const one=await first.json();const message=one.choices[0].message;assert.equal(message.tool_calls.length,2);assert.match(message.tool_calls[0].id,/^call_pmn_/);assert.equal(JSON.stringify(message).includes('conv-1-up'),false);const second=await request(base,'/v1/chat/completions',{method:'POST',body:continuation(message)});assert.equal(second.status,200);assert.equal((await second.json()).choices[0].message.content,'final');const input=captures[1].input;assert.equal(input.chatType,'TOOL_RESPONSE');assert.equal(input.query,'');assert.equal(input.conversationId,'conv-1');assert.equal(input.toolCallGroupId,'grp-1');assert.deepEqual(input.toolResponses.map(x=>[x.toolCallId,x.content,x.toolResponseStatus]),[['conv-1-up-a','X','SUCCESS'],['conv-1-up-b','Y','SUCCESS']]);});});
|
|
20
|
+
test('partial duplicate history model tools and consumed replay never redispatch',async()=>{let calls=0;await withServer(async()=>{calls++;return calls===1?sseResponse(frames()):sseResponse([{data:{eventType:'conversation',data:{id:'conv-1'}}},{data:{eventType:'textChunk',data:{textContent:'done'}}}]);},async base=>{const message=(await (await request(base,'/v1/chat/completions',{method:'POST',body:initial})).json()).choices[0].message;const partial={...continuation(message),messages:[{role:'user',content:'hi'},message,{role:'tool',tool_call_id:message.tool_calls[0].id,content:'X'}]};assert.equal((await request(base,'/v1/chat/completions',{method:'POST',body:partial})).status,400);assert.equal((await request(base,'/v1/chat/completions',{method:'POST',body:continuation(message,['X','Y'],{model:'other-model'})})).status,400);const history=continuation(message);history.messages[0]={role:'user',content:'changed'};assert.equal((await request(base,'/v1/chat/completions',{method:'POST',body:history})).status,409);const ok=await request(base,'/v1/chat/completions',{method:'POST',body:continuation(message)});assert.equal(ok.status,200);const replay=await request(base,'/v1/chat/completions',{method:'POST',body:continuation(message)});assert.equal(replay.status,409);assert.equal((await replay.json()).error.code,'tool_call_already_consumed');assert.equal(calls,2);});});
|
|
21
|
+
test('streaming tool calls are buffered until admission and expose only opaque IDs',async()=>{await withServer(async()=>sseResponse(frames()),async base=>{const response=await request(base,'/v1/chat/completions',{method:'POST',body:{...initial,stream:true}});assert.equal(response.status,200);const wire=await response.text();assert.match(wire,/call_pmn_/);assert.equal(wire.includes('conv-1-up-a'),false);assert.match(wire,/finish_reason":"tool_calls/);});});
|
|
22
|
+
test('sequential rounds preserve resolved transcript and create a fresh group',async()=>{const captures=[];let n=0;await withServer(async(_url,init)=>{captures.push(JSON.parse(init.body));n++;if(n===1)return sseResponse(frames('conv-1','grp-1'));if(n===2)return sseResponse(frames('conv-2','grp-2'));return sseResponse([{data:{eventType:'conversation',data:{id:'conv-2'}}},{data:{eventType:'textChunk',data:{textContent:'complete'}}}]);},async base=>{const m1=(await (await request(base,'/v1/chat/completions',{method:'POST',body:initial})).json()).choices[0].message;const c1=continuation(m1);const m2=(await (await request(base,'/v1/chat/completions',{method:'POST',body:c1})).json()).choices[0].message;const c2={model:'fixture-model',tools:[tool],messages:[...c1.messages,m2,...m2.tool_calls.map((call,i)=>({role:'tool',tool_call_id:call.id,content:`Z${i}`}))]};const final=await request(base,'/v1/chat/completions',{method:'POST',body:c2});assert.equal(final.status,200);assert.equal((await final.json()).choices[0].message.content,'complete');assert.equal(captures[1].input.chatType,'TOOL_RESPONSE');assert.equal(captures[2].input.chatType,'TOOL_RESPONSE');});});
|
|
23
|
+
test('failed tool stream emits no actionable IDs and admits no correlation record',async()=>{let calls=0;await withServer(async()=>{calls++;return sseResponse([...frames().slice(0,2),{data:{eventType:'error',data:{errorType:'LLM_STREAM_ERROR',message:'call_pmn_fake secret'}}}],{done:false});},async base=>{const res=await request(base,'/v1/chat/completions',{method:'POST',body:{...initial,stream:true}});assert.equal(res.status,200);const wire=await res.text();assert.match(wire,/"code":"upstream_stream_error"/);assert.equal(wire.includes('call_pmn_'),false);assert.equal(wire.includes('[DONE]'),false);assert.equal(calls,1);});});
|
|
24
|
+
test('structured continuation failure after dispatch becomes UNCERTAIN and never redispatches',async()=>{let calls=0;await withServer(async()=>{calls++;if(calls===1)return sseResponse(frames());return sseResponse([{data:{eventType:'error',data:{errorType:'LLM_STREAM_ERROR'}}}]);},async base=>{const message=(await (await request(base,'/v1/chat/completions',{method:'POST',body:initial})).json()).choices[0].message;const first=await request(base,'/v1/chat/completions',{method:'POST',body:continuation(message)});assert.equal(first.status,502);assert.equal((await first.json()).error.code,'upstream_stream_error');const retry=await request(base,'/v1/chat/completions',{method:'POST',body:continuation(message)});assert.equal(retry.status,502);assert.equal((await retry.json()).error.code,'continuation_outcome_uncertain');assert.equal(calls,2);});});
|
|
25
|
+
|
|
26
|
+
test('configured catalog metadata is stable through initial and continuation dispatch',async()=>{const captures=[];let n=0;await withServer(async(_url,init)=>{captures.push(JSON.parse(init.body));return n++===0?sseResponse(frames()):sseResponse([{data:{eventType:'conversation',data:{id:'conv-1'}}},{data:{eventType:'textChunk',data:{textContent:'done'}}}]);},async base=>{const message=(await (await request(base,'/v1/chat/completions',{method:'POST',body:{...initial,platform:'caller-value',nativeToolsHash:'caller-value',excludedTools:['caller-value']}})).json()).choices[0].message;const result=await request(base,'/v1/chat/completions',{method:'POST',body:continuation(message)});assert.equal(result.status,200);assert.equal(captures[0].platform,'CUSTOM_PLATFORM');assert.equal(captures[1].platform,'CUSTOM_PLATFORM');assert.deepEqual(captures[0].clientTools,captures[1].clientTools);assert.equal(captures[0].clientTools.nativeToolsHash,'catalog:authoritative-1');assert.deepEqual(captures[0].clientTools.excludedTools,['readFile']);},{platform:'CUSTOM_PLATFORM',nativeToolsHash:'catalog:authoritative-1',excludedTools:['readFile']});});
|
|
27
|
+
|
|
28
|
+
test('tool_choice required to auto continuation succeeds and changed tool definition still rejects', async () => {
|
|
29
|
+
let calls = 0;
|
|
30
|
+
await withServer(async () => {
|
|
31
|
+
calls++;
|
|
32
|
+
return calls === 1 ? sseResponse(frames()) : sseResponse([{ data: { eventType: 'conversation', data: { id: 'conv-1' } } }, { data: { eventType: 'textChunk', data: { textContent: 'done' } } }]);
|
|
33
|
+
}, async base => {
|
|
34
|
+
const forcedInitial = { ...initial, tool_choice: 'required' };
|
|
35
|
+
const initialRes = await request(base, '/v1/chat/completions', { method: 'POST', body: forcedInitial });
|
|
36
|
+
assert.equal(initialRes.status, 200);
|
|
37
|
+
const message = (await initialRes.json()).choices[0].message;
|
|
38
|
+
const mutatedTool = { type: 'function', function: { name: 'lookup', description: 'lookup', parameters: { type: 'object', properties: { other: { type: 'string' } } } } };
|
|
39
|
+
const badContinuation = { ...continuation(message), tools: [mutatedTool], tool_choice: 'auto' };
|
|
40
|
+
const badRes = await request(base, '/v1/chat/completions', { method: 'POST', body: badContinuation });
|
|
41
|
+
assert.equal(badRes.status, 409);
|
|
42
|
+
assert.equal((await badRes.json()).error.code, 'continuation_tools_mismatch');
|
|
43
|
+
const relaxedContinuation = { ...continuation(message), tool_choice: 'auto' };
|
|
44
|
+
const okRes = await request(base, '/v1/chat/completions', { method: 'POST', body: relaxedContinuation });
|
|
45
|
+
assert.equal(okRes.status, 200);
|
|
46
|
+
assert.equal((await okRes.json()).choices[0].message.content, 'done');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { runEventualToolLoop } from '../scripts/eventual-tool-loop.mjs';
|
|
4
|
+
|
|
5
|
+
const call=(id,name,args)=>({id,type:'function',function:{name,arguments:JSON.stringify(args)}});
|
|
6
|
+
const turn=(toolCalls,content=null)=>({status:200,data:{choices:[{finish_reason:toolCalls.length?'tool_calls':'stop',message:{role:'assistant',content, ...(toolCalls.length?{tool_calls:toolCalls}:{})}}]}});
|
|
7
|
+
const base={model:'m',messages:[{role:'user',content:'use both'}],tools:[],tool_choice:'required'};
|
|
8
|
+
const execute=async(name,args)=>`${name.toUpperCase()}_${args.slot}`;
|
|
9
|
+
const validate=(content,executed)=>[...executed.values()].every(item=>content.includes(item.content));
|
|
10
|
+
|
|
11
|
+
async function scripted(responses, options={}) {
|
|
12
|
+
let index=0; const requests=[];
|
|
13
|
+
const result=await runEventualToolLoop({initialBody:base,requiredTools:['alpha','beta'],executeTool:execute,validateFinal:validate,maxTurns:3,maxDispatches:4,request:async body=>{requests.push(structuredClone(body));return responses[index++]},...options});
|
|
14
|
+
return {result,requests};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('eventual loop accepts two parallel calls and one final turn',async()=>{
|
|
18
|
+
const {result,requests}=await scripted([turn([call('a','alpha',{slot:'a'}),call('b','beta',{slot:'b'})]),turn([], 'ALPHA_a BETA_b')]);
|
|
19
|
+
assert.equal(result.turns,2);assert.equal(result.dispatches,2);assert.deepEqual(result.executed.map(x=>x.name),['alpha','beta']);assert.equal(requests[1].messages.filter(x=>x.role==='tool').length,2);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('eventual loop accepts sequential calls across bounded turns',async()=>{
|
|
23
|
+
const {result,requests}=await scripted([turn([call('a','alpha',{slot:'a'})]),turn([call('b','beta',{slot:'b'})]),turn([], 'ALPHA_a BETA_b')]);
|
|
24
|
+
assert.equal(result.turns,3);assert.equal(result.dispatches,3);assert.deepEqual(result.executed.map(x=>x.name),['alpha','beta']);assert.equal(requests[1].messages.at(-1).role,'tool');assert.equal(requests[2].messages.filter(x=>x.role==='tool').length,2);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('eventual loop rejects replayed signature and duplicate tool name',async()=>{
|
|
28
|
+
await assert.rejects(()=>scripted([turn([call('a','alpha',{slot:'a'})]),turn([call('a2','alpha',{slot:'a'})])]),error=>error.code==='tool_call_replay');
|
|
29
|
+
await assert.rejects(()=>scripted([turn([call('a','alpha',{slot:'a'})]),turn([call('a2','alpha',{slot:'other'})])]),error=>error.code==='tool_called_more_than_once');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('eventual loop rejects stop-before-completion, invalid args, unexpected tool and false final',async()=>{
|
|
33
|
+
await assert.rejects(()=>scripted([turn([call('a','alpha',{slot:'a'})]),turn([], 'early')]),error=>error.code==='assistant_stopped_before_required_tools');
|
|
34
|
+
await assert.rejects(()=>scripted([turn([{id:'a',type:'function',function:{name:'alpha',arguments:'{'}}])]),error=>error.code==='invalid_tool_arguments');
|
|
35
|
+
await assert.rejects(()=>scripted([turn([call('x','gamma',{slot:'x'})])]),error=>error.code==='unexpected_tool_call');
|
|
36
|
+
await assert.rejects(()=>scripted([turn([call('a','alpha',{slot:'a'}),call('b','beta',{slot:'b'})]),turn([], 'wrong')]),error=>error.code==='final_validation_failed');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('eventual loop enforces dispatch and turn budgets',async()=>{
|
|
40
|
+
await assert.rejects(()=>runEventualToolLoop({initialBody:base,requiredTools:['alpha','beta'],executeTool:execute,request:async()=>turn([call('a','alpha',{slot:'a'})]),maxTurns:3,maxDispatches:1}),error=>error.code==='dispatch_budget_exhausted');
|
|
41
|
+
await assert.rejects(()=>runEventualToolLoop({initialBody:base,requiredTools:['alpha','beta'],executeTool:execute,request:async({messages})=>messages.some(x=>x.role==='tool')?turn([call('b','beta',{slot:'b'})]):turn([call('a','alpha',{slot:'a'})]),validateFinal:validate,maxTurns:2,maxDispatches:4}),error=>error.code==='turn_budget_exhausted');
|
|
42
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"fixture_status": "source-derived-not-live-captured",
|
|
3
|
+
"frames": [
|
|
4
|
+
{"event":"message","data":{"eventType":"conversation","data":{"id":"conv_fixture"}}},
|
|
5
|
+
{"event":"message","data":{"eventType":"textChunk","data":{"textContent":"hello"}}},
|
|
6
|
+
{"event":"message","data":{"eventType":"usage","data":{"inputTokens":3,"outputTokens":1}}}
|
|
7
|
+
]
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"fixture_status": "source-derived-not-live-captured",
|
|
3
|
+
"frames": [
|
|
4
|
+
{"event":"message","data":{"eventType":"toolCallChunk","data":{"id":"call_fixture","index":0,"name":"openai__lookup","arguments":"{\"q\":"}}},
|
|
5
|
+
{"event":"message","data":{"eventType":"toolCallChunk","data":{"id":"call_fixture","index":0,"arguments":"\"x\"}"}}}
|
|
6
|
+
]
|
|
7
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provenance": "source-derived synthetic fixture informed by sanitized OWNER-SESSION-TEXT-PROBE metadata; not retained raw captured SSE",
|
|
3
|
+
"routeEvidence": "one owner session returned HTTP 200 text/event-stream, seven textChunk events, exact PROXY_OBSERVATION_OK, explicit DONE, clean EOF, and observed model label gpt-5.6-sol",
|
|
4
|
+
"events": [
|
|
5
|
+
{ "eventType": "conversation", "data": { "id": "conversation_fixture" } },
|
|
6
|
+
{ "eventType": "planningChunk", "data": {} },
|
|
7
|
+
{ "eventType": "textChunk", "data": { "textContent": "PROXY_", "metadata": { "model": "gpt-5.6-sol" } } },
|
|
8
|
+
{ "eventType": "textChunk", "data": { "textContent": "OBSERVATION_" } },
|
|
9
|
+
{ "eventType": "textChunk", "data": { "textContent": "OK" } }
|
|
10
|
+
],
|
|
11
|
+
"done": true
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"provenance": "offline source-derived fixture; synthetic identifiers/content; not captured from a live account",
|
|
3
|
+
"events": [
|
|
4
|
+
{ "eventType": "conversation", "data": { "id": "conv_source_fixture" } },
|
|
5
|
+
{ "eventType": "toolCallChunk", "data": { "toolCalls": [
|
|
6
|
+
{ "index": 0, "id": "up_call_a", "toolCallGroupId": "up_group_1", "function": { "name": "openai__lookup", "arguments": "{\"q\":" } },
|
|
7
|
+
{ "index": 1, "id": "up_call_b", "toolCallGroupId": "up_group_1", "function": { "name": "openai__count", "arguments": "{\"n\":" } }
|
|
8
|
+
] } },
|
|
9
|
+
{ "eventType": "toolCallChunk", "data": { "toolCalls": [
|
|
10
|
+
{ "index": 0, "id": "up_call_a", "toolCallGroupId": "up_group_1", "function": { "name": "openai__lookup", "arguments": "\"x\"}" } },
|
|
11
|
+
{ "index": 1, "id": "up_call_b", "toolCallGroupId": "up_group_1", "function": { "name": "openai__count", "arguments": "2}" } }
|
|
12
|
+
] } }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const root = process.env.NINEROUTER_ROOT;
|
|
6
|
+
if (!root) throw new Error('NINEROUTER_ROOT is required');
|
|
7
|
+
|
|
8
|
+
function candidate(specifier) {
|
|
9
|
+
if (specifier.startsWith('@/')) return path.join(root, 'src', specifier.slice(2));
|
|
10
|
+
if (specifier === 'open-sse') return path.join(root, 'open-sse', 'index.js');
|
|
11
|
+
if (specifier.startsWith('open-sse/')) return path.join(root, specifier);
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
16
|
+
const mapped = candidate(specifier);
|
|
17
|
+
if (!mapped) return nextResolve(specifier, context);
|
|
18
|
+
for (const file of [mapped, `${mapped}.js`, path.join(mapped, 'index.js')]) {
|
|
19
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile()) return { url: pathToFileURL(file).href, shortCircuit: true };
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Cannot resolve isolated 9router alias: ${specifier}`);
|
|
22
|
+
}
|