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,358 @@
|
|
|
1
|
+
import { upstream } from './errors.mjs';
|
|
2
|
+
|
|
3
|
+
const PROGRESS_EVENTS = new Set([
|
|
4
|
+
'thinkingChunk',
|
|
5
|
+
'thinkingComplete',
|
|
6
|
+
'planningChunk',
|
|
7
|
+
'progressUpdate',
|
|
8
|
+
'info',
|
|
9
|
+
'ping',
|
|
10
|
+
'todoChunk',
|
|
11
|
+
'streamingFormat',
|
|
12
|
+
]);
|
|
13
|
+
const UNSUPPORTED_EVENTS = new Map([
|
|
14
|
+
['loopApprovalChunk', ['Postman web-session text profile does not support loop approval.', 'upstream_web_session_loop_approval_unsupported']],
|
|
15
|
+
]);
|
|
16
|
+
const object = value => value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
17
|
+
const stableError = (message, code, status = 502) => upstream(message, code, status);
|
|
18
|
+
const errorView = error => error ? Object.freeze({ status: error.status, code: error.code, message: error.message }) : null;
|
|
19
|
+
const exactStrings = values => {
|
|
20
|
+
if (values === undefined) return Object.freeze([]);
|
|
21
|
+
if (!Array.isArray(values) || values.some(value => typeof value !== 'string' || !value || Buffer.byteLength(value, 'utf8') > 128)) throw new TypeError('allowedObservedModels must contain bounded non-empty strings.');
|
|
22
|
+
return Object.freeze([...new Set(values)]);
|
|
23
|
+
};
|
|
24
|
+
const boundedIdentity = (value, field) => {
|
|
25
|
+
if (value === undefined || value === null) return null;
|
|
26
|
+
if (typeof value !== 'string' || !value || Buffer.byteLength(value, 'utf8') > 128) throw new TypeError(`${field} must be a bounded non-empty string when provided.`);
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
const positiveBound = (value, field, fallback) => {
|
|
30
|
+
const number = value ?? fallback;
|
|
31
|
+
if (!Number.isSafeInteger(number) || number < 1) throw new TypeError(`${field} must be a positive safe integer.`);
|
|
32
|
+
return number;
|
|
33
|
+
};
|
|
34
|
+
const eventName = (event, frameEvent) => {
|
|
35
|
+
if (typeof event.eventType === 'string' && event.eventType) return event.eventType;
|
|
36
|
+
if (typeof event.type === 'string' && event.type) return event.type;
|
|
37
|
+
return frameEvent || 'message';
|
|
38
|
+
};
|
|
39
|
+
const firstString = values => values.find(value => typeof value === 'string' && value.length) ?? null;
|
|
40
|
+
const upstreamFailureDetail = event => {
|
|
41
|
+
const data = object(event?.data);
|
|
42
|
+
const nested = object(data?.error) || object(event?.error);
|
|
43
|
+
const code = firstString([nested?.code, data?.code, event?.code, nested?.type, data?.type]);
|
|
44
|
+
const requestId = firstString([nested?.requestId, nested?.request_id, data?.requestId, data?.request_id, event?.requestId, event?.request_id]);
|
|
45
|
+
const message = firstString([nested?.message, typeof data?.error === 'string' ? data.error : null, typeof event?.error === 'string' ? event.error : null, data?.message, event?.message]);
|
|
46
|
+
const bounded = value => typeof value === 'string' ? value.slice(0, 200).replace(/[\r\n]+/g, ' ') : null;
|
|
47
|
+
const safeMessage = bounded(message)?.replace(/(?:bearer|authorization|api[_-]?key|token|cookie|secret)[^,;]*/gi, '[redacted]') ?? null;
|
|
48
|
+
return Object.freeze({ code: bounded(code), requestId: bounded(requestId), message: safeMessage });
|
|
49
|
+
};
|
|
50
|
+
const firstModelField = entries => {
|
|
51
|
+
const found = entries.find(([, value]) => typeof value === 'string' && value.length);
|
|
52
|
+
return found ? { fieldPath: found[0], value: found[1] } : null;
|
|
53
|
+
};
|
|
54
|
+
const observedModelFor = event => firstModelField([
|
|
55
|
+
['metadata.model', event?.metadata?.model],
|
|
56
|
+
['metadata.modelId', event?.metadata?.modelId],
|
|
57
|
+
['metadata.selectedModel', event?.metadata?.selectedModel],
|
|
58
|
+
['data.metadata.model', event?.data?.metadata?.model],
|
|
59
|
+
['data.metadata.modelId', event?.data?.metadata?.modelId],
|
|
60
|
+
['data.metadata.selectedModel', event?.data?.metadata?.selectedModel],
|
|
61
|
+
['data.model', event?.data?.model],
|
|
62
|
+
['data.modelId', event?.data?.modelId],
|
|
63
|
+
['data.selectedModel', event?.data?.selectedModel],
|
|
64
|
+
]);
|
|
65
|
+
const nonNegativeSafe = value => Number.isSafeInteger(value) && value >= 0;
|
|
66
|
+
|
|
67
|
+
export function createWebSessionEventParser(options = {}) {
|
|
68
|
+
const maxEventBytes = positiveBound(options.maxEventBytes, 'maxEventBytes', 1_048_576);
|
|
69
|
+
const maxOutputBytes = positiveBound(options.maxOutputBytes, 'maxOutputBytes', 4_194_304);
|
|
70
|
+
const maxObservationBytes = positiveBound(options.maxObservationBytes, 'maxObservationBytes', Math.min(maxEventBytes, 4096));
|
|
71
|
+
const maxEvents = positiveBound(options.maxEvents, 'maxEvents', 4096);
|
|
72
|
+
const requestedModel = boundedIdentity(options.requestedModel, 'requestedModel');
|
|
73
|
+
const selectedUpstreamModel = boundedIdentity(options.selectedUpstreamModel, 'selectedUpstreamModel');
|
|
74
|
+
const allowedObservedModels = exactStrings(options.allowedObservedModels);
|
|
75
|
+
const toolNameMap = options.toolNameMap instanceof Map ? options.toolNameMap : new Map();
|
|
76
|
+
const maxToolArgumentBytes = positiveBound(options.maxToolArgumentBytes, 'maxToolArgumentBytes', 1_048_576);
|
|
77
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
78
|
+
|
|
79
|
+
let buffer = '';
|
|
80
|
+
let frameEvent = '';
|
|
81
|
+
let frameData = [];
|
|
82
|
+
let frameBytes = 0;
|
|
83
|
+
let text = '';
|
|
84
|
+
let outputBytes = 0;
|
|
85
|
+
let toolArgumentBytes = 0;
|
|
86
|
+
const toolCalls = new Map();
|
|
87
|
+
let conversationId = null;
|
|
88
|
+
let usage = null;
|
|
89
|
+
let observedUpstreamModel = null;
|
|
90
|
+
let modelVerification = 'unobserved';
|
|
91
|
+
let explicitTerminal = null;
|
|
92
|
+
let transportEof = false;
|
|
93
|
+
let parserBufferComplete = false;
|
|
94
|
+
let sawValidEvent = false;
|
|
95
|
+
let sawMeaningfulOutput = false;
|
|
96
|
+
let eventCount = 0;
|
|
97
|
+
let ended = false;
|
|
98
|
+
let terminalError = null;
|
|
99
|
+
let outcome = 'open';
|
|
100
|
+
const eventTypes = new Set();
|
|
101
|
+
|
|
102
|
+
const fail = error => {
|
|
103
|
+
terminalError ||= error;
|
|
104
|
+
outcome = 'failed';
|
|
105
|
+
throw terminalError;
|
|
106
|
+
};
|
|
107
|
+
const addFrameBytes = value => {
|
|
108
|
+
frameBytes += Buffer.byteLength(value, 'utf8') + 1;
|
|
109
|
+
if (frameBytes > maxEventBytes) fail(stableError('Upstream SSE event exceeds the configured limit.', 'upstream_event_too_large'));
|
|
110
|
+
};
|
|
111
|
+
const boundedObservation = (value, code, message) => {
|
|
112
|
+
if (typeof value !== 'string' || !value || Buffer.byteLength(value, 'utf8') > maxObservationBytes) fail(stableError(message, code));
|
|
113
|
+
return value;
|
|
114
|
+
};
|
|
115
|
+
const observeModel = event => {
|
|
116
|
+
const observation = observedModelFor(event);
|
|
117
|
+
if (observation === null) return;
|
|
118
|
+
const value = observation.value.trim();
|
|
119
|
+
boundedObservation(value, 'upstream_unverified_event_shape', 'Postman web-session model observation has an invalid shape.');
|
|
120
|
+
if (observedUpstreamModel && observedUpstreamModel !== value) {
|
|
121
|
+
const err = stableError('Postman web-session model observation changed mid-stream.', 'upstream_model_mismatch');
|
|
122
|
+
err.observedModel = value; err.observedModelField = observation.fieldPath;
|
|
123
|
+
fail(err);
|
|
124
|
+
}
|
|
125
|
+
observedUpstreamModel = value;
|
|
126
|
+
if (!allowedObservedModels.length) {
|
|
127
|
+
modelVerification = 'unverified';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!allowedObservedModels.includes(value)) {
|
|
131
|
+
modelVerification = 'mismatched';
|
|
132
|
+
const err = stableError('Postman web-session observed model did not match the configured mapping.', 'upstream_model_mismatch');
|
|
133
|
+
err.observedModel = value; err.observedModelField = observation.fieldPath;
|
|
134
|
+
fail(err);
|
|
135
|
+
}
|
|
136
|
+
modelVerification = 'matched';
|
|
137
|
+
};
|
|
138
|
+
const recordEvent = name => {
|
|
139
|
+
eventCount++;
|
|
140
|
+
if (eventCount > maxEvents) fail(stableError('Postman web-session emitted too many events.', 'upstream_event_count_exceeded'));
|
|
141
|
+
eventTypes.add(name);
|
|
142
|
+
};
|
|
143
|
+
const handleUsage = data => {
|
|
144
|
+
const value = object(data);
|
|
145
|
+
if (!value) fail(stableError('Postman web-session usage event has an unverified shape.', 'upstream_unverified_event_shape'));
|
|
146
|
+
if (typeof value.usageState === 'string') {
|
|
147
|
+
const state = value.usageState.toUpperCase();
|
|
148
|
+
if (state === 'BLOCKED' || state === 'EXCEEDED' || state === 'UNAVAILABLE') fail(stableError('Postman AI credit access is unavailable.', 'upstream_credit_blocked', 400));
|
|
149
|
+
if (state !== 'AVAILABLE') fail(stableError('Postman gateway reported an unknown credit state.', 'upstream_unknown_credit_state'));
|
|
150
|
+
}
|
|
151
|
+
const input = value.inputTokens ?? value.input_tokens ?? value.promptTokens ?? value.prompt_tokens ?? 0;
|
|
152
|
+
const output = value.outputTokens ?? value.output_tokens ?? value.completionTokens ?? value.completion_tokens ?? 0;
|
|
153
|
+
const total = value.totalTokens ?? value.total_tokens ?? (input + output);
|
|
154
|
+
if (![input, output, total].every(nonNegativeSafe) || total < input || total < output) fail(stableError('Postman web-session usage event has invalid token counts.', 'upstream_unverified_event_shape'));
|
|
155
|
+
usage = Object.freeze({ prompt_tokens: input, completion_tokens: output, total_tokens: total, estimated: false });
|
|
156
|
+
};
|
|
157
|
+
const toolEntries = data => {
|
|
158
|
+
if (!object(data)) fail(stableError('Postman web-session toolCallChunk has an unverified shape.', 'upstream_unverified_event_shape'));
|
|
159
|
+
if (Array.isArray(data.toolCalls)) {
|
|
160
|
+
if (!data.toolCalls.length || data.toolCalls.some(value => !object(value))) fail(stableError('Postman web-session toolCallChunk has an invalid toolCalls array.', 'upstream_unverified_event_shape'));
|
|
161
|
+
return data.toolCalls.map((value, position) => ({ value, position, container: data }));
|
|
162
|
+
}
|
|
163
|
+
return [{ value: data, position: 0, container: data }];
|
|
164
|
+
};
|
|
165
|
+
const boundedToolIdentity = (value, field) => boundedObservation(value, 'upstream_tool_fragment_error', `Postman web-session tool call is missing a bounded ${field}.`);
|
|
166
|
+
const handleToolChunk = data => {
|
|
167
|
+
for (const { value, position, container } of toolEntries(data)) {
|
|
168
|
+
const explicitIndex = value.index ?? value.toolCallIndex ?? container.index ?? container.toolCallIndex;
|
|
169
|
+
if (explicitIndex !== undefined && (!Number.isSafeInteger(explicitIndex) || explicitIndex < 0)) fail(stableError('Postman web-session tool call index is invalid.', 'upstream_tool_fragment_error'));
|
|
170
|
+
const upstreamId = firstString([value.id, value.callId, value.toolCallId, value.call_id, value.tool_call_id, container.id, container.callId, container.toolCallId]);
|
|
171
|
+
const key = upstreamId ? `id:${upstreamId}` : explicitIndex !== undefined ? `index:${explicitIndex}` : null;
|
|
172
|
+
if (!key) fail(stableError('Postman web-session tool fragment is missing an ID and index.', 'upstream_tool_fragment_error'));
|
|
173
|
+
let call = toolCalls.get(key);
|
|
174
|
+
const sourceName = firstString([value.function?.name, value.name, value.tool?.name, container.function?.name, container.name]);
|
|
175
|
+
const groupId = firstString([value.toolCallGroupId, value.groupId, value.tool_call_group_id, value.group_id, container.toolCallGroupId, container.groupId, container.tool_call_group_id, container.group_id]);
|
|
176
|
+
if (!call) {
|
|
177
|
+
if (!upstreamId) fail(stableError('Postman web-session first tool fragment is missing a bounded ID.', 'upstream_tool_fragment_error'));
|
|
178
|
+
if (!sourceName) fail(stableError('Postman web-session first tool fragment is missing a bounded name.', 'upstream_tool_fragment_error'));
|
|
179
|
+
const originalName = toolNameMap.get(boundedToolIdentity(sourceName, 'name'));
|
|
180
|
+
if (!originalName) fail(stableError('Postman web-session returned an undeclared tool.', 'upstream_unregistered_tool'));
|
|
181
|
+
call = { index: explicitIndex ?? position, upstreamId: boundedToolIdentity(upstreamId, 'ID'), sourceName, originalName, groupId: groupId ? boundedToolIdentity(groupId, 'group ID') : null, arguments: '' };
|
|
182
|
+
if ([...toolCalls.values()].some(existing => existing.index === call.index || existing.upstreamId === call.upstreamId)) fail(stableError('Postman web-session tool fragments collide.', 'upstream_tool_fragment_error'));
|
|
183
|
+
toolCalls.set(key, call);
|
|
184
|
+
} else {
|
|
185
|
+
if (upstreamId && upstreamId !== call.upstreamId) fail(stableError('Postman web-session tool call ID changed mid-stream.', 'upstream_tool_fragment_error'));
|
|
186
|
+
if (sourceName && sourceName !== call.sourceName) fail(stableError('Postman web-session tool call name changed mid-stream.', 'upstream_tool_fragment_error'));
|
|
187
|
+
if (groupId && groupId !== call.groupId) fail(stableError('Postman web-session tool group changed mid-stream.', 'upstream_tool_fragment_error'));
|
|
188
|
+
}
|
|
189
|
+
const rawArguments = value.function?.arguments ?? value.arguments ?? value.input ?? value.input_json;
|
|
190
|
+
if (rawArguments !== undefined) {
|
|
191
|
+
const fragment = typeof rawArguments === 'string' ? rawArguments : JSON.stringify(rawArguments);
|
|
192
|
+
if (typeof fragment !== 'string') fail(stableError('Postman web-session tool arguments have an invalid shape.', 'upstream_tool_fragment_error'));
|
|
193
|
+
toolArgumentBytes += Buffer.byteLength(fragment, 'utf8');
|
|
194
|
+
if (toolArgumentBytes > maxToolArgumentBytes) fail(stableError('Postman web-session tool arguments exceed the configured limit.', 'upstream_tool_arguments_too_large'));
|
|
195
|
+
call.arguments += fragment;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
sawMeaningfulOutput = true;
|
|
199
|
+
};
|
|
200
|
+
const handleEvent = (event, name) => {
|
|
201
|
+
observeModel(event);
|
|
202
|
+
recordEvent(name);
|
|
203
|
+
const data = event.data;
|
|
204
|
+
const observedConversationId = firstString(name === 'conversation'
|
|
205
|
+
? [data?.id, data?.conversationId, data?.conversation_id, data?.conversation?.id, data?.conversation?.conversationId]
|
|
206
|
+
: [event?.conversationId, event?.conversation_id, event?.metadata?.conversationId, event?.metadata?.conversation_id, data?.conversationId, data?.conversation_id, data?.conversation?.id, data?.conversation?.conversationId, data?.metadata?.conversationId, data?.metadata?.conversation_id]);
|
|
207
|
+
if (observedConversationId) conversationId = boundedObservation(observedConversationId, 'upstream_invalid_conversation', 'Postman web-session conversation ID is invalid.');
|
|
208
|
+
if (name === 'conversation') {
|
|
209
|
+
if (!conversationId) fail(stableError('Postman web-session conversation event is missing a bounded ID.', 'upstream_invalid_conversation'));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (name === 'textChunk') {
|
|
213
|
+
if (typeof data?.textContent !== 'string') fail(stableError('Postman web-session textChunk has an unverified shape.', 'upstream_unverified_event_shape'));
|
|
214
|
+
outputBytes += Buffer.byteLength(data.textContent, 'utf8');
|
|
215
|
+
if (outputBytes > maxOutputBytes) fail(stableError('Upstream output exceeds the configured limit.', 'upstream_output_too_large'));
|
|
216
|
+
text += data.textContent;
|
|
217
|
+
if (data.textContent.length) sawMeaningfulOutput = true;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (name === 'usage') return handleUsage(data);
|
|
221
|
+
if (name === 'toolCallChunk') return handleToolChunk(data);
|
|
222
|
+
if (PROGRESS_EVENTS.has(name)) return;
|
|
223
|
+
const unsupported = UNSUPPORTED_EVENTS.get(name);
|
|
224
|
+
if (unsupported) fail(stableError(unsupported[0], unsupported[1], 400));
|
|
225
|
+
if (name === 'failure' || name === 'error' || /fail|error/i.test(name) || event.result === 'failure' || event.error || data?.error) {
|
|
226
|
+
const detail = upstreamFailureDetail(event);
|
|
227
|
+
const error = stableError('Postman web-session reported an upstream failure.', 'upstream_gateway_failure');
|
|
228
|
+
error.upstreamDetail = detail;
|
|
229
|
+
fail(error);
|
|
230
|
+
}
|
|
231
|
+
fail(stableError('Postman web-session emitted an unverified event type.', 'upstream_unverified_event_shape'));
|
|
232
|
+
};
|
|
233
|
+
const dispatch = () => {
|
|
234
|
+
if (!frameData.length) {
|
|
235
|
+
frameEvent = '';
|
|
236
|
+
frameBytes = 0;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const payload = frameData.join('\n');
|
|
240
|
+
const outerEvent = frameEvent || 'message';
|
|
241
|
+
frameEvent = '';
|
|
242
|
+
frameData = [];
|
|
243
|
+
frameBytes = 0;
|
|
244
|
+
if (explicitTerminal) fail(stableError('Postman web-session sent data after its terminal marker.', 'upstream_data_after_terminal'));
|
|
245
|
+
if (payload === '[DONE]') {
|
|
246
|
+
if (outerEvent !== 'message') fail(stableError('Postman web-session terminal marker used an invalid event type.', 'upstream_invalid_terminal'));
|
|
247
|
+
explicitTerminal = 'done';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
let event;
|
|
251
|
+
try { event = JSON.parse(payload); } catch { fail(stableError('Postman web-session sent malformed SSE JSON.', 'upstream_invalid_json')); }
|
|
252
|
+
if (!object(event)) fail(stableError('Postman web-session sent an invalid SSE event.', 'upstream_invalid_event'));
|
|
253
|
+
sawValidEvent = true;
|
|
254
|
+
handleEvent(event, eventName(event, frameEvent || (outerEvent === 'message' ? '' : outerEvent)));
|
|
255
|
+
};
|
|
256
|
+
const line = value => {
|
|
257
|
+
if (explicitTerminal && value !== '') fail(stableError('Postman web-session sent data after its terminal marker.', 'upstream_data_after_terminal'));
|
|
258
|
+
if (value === '') return dispatch();
|
|
259
|
+
addFrameBytes(value);
|
|
260
|
+
if (value.startsWith(':')) return;
|
|
261
|
+
const colon = value.indexOf(':');
|
|
262
|
+
const field = colon < 0 ? value : value.slice(0, colon);
|
|
263
|
+
const content = colon < 0 ? '' : value.slice(colon + 1).replace(/^ /, '');
|
|
264
|
+
if (field === 'data') frameData.push(content);
|
|
265
|
+
else if (field === 'event') frameEvent = content;
|
|
266
|
+
};
|
|
267
|
+
const processLines = final => {
|
|
268
|
+
while (buffer.length) {
|
|
269
|
+
let index = -1;
|
|
270
|
+
let width = 0;
|
|
271
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
272
|
+
if (buffer[i] === '\n') { index = i; width = 1; break; }
|
|
273
|
+
if (buffer[i] === '\r') {
|
|
274
|
+
if (i === buffer.length - 1 && !final) return;
|
|
275
|
+
index = i;
|
|
276
|
+
width = buffer[i + 1] === '\n' ? 2 : 1;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (index < 0) return;
|
|
281
|
+
const value = buffer.slice(0, index);
|
|
282
|
+
buffer = buffer.slice(index + width);
|
|
283
|
+
line(value);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const snapshot = () => Object.freeze({
|
|
287
|
+
outcome,
|
|
288
|
+
successful: outcome === 'complete_explicit',
|
|
289
|
+
text,
|
|
290
|
+
observations: Object.freeze({
|
|
291
|
+
requestedModel,
|
|
292
|
+
selectedUpstreamModel,
|
|
293
|
+
observedUpstreamModel,
|
|
294
|
+
modelVerification,
|
|
295
|
+
conversationId,
|
|
296
|
+
usage,
|
|
297
|
+
eventTypes: Object.freeze([...eventTypes]),
|
|
298
|
+
eventCount,
|
|
299
|
+
sawValidEvent,
|
|
300
|
+
sawMeaningfulOutput,
|
|
301
|
+
explicitTerminal,
|
|
302
|
+
transportEof,
|
|
303
|
+
parserBufferComplete,
|
|
304
|
+
}),
|
|
305
|
+
toolCalls: Object.freeze([...toolCalls.values()].sort((a,b)=>a.index-b.index).map(value => Object.freeze({ ...value }))),
|
|
306
|
+
error: errorView(terminalError),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
return Object.freeze({
|
|
310
|
+
feed(chunk) {
|
|
311
|
+
if (ended) throw new TypeError('Cannot feed a web-session parser after end().');
|
|
312
|
+
if (terminalError) throw terminalError;
|
|
313
|
+
if (!(chunk instanceof Uint8Array)) throw new TypeError('Web-session parser chunks must be Uint8Array values.');
|
|
314
|
+
try {
|
|
315
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
316
|
+
} catch {
|
|
317
|
+
fail(stableError('Postman web-session sent invalid UTF-8 SSE.', 'upstream_invalid_utf8'));
|
|
318
|
+
}
|
|
319
|
+
if (Buffer.byteLength(buffer, 'utf8') + frameBytes > maxEventBytes * 2) fail(stableError('Upstream SSE parser buffer exceeds the configured limit.', 'upstream_event_too_large'));
|
|
320
|
+
processLines(false);
|
|
321
|
+
return snapshot();
|
|
322
|
+
},
|
|
323
|
+
end() {
|
|
324
|
+
if (ended) return snapshot();
|
|
325
|
+
ended = true;
|
|
326
|
+
transportEof = true;
|
|
327
|
+
if (terminalError) return snapshot();
|
|
328
|
+
try {
|
|
329
|
+
buffer += decoder.decode();
|
|
330
|
+
} catch {
|
|
331
|
+
terminalError = stableError('Postman web-session ended with incomplete or invalid UTF-8.', 'upstream_invalid_utf8');
|
|
332
|
+
outcome = 'incomplete_truncated';
|
|
333
|
+
return snapshot();
|
|
334
|
+
}
|
|
335
|
+
try { processLines(true); } catch (error) { return snapshot(); }
|
|
336
|
+
const partialLine = buffer.length > 0;
|
|
337
|
+
const partialFrame = frameData.length > 0 || frameEvent.length > 0;
|
|
338
|
+
parserBufferComplete = !partialLine && !partialFrame;
|
|
339
|
+
if (terminalError) return snapshot();
|
|
340
|
+
if (explicitTerminal && !parserBufferComplete) {
|
|
341
|
+
outcome = 'failed';
|
|
342
|
+
terminalError = stableError('Postman web-session sent data after its terminal marker.', 'upstream_data_after_terminal');
|
|
343
|
+
} else if (explicitTerminal) outcome = 'complete_explicit';
|
|
344
|
+
else if (!parserBufferComplete) {
|
|
345
|
+
outcome = 'incomplete_truncated';
|
|
346
|
+
terminalError = stableError('Postman web-session ended with a partial SSE frame.', 'upstream_incomplete_stream');
|
|
347
|
+
} else if (sawMeaningfulOutput) {
|
|
348
|
+
outcome = 'ambiguous_eof';
|
|
349
|
+
terminalError = stableError('Postman web-session ended without proven terminal evidence.', 'upstream_completion_ambiguous');
|
|
350
|
+
} else {
|
|
351
|
+
outcome = 'incomplete_empty';
|
|
352
|
+
terminalError = stableError('Postman web-session ended without meaningful output or terminal evidence.', 'upstream_empty_stream');
|
|
353
|
+
}
|
|
354
|
+
return snapshot();
|
|
355
|
+
},
|
|
356
|
+
snapshot,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
|
|
2
|
+
export const POSTMAN_MODEL_MAP = Object.freeze({
|
|
3
|
+
'gpt-5.6-sol': 'GPT_56_SOL',
|
|
4
|
+
'gpt-5.6-sol-thinking': 'GPT_56_SOL',
|
|
5
|
+
'gpt-5.6-terra': 'GPT_56_TERRA',
|
|
6
|
+
'gpt-5.6-terra-thinking': 'GPT_56_TERRA',
|
|
7
|
+
'claude-opus-4-8': 'CLAUDE_OPUS_48_BEDROCK',
|
|
8
|
+
'claude-opus-4-8-thinking': 'CLAUDE_OPUS_48_BEDROCK',
|
|
9
|
+
'claude-opus-4-7': 'CLAUDE_OPUS_47_BEDROCK',
|
|
10
|
+
'claude-opus-4-7-thinking': 'CLAUDE_OPUS_47_BEDROCK',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function resolveUpstreamModel(model, fallback) {
|
|
14
|
+
if (typeof model !== 'string') return fallback;
|
|
15
|
+
const key = model.trim().toLowerCase();
|
|
16
|
+
return POSTMAN_MODEL_MAP[key] || fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isThinkingModel(model) {
|
|
20
|
+
return typeof model === 'string' && model.trim().toLowerCase().endsWith('-thinking');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { webSessionUrl } from './config.mjs';
|
|
24
|
+
import { ProviderError, invalid } from './errors.mjs';
|
|
25
|
+
import { buildTranscript } from './openai.mjs';
|
|
26
|
+
|
|
27
|
+
const USER_AGENT = 'postman-openai-provider/0.1.0 web-session-offline-builder';
|
|
28
|
+
|
|
29
|
+
function assertRequest(request) {
|
|
30
|
+
if (!request || typeof request !== 'object') throw invalid('Normalized request is required.', 'invalid_web_session_request');
|
|
31
|
+
if (!Array.isArray(request.messages) || !request.messages.length) throw invalid('Web-session profile requires message history.', 'invalid_messages');
|
|
32
|
+
if (request.toolChoice && typeof request.toolChoice === 'object') throw invalid('Web-session profile does not support a named tool_choice.', 'unsupported_web_session_tool_choice');
|
|
33
|
+
if (!['none', 'auto', 'required'].includes(request.toolChoice)) throw invalid('Web-session profile received an unsupported tool_choice.', 'unsupported_web_session_tool_choice');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function thirdPartyTools(request) {
|
|
37
|
+
if (!request.tools?.length) return undefined;
|
|
38
|
+
return {
|
|
39
|
+
'proxy-tools': {
|
|
40
|
+
tools: request.tools.map(tool => ({
|
|
41
|
+
name: tool.name,
|
|
42
|
+
description: tool.description,
|
|
43
|
+
parameters: tool.parameters,
|
|
44
|
+
})),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function clientTools(request, config) {
|
|
50
|
+
const thirdParty = thirdPartyTools(request);
|
|
51
|
+
if (!thirdParty) return { native: [] };
|
|
52
|
+
// Keep the legacy direct-tool envelope until current Postman metadata is
|
|
53
|
+
// discovered from the authenticated client. A bundled/stale native hash can
|
|
54
|
+
// make upstream store the call under a contract the continuation cannot use.
|
|
55
|
+
if (config.nativeToolsHash && config.webSessionMetadataSource === 'discovered') {
|
|
56
|
+
return { nativeToolsHash: config.nativeToolsHash, excludedTools: config.excludedTools || [], thirdParty };
|
|
57
|
+
}
|
|
58
|
+
return { native: [], thirdParty };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function clientKBTerms(config) {
|
|
62
|
+
if (config.nativeTermsHash && config.webSessionMetadataSource === 'discovered') return { nativeTermsHash: config.nativeTermsHash, excludedKBTerms: config.excludedKBTerms || [] };
|
|
63
|
+
return { native: [] };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function devModeOptions(request, config) {
|
|
67
|
+
return {
|
|
68
|
+
selectedModel: resolveUpstreamModel(request.model, config.selectedUpstreamModel),
|
|
69
|
+
// /_gw/chat requires a toolCallGroupId for parallel continuation, but current
|
|
70
|
+
// live single-call events do not emit one. Advertise sequential execution so
|
|
71
|
+
// Postman stores the call under the flat toolCallId/toolResponse contract.
|
|
72
|
+
isParallelToolCallingSupported: false,
|
|
73
|
+
autoRun: request.tools.length > 0 && request.toolChoice !== 'none',
|
|
74
|
+
supportsAskUser: false,
|
|
75
|
+
supportsActionRecommendations: request.tools.length > 0,
|
|
76
|
+
useThinkingModeIfAvailable: isThinkingModel(request.model),
|
|
77
|
+
isLoopApprovalEnabled: true,
|
|
78
|
+
enableWebAccess: true,
|
|
79
|
+
agentMode: true,
|
|
80
|
+
agentModeEnabled: true,
|
|
81
|
+
ai_user_agent_mode: true,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function prepareMessagesAndContext(request) {
|
|
86
|
+
const transcript = buildTranscript(request.messages);
|
|
87
|
+
const fullJson = JSON.stringify({
|
|
88
|
+
format: 'openai-full-history-v1',
|
|
89
|
+
messages: transcript,
|
|
90
|
+
tool_choice: request.toolChoice,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Strict byte measurement: keep query string byte length <= 6.000 bytes (well under the ~9.500 byte limit)
|
|
94
|
+
if (Buffer.byteLength(fullJson, 'utf8') <= 6000) {
|
|
95
|
+
return {
|
|
96
|
+
query: fullJson,
|
|
97
|
+
selectedContext: [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Large context optimization:
|
|
102
|
+
// Postman input.query has a hard limit of ~9.500 bytes, but selectedContext files support >400.000 bytes!
|
|
103
|
+
// Separate system prompt and older history into virtual file, keeping the active turn in query.
|
|
104
|
+
const systemMessages = [];
|
|
105
|
+
const otherMessages = [];
|
|
106
|
+
|
|
107
|
+
for (const m of transcript) {
|
|
108
|
+
if (m.role === 'system' || m.role === 'developer') {
|
|
109
|
+
systemMessages.push(m);
|
|
110
|
+
} else {
|
|
111
|
+
otherMessages.push(m);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Recent messages for query: keep tail <= 4.000 bytes to guarantee room for metadata
|
|
116
|
+
const recentMessages = [];
|
|
117
|
+
let currentBytes = 0;
|
|
118
|
+
for (let i = otherMessages.length - 1; i >= 0; i--) {
|
|
119
|
+
const msg = otherMessages[i];
|
|
120
|
+
const msgBytes = Buffer.byteLength(JSON.stringify(msg), 'utf8');
|
|
121
|
+
if (recentMessages.length === 0 || (currentBytes + msgBytes <= 4000)) {
|
|
122
|
+
recentMessages.unshift(msg);
|
|
123
|
+
currentBytes += msgBytes;
|
|
124
|
+
} else {
|
|
125
|
+
systemMessages.unshift(msg); // move older turns cleanly into context file
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const selectedContext = [];
|
|
130
|
+
if (systemMessages.length > 0) {
|
|
131
|
+
let systemContent = systemMessages
|
|
132
|
+
.map(m => `### ${m.role.toUpperCase()}\n${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`)
|
|
133
|
+
.join('\n\n');
|
|
134
|
+
|
|
135
|
+
// Cap selectedContext content to 350.000 bytes (Postman virtual file safe ceiling ~400KB)
|
|
136
|
+
// If oversized due to massive room history (e.g. 100MB session), keep head (system prompt) + tail (recent context)
|
|
137
|
+
const MAX_CONTEXT_FILE_BYTES = 350000;
|
|
138
|
+
if (Buffer.byteLength(systemContent, 'utf8') > MAX_CONTEXT_FILE_BYTES) {
|
|
139
|
+
const head = systemContent.slice(0, 100000); // 100KB head (system directives)
|
|
140
|
+
const tail = systemContent.slice(-200000); // 200KB tail (recent history)
|
|
141
|
+
systemContent = `${head}\n\n... [TRUNCATED OVERSIZED ROOM HISTORY TO FIT 350KB SAFE LIMIT] ...\n\n${tail}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
selectedContext.push({
|
|
145
|
+
type: 'file',
|
|
146
|
+
id: 'system_and_history_context.md',
|
|
147
|
+
value: {
|
|
148
|
+
content: systemContent,
|
|
149
|
+
name: 'system_and_history_context.md',
|
|
150
|
+
accessible: true,
|
|
151
|
+
platform: 'web',
|
|
152
|
+
},
|
|
153
|
+
dataGuide: 'System prompt instructions, agent identity, tools and conversation context. Follow these directives strictly.',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Inject High-Priority Tool Calling Directive for AICoworker Native Tools
|
|
158
|
+
const toolDirective = request.tools?.length ? [
|
|
159
|
+
'GLOBAL HIGH-PRIORITY DIRECTIVE FOR AICOWORKER NATIVE TOOLS:',
|
|
160
|
+
'- Interactive Input: When asking the user questions, gathering requirements, or offering choices, you MUST call "ask_user_question" immediately (never output numbered questions in text).',
|
|
161
|
+
'- Adaptive Visuals: When presenting structured data, diagrams, comparison matrices, KPIs, or visual content, you MUST call "show_adaptive_ui".',
|
|
162
|
+
'- Web & Research: For real-time information, search queries, or web content, call "web_search", "searxng_search", or "web_fetch". For rich browser actions, call "browser".',
|
|
163
|
+
'- File & Code Operations: For file reads/edits/writes, call "read", "write", "edit", or "ripgrep". For command line execution, call "exec" or "process".',
|
|
164
|
+
'- Planning & Collaboration: Use "plan_task" / "write_todos" for structured execution, and "room_delegate" / "sessions_spawn" for agent delegation.',
|
|
165
|
+
'- Memory & Context: Recall past facts via "memory_search" / "memory_get", and optimize tokens via "mark_context_wasted".',
|
|
166
|
+
'- Always prefer invoking appropriate native function tools whenever user intent matches their capabilities.',
|
|
167
|
+
].join('\n') : '';
|
|
168
|
+
|
|
169
|
+
const queryMessages = [...recentMessages];
|
|
170
|
+
if (toolDirective) {
|
|
171
|
+
queryMessages.unshift({
|
|
172
|
+
role: 'system',
|
|
173
|
+
content: toolDirective,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const query = JSON.stringify({
|
|
178
|
+
format: 'openai-full-history-v1',
|
|
179
|
+
messages: queryMessages,
|
|
180
|
+
tool_choice: request.toolChoice,
|
|
181
|
+
context_reference: 'System instructions and prior context are provided in the attached system_and_history_context.md file. Adhere to all rules and roles defined therein.',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return { query, selectedContext };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function initialInput(request, contextHolder) {
|
|
188
|
+
const { query, selectedContext } = prepareMessagesAndContext(request);
|
|
189
|
+
if (contextHolder) {
|
|
190
|
+
contextHolder.selectedContext = selectedContext;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
chatType: 'USER_QUERY',
|
|
194
|
+
query,
|
|
195
|
+
conversationId: request.establishedConversationId || null,
|
|
196
|
+
product: 'workspace_v12',
|
|
197
|
+
useCase: null,
|
|
198
|
+
agent: null,
|
|
199
|
+
startedFrom: 'CHAT_INPUT',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function continuationInput(continuation) {
|
|
204
|
+
if (!continuation) throw invalid('Tool-result continuation is missing.', 'unknown_tool_call');
|
|
205
|
+
const group = continuation.group || continuation;
|
|
206
|
+
const orderedResults = continuation.orderedResults || [];
|
|
207
|
+
const convId = group.conversationId || continuation.conversationId;
|
|
208
|
+
if (!convId) throw invalid('Correlated web-session continuation is missing a conversation ID.', 'upstream_invalid_conversation');
|
|
209
|
+
const input = {
|
|
210
|
+
chatType: 'TOOL_RESPONSE', query: '', conversationId: convId,
|
|
211
|
+
product: 'workspace_v12', useCase: null, agent: null, startedFrom: 'CHAT_INPUT',
|
|
212
|
+
};
|
|
213
|
+
if (group.calls && group.calls.length > 0) {
|
|
214
|
+
const primary = group.calls[0];
|
|
215
|
+
// Current /_gw/chat accepts the flat response fields even when the tool
|
|
216
|
+
// call belongs to a group. The group ID is additional correlation data;
|
|
217
|
+
// input.toolResponses is rejected by this route for live single-call turns.
|
|
218
|
+
input.toolCallId = primary.upstreamToolCallId;
|
|
219
|
+
input.toolResponse = group.calls.length === 1
|
|
220
|
+
? orderedResults[0]?.content || ''
|
|
221
|
+
: JSON.stringify(group.calls.map((call, index) => ({
|
|
222
|
+
toolCallId: call.upstreamToolCallId,
|
|
223
|
+
status: 'SUCCESS',
|
|
224
|
+
content: orderedResults[index]?.content || '',
|
|
225
|
+
})));
|
|
226
|
+
input.toolResponseSummary = group.calls.length === 1 ? `Result for ${primary.originalName}` : `${group.calls.length} tool calls completed`;
|
|
227
|
+
input.toolResponseStatus = 'SUCCESS';
|
|
228
|
+
if (group.upstreamGroupId) input.toolCallGroupId = group.upstreamGroupId;
|
|
229
|
+
} else {
|
|
230
|
+
// Synthetic continuation fallback
|
|
231
|
+
input.toolCallId = continuation.syntheticToolCallId || 'call_01';
|
|
232
|
+
input.toolResponse = orderedResults[0]?.content || '';
|
|
233
|
+
input.toolResponseSummary = 'Result for tool';
|
|
234
|
+
input.toolResponseStatus = 'SUCCESS';
|
|
235
|
+
}
|
|
236
|
+
return input;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildBody(request, config, continuation) {
|
|
240
|
+
const contextHolder = { selectedContext: [] };
|
|
241
|
+
const input = continuation ? continuationInput(continuation) : initialInput(request, contextHolder);
|
|
242
|
+
return {
|
|
243
|
+
input,
|
|
244
|
+
platform: 'WEB',
|
|
245
|
+
clientTools: clientTools(request, config),
|
|
246
|
+
clientKBTerms: clientKBTerms(config),
|
|
247
|
+
mandatoryContext: { workspaceId: config.workspaceId },
|
|
248
|
+
selectedContext: contextHolder.selectedContext,
|
|
249
|
+
backgroundContext: [],
|
|
250
|
+
devModeOptions: devModeOptions(request, config),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function webSessionCatalogMetadata(request, config) {
|
|
255
|
+
return {
|
|
256
|
+
strategy: 'web_session',
|
|
257
|
+
origin: webSessionUrl(config).origin,
|
|
258
|
+
platform: 'WEB',
|
|
259
|
+
toolNamespace: request.tools.length ? 'proxy-tools' : null,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function buildWebSessionRequest(request, config, continuation = null) {
|
|
264
|
+
if (config?.transportStrategy !== 'web_session') throw new Error('buildWebSessionRequest requires web_session configuration.');
|
|
265
|
+
assertRequest(request);
|
|
266
|
+
if (request.toolResults?.length && !continuation) throw invalid('Tool-result continuation is not correlated.', config.correlationEnabled === false ? 'unsupported_tool_result_continuation' : 'unknown_tool_call');
|
|
267
|
+
const url = webSessionUrl(config);
|
|
268
|
+
const origin = url.origin;
|
|
269
|
+
const headers = {
|
|
270
|
+
'content-type': 'application/json', accept: 'text/event-stream',
|
|
271
|
+
cookie: `postman.sid=${config.webSessionCookie}`,
|
|
272
|
+
'x-pstmn-req-service': 'agent-mode-service', origin, referer: `${origin}/`, 'user-agent': USER_AGENT,
|
|
273
|
+
};
|
|
274
|
+
if (config.webSessionAppVersion !== undefined) headers['x-app-version'] = config.webSessionAppVersion;
|
|
275
|
+
const body = JSON.stringify(buildBody(request, config, continuation));
|
|
276
|
+
if (Buffer.byteLength(body, 'utf8') > config.maxBodyBytes) throw new ProviderError(413, 'Web-session request body exceeds the configured limit.', 'invalid_request_error', 'request_too_large');
|
|
277
|
+
return { url, init: { method: 'POST', redirect: 'error', headers, body } };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const buildWebSessionTextRequest = buildWebSessionRequest;
|