@juspay/neurolink 13.8.13 → 13.8.15
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/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +422 -422
- package/dist/cli/utils/serverUtils.d.ts +4 -0
- package/dist/cli/utils/serverUtils.js +5 -1
- package/dist/server/abstract/baseServerAdapter.d.ts +30 -1
- package/dist/server/abstract/baseServerAdapter.js +71 -0
- package/dist/server/adapters/expressAdapter.js +5 -0
- package/dist/server/adapters/fastifyAdapter.d.ts +1 -0
- package/dist/server/adapters/fastifyAdapter.js +16 -3
- package/dist/server/adapters/honoAdapter.d.ts +6 -0
- package/dist/server/adapters/honoAdapter.js +67 -16
- package/dist/server/adapters/koaAdapter.js +5 -0
- package/dist/server/middleware/auth.js +3 -0
- package/dist/server/routes/agentRoutes.d.ts +42 -1
- package/dist/server/routes/agentRoutes.js +140 -31
- package/dist/server/routes/codexProxyRoutes.js +65 -6
- package/dist/server/routes/index.d.ts +9 -0
- package/dist/server/routes/index.js +15 -1
- package/dist/server/utils/carriedContextMark.d.ts +28 -0
- package/dist/server/utils/carriedContextMark.js +32 -0
- package/dist/types/proxy.d.ts +4 -0
- package/dist/types/server.d.ts +140 -2
- package/docs-site/static/search-index.json +8 -6
- package/package.json +4 -2
|
@@ -7,6 +7,7 @@ import { ProviderFactory } from "../../factories/providerFactory.js";
|
|
|
7
7
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
8
8
|
import { tracers } from "../../telemetry/tracers.js";
|
|
9
9
|
import { logger } from "../../utils/logger.js";
|
|
10
|
+
import { isContextCarried } from "../utils/carriedContextMark.js";
|
|
10
11
|
/**
|
|
11
12
|
* Resolve `request.tools` (an array of tool NAMES shared by the execute and
|
|
12
13
|
* stream endpoints) into a `toolFilter` whitelist. The field was previously
|
|
@@ -78,14 +79,144 @@ async function resolveRequestInput(ctx, requestInput, options) {
|
|
|
78
79
|
/**
|
|
79
80
|
* Create agent routes
|
|
80
81
|
*
|
|
82
|
+
* `options.perUserAuth` decides where a request's identity comes from; see
|
|
83
|
+
* `ServerAgentRouteOptions`. Authentication middleware that sets `ctx.user`
|
|
84
|
+
* does not change that on its own: one shared API key sets the same user for
|
|
85
|
+
* every caller, so trusting it as a person would give every client the same
|
|
86
|
+
* userId and no conversation memory. The converse is the operator's to act on:
|
|
87
|
+
* a server whose authentication identifies individual people must set
|
|
88
|
+
* `perUserAuth`, or any authenticated caller can name another userId and
|
|
89
|
+
* sessionId in the body.
|
|
90
|
+
*
|
|
91
|
+
* Unset, identity resolves as it did before the adapters carried `ctx.user` to
|
|
92
|
+
* the handler: the body's `userId` and `sessionId`, with one exception. On
|
|
93
|
+
* Hono, a request whose client sent its own `X-Request-ID` header reached the
|
|
94
|
+
* handler with the middleware's context, so an authenticated `ctx.user` (and
|
|
95
|
+
* `ctx.session`) won over the body there, and still does. A context the
|
|
96
|
+
* built-in adapters did not hand the carry to also keeps that rule: a custom
|
|
97
|
+
* adapter that sets `ctx.user` itself, or a handler called directly with a
|
|
98
|
+
* context the caller built. Only `perUserAuth` (with the server carrying
|
|
99
|
+
* middleware state) makes these routes take the user from `ctx.user`, and
|
|
100
|
+
* ignore the body's `userId`, on every built-in adapter and for every request.
|
|
101
|
+
*
|
|
102
|
+
* `perUserAuth` needs the user in the handler, which an adapter hands over on
|
|
103
|
+
* every request only when the server carries middleware state.
|
|
104
|
+
* `registerAllRoutes()` turns that on for the server when it is given
|
|
105
|
+
* `perUserAuth`. A caller that registers these routes itself with `perUserAuth`
|
|
106
|
+
* must create the server with `carryMiddlewareState: true`. Without it, a
|
|
107
|
+
* request has a user only on Hono when the client sent its own `X-Request-ID`
|
|
108
|
+
* (Hono then hands the handler the middleware's context regardless); every
|
|
109
|
+
* other request (Hono without that header, Express, Fastify, Koa) runs as
|
|
110
|
+
* anonymous, with no `userId` and no session.
|
|
111
|
+
*
|
|
112
|
+
* Without `perUserAuth`, and when the server carries middleware state, the
|
|
113
|
+
* routes log one warning per instance the first time a request whose body names
|
|
114
|
+
* a different `userId` than its `ctx.user` is served from the body, unless that
|
|
115
|
+
* user is marked `shared: true` (an identity for every holder of one
|
|
116
|
+
* credential, which the CLI's API-key middleware sets). Such a request is
|
|
117
|
+
* served from the body; the warning only tells the operator that per-person
|
|
118
|
+
* authentication is running without the flag that makes it hold. With the
|
|
119
|
+
* carry off the handler has no `ctx.user` on those requests, so there is
|
|
120
|
+
* nothing to compare and no warning. A request where `ctx.user` already won has
|
|
121
|
+
* nothing to warn about.
|
|
122
|
+
*
|
|
81
123
|
* @param options - Limits for `input.images` / `input.files`:
|
|
82
124
|
* `allowedInputRoots` (default: none, every local-path attachment is
|
|
83
125
|
* refused), `maxInputBytes` per attachment (default 50 MiB),
|
|
84
126
|
* `maxInputAttachments` per request (default 20, images and files together)
|
|
85
127
|
* and `maxInputTotalBytes` per request (default 100 MiB, one budget shared by
|
|
86
|
-
* images and files).
|
|
128
|
+
* images and files); and `perUserAuth` (described above).
|
|
87
129
|
*/
|
|
88
130
|
export function createAgentRoutes(basePath = "/api", options) {
|
|
131
|
+
// Truthiness, not `=== true`: a JavaScript caller passing "true" or 1 must get
|
|
132
|
+
// the strict mode, not silently the permissive one.
|
|
133
|
+
const perUser = Boolean(options?.perUserAuth);
|
|
134
|
+
// Without perUserAuth the body's userId wins (except where ctx.user already
|
|
135
|
+
// did), so an authenticated person who names someone else is acting as them.
|
|
136
|
+
// That is expected under one shared key (marked `shared`), where the body is
|
|
137
|
+
// how callers keep separate memory, and a misconfiguration under per-person
|
|
138
|
+
// authentication: say so, once, with no request values in the message.
|
|
139
|
+
// alwaysStderr, not warn: logger.warn is dropped unless debug logging is on,
|
|
140
|
+
// which would hide a security notice from the operator it is for.
|
|
141
|
+
let warnedNamingAnotherUser = false;
|
|
142
|
+
const warnIfNamingAnotherUser = (ctx, request) => {
|
|
143
|
+
if (warnedNamingAnotherUser ||
|
|
144
|
+
!ctx.user ||
|
|
145
|
+
ctx.user.shared === true ||
|
|
146
|
+
!isContextCarried(ctx) ||
|
|
147
|
+
typeof request.userId !== "string" ||
|
|
148
|
+
request.userId === ctx.user.id) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
warnedNamingAnotherUser = true;
|
|
152
|
+
logger.alwaysStderr("[agentRoutes] An authenticated user named a different userId in the request body, and the body's userId was used. " +
|
|
153
|
+
"Per-person authentication without perUserAuth lets a caller name another user. " +
|
|
154
|
+
"Set perUserAuth: true, or mark an identity that stands for a shared credential with shared: true. " +
|
|
155
|
+
"This warning is logged once per server.");
|
|
156
|
+
};
|
|
157
|
+
// Who a skill mutation is attributed to. With perUserAuth the authenticated
|
|
158
|
+
// user wins and a caller-supplied name is never trusted. Otherwise the body's
|
|
159
|
+
// `requestedBy` is used, since a shared key's `ctx.user` names no person, for
|
|
160
|
+
// a context that received the adapters' carry; a context that did not (Hono
|
|
161
|
+
// with the caller's X-Request-ID, a custom adapter, a direct call) lets the
|
|
162
|
+
// authenticated user win, as it did before.
|
|
163
|
+
const skillRequester = (ctx, bodyRequestedBy) => {
|
|
164
|
+
if (perUser) {
|
|
165
|
+
return ctx.user?.id;
|
|
166
|
+
}
|
|
167
|
+
return isContextCarried(ctx)
|
|
168
|
+
? bodyRequestedBy
|
|
169
|
+
: (ctx.user?.id ?? bodyRequestedBy);
|
|
170
|
+
};
|
|
171
|
+
// The identity generate() and stream() run under.
|
|
172
|
+
//
|
|
173
|
+
// perUserAuth: the middleware identifies individual people, so userId, email,
|
|
174
|
+
// roles and the session come only from ctx.user / ctx.session and the body's
|
|
175
|
+
// are ignored (no caller-supplied impersonation). The session is the
|
|
176
|
+
// authenticated user's: a request with no ctx.user gets none, even if a
|
|
177
|
+
// session middleware named one, because with optional authentication every
|
|
178
|
+
// anonymous caller that sends the same session id would otherwise share one
|
|
179
|
+
// conversation.
|
|
180
|
+
//
|
|
181
|
+
// Otherwise the body keeps choosing userId and sessionId, which is what every
|
|
182
|
+
// adapter did before it carried ctx.user to the handler: a context that
|
|
183
|
+
// received the adapters' carry is treated as the fresh one it used to be. The
|
|
184
|
+
// exceptions are the contexts that did not get the carry, where an
|
|
185
|
+
// authenticated ctx.user wins over the body, as it did, and so does the
|
|
186
|
+
// session: Hono with the caller's own X-Request-ID, a custom adapter that sets
|
|
187
|
+
// ctx.user itself, a handler called directly. Per-person authentication
|
|
188
|
+
// without perUserAuth therefore does not stop a caller naming someone else on
|
|
189
|
+
// the other requests.
|
|
190
|
+
const callerContext = (ctx, request) => {
|
|
191
|
+
if (perUser) {
|
|
192
|
+
return {
|
|
193
|
+
sessionId: ctx.user ? ctx.session?.id : undefined,
|
|
194
|
+
userId: ctx.user?.id,
|
|
195
|
+
userEmail: ctx.user?.email,
|
|
196
|
+
userRoles: ctx.user?.roles,
|
|
197
|
+
requestId: ctx.requestId,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (!isContextCarried(ctx)) {
|
|
201
|
+
return {
|
|
202
|
+
sessionId: ctx.user
|
|
203
|
+
? ctx.session?.id
|
|
204
|
+
: (ctx.session?.id ?? request.sessionId),
|
|
205
|
+
userId: ctx.user ? ctx.user.id : request.userId,
|
|
206
|
+
userEmail: ctx.user?.email,
|
|
207
|
+
userRoles: ctx.user?.roles,
|
|
208
|
+
requestId: ctx.requestId,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
warnIfNamingAnotherUser(ctx, request);
|
|
212
|
+
return {
|
|
213
|
+
sessionId: request.sessionId,
|
|
214
|
+
userId: request.userId,
|
|
215
|
+
userEmail: undefined,
|
|
216
|
+
userRoles: undefined,
|
|
217
|
+
requestId: ctx.requestId,
|
|
218
|
+
};
|
|
219
|
+
};
|
|
89
220
|
return {
|
|
90
221
|
prefix: `${basePath}/agent`,
|
|
91
222
|
routes: [
|
|
@@ -121,17 +252,7 @@ export function createAgentRoutes(basePath = "/api", options) {
|
|
|
121
252
|
temperature: request.temperature,
|
|
122
253
|
maxTokens: request.maxTokens,
|
|
123
254
|
...(requestToolFilter ? { toolFilter: requestToolFilter } : {}),
|
|
124
|
-
context:
|
|
125
|
-
// When an authenticated user context exists (set by auth middleware),
|
|
126
|
-
// always use its IDs to prevent caller-supplied impersonation.
|
|
127
|
-
sessionId: ctx.user
|
|
128
|
-
? ctx.session?.id
|
|
129
|
-
: (ctx.session?.id ?? request.sessionId),
|
|
130
|
-
userId: ctx.user ? ctx.user.id : request.userId,
|
|
131
|
-
userEmail: ctx.user?.email,
|
|
132
|
-
userRoles: ctx.user?.roles,
|
|
133
|
-
requestId: ctx.requestId,
|
|
134
|
-
},
|
|
255
|
+
context: callerContext(ctx, request),
|
|
135
256
|
});
|
|
136
257
|
// Map tool calls from SDK format to API format
|
|
137
258
|
const toolCalls = result.toolCalls?.map((tc) => ({
|
|
@@ -174,17 +295,7 @@ export function createAgentRoutes(basePath = "/api", options) {
|
|
|
174
295
|
temperature: request.temperature,
|
|
175
296
|
maxTokens: request.maxTokens,
|
|
176
297
|
...(streamToolFilter ? { toolFilter: streamToolFilter } : {}),
|
|
177
|
-
context:
|
|
178
|
-
// When an authenticated user context exists (set by auth middleware),
|
|
179
|
-
// always use its IDs to prevent caller-supplied impersonation.
|
|
180
|
-
sessionId: ctx.user
|
|
181
|
-
? ctx.session?.id
|
|
182
|
-
: (ctx.session?.id ?? request.sessionId),
|
|
183
|
-
userId: ctx.user ? ctx.user.id : request.userId,
|
|
184
|
-
userEmail: ctx.user?.email,
|
|
185
|
-
userRoles: ctx.user?.roles,
|
|
186
|
-
requestId: ctx.requestId,
|
|
187
|
-
},
|
|
298
|
+
context: callerContext(ctx, request),
|
|
188
299
|
});
|
|
189
300
|
// Create redactor (no-op if redaction is not enabled)
|
|
190
301
|
const redactor = createStreamRedactor(ctx.redaction);
|
|
@@ -369,14 +480,12 @@ export function createAgentRoutes(basePath = "/api", options) {
|
|
|
369
480
|
return validation.error;
|
|
370
481
|
}
|
|
371
482
|
const { requestedBy, ...skill } = validation.data;
|
|
483
|
+
const requester = skillRequester(ctx, requestedBy);
|
|
372
484
|
try {
|
|
373
485
|
const result = await manager.requestMutation({
|
|
374
486
|
type: "create",
|
|
375
487
|
skill,
|
|
376
|
-
|
|
377
|
-
...(ctx.user?.id || requestedBy
|
|
378
|
-
? { requestedBy: ctx.user?.id ?? requestedBy }
|
|
379
|
-
: {}),
|
|
488
|
+
...(requester ? { requestedBy: requester } : {}),
|
|
380
489
|
});
|
|
381
490
|
return result;
|
|
382
491
|
}
|
|
@@ -400,14 +509,13 @@ export function createAgentRoutes(basePath = "/api", options) {
|
|
|
400
509
|
return validation.error;
|
|
401
510
|
}
|
|
402
511
|
const { requestedBy, ...patch } = validation.data;
|
|
512
|
+
const requester = skillRequester(ctx, requestedBy);
|
|
403
513
|
try {
|
|
404
514
|
const result = await manager.requestMutation({
|
|
405
515
|
type: "update",
|
|
406
516
|
skillId: ctx.params.id,
|
|
407
517
|
patch,
|
|
408
|
-
...(
|
|
409
|
-
? { requestedBy: ctx.user?.id ?? requestedBy }
|
|
410
|
-
: {}),
|
|
518
|
+
...(requester ? { requestedBy: requester } : {}),
|
|
411
519
|
});
|
|
412
520
|
return result;
|
|
413
521
|
}
|
|
@@ -427,11 +535,12 @@ export function createAgentRoutes(basePath = "/api", options) {
|
|
|
427
535
|
if ("error" in manager) {
|
|
428
536
|
return manager;
|
|
429
537
|
}
|
|
538
|
+
const requester = skillRequester(ctx, undefined);
|
|
430
539
|
try {
|
|
431
540
|
const result = await manager.requestMutation({
|
|
432
541
|
type: "delete",
|
|
433
542
|
skillId: ctx.params.id,
|
|
434
|
-
...(
|
|
543
|
+
...(requester ? { requestedBy: requester } : {}),
|
|
435
544
|
});
|
|
436
545
|
return result;
|
|
437
546
|
}
|
|
@@ -24,7 +24,7 @@ import { loadAccountQuotas, saveAccountQuota, } from "../../proxy/accountQuota.j
|
|
|
24
24
|
import { createCodexUsageTap } from "../../proxy/codexUsage.js";
|
|
25
25
|
import { CODEX_ACCOUNT_PREFIX, parseCodexRateLimitHeaders, } from "../../proxy/codexAccountUsage.js";
|
|
26
26
|
import { buildClientAttribution } from "../../proxy/clientAttribution.js";
|
|
27
|
-
import { getProxyUpstreamFailure } from "../../proxy/proxyFailureDetails.js";
|
|
27
|
+
import { classifyProxyFailureCode, getProxyUpstreamFailure, } from "../../proxy/proxyFailureDetails.js";
|
|
28
28
|
import { registerProxyResponseObserver, isProxyRequestFinalized, registerInternalProxyRequest, getProxyBridgeResult, releaseProxyRequestAccounting, } from "../../proxy/proxyActivity.js";
|
|
29
29
|
import { parseCodexNativeRequest, translateCodexRequestToClaude, classifyCodexOutboundFailure, buildSystemBlocksFromDeveloperMessages, } from "../../proxy/codexOutboundFallback.js";
|
|
30
30
|
import { createAnthropicFallbackStream } from "../../proxy/codexToAnthropicFallback.js";
|
|
@@ -77,6 +77,32 @@ function codexTransportScope(error) {
|
|
|
77
77
|
function summarizeCodexUpstreamError(errorText, fallback) {
|
|
78
78
|
return sanitizeForLog(errorText).slice(0, 200) || fallback;
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether a pre-stream 401/403 refuses the REQUEST rather than the credential.
|
|
82
|
+
*
|
|
83
|
+
* Only the body can tell: a content-policy 403 and an expired-token 403 share a
|
|
84
|
+
* status. `classifyProxyFailureCode` is the verdict the outbound fallback stops
|
|
85
|
+
* on, so the pool and the fallback read one code the same way. Anything it does
|
|
86
|
+
* not call non-retryable, including a body with no code at all, stays an
|
|
87
|
+
* authentication failure.
|
|
88
|
+
*/
|
|
89
|
+
function readCodexRequestRefusal(status, errorText) {
|
|
90
|
+
if (status !== 401 && status !== 403) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const failure = getProxyUpstreamFailure({ responseBody: errorText });
|
|
94
|
+
if (failure?.code === undefined) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const known = classifyProxyFailureCode(failure.code);
|
|
98
|
+
return known.retryable === false
|
|
99
|
+
? {
|
|
100
|
+
code: failure.code,
|
|
101
|
+
status: known.status ?? status,
|
|
102
|
+
message: failure.message,
|
|
103
|
+
}
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
80
106
|
/**
|
|
81
107
|
* In-flight proactive refreshes, keyed by account.
|
|
82
108
|
*
|
|
@@ -1389,6 +1415,32 @@ async function handleCodexUpstreamFailure(rt, args) {
|
|
|
1389
1415
|
const { writeAttempt } = rt;
|
|
1390
1416
|
const { account, attempt, attemptStartedAt, upstream, authRetried } = args;
|
|
1391
1417
|
const errText = await upstream.text().catch(() => "");
|
|
1418
|
+
// A refusal of the request itself says nothing about the account: a refresh
|
|
1419
|
+
// would rotate its refresh token for nothing, a cooldown would park a healthy
|
|
1420
|
+
// account, and every other account would be refused the same way.
|
|
1421
|
+
const refusal = readCodexRequestRefusal(upstream.status, errText);
|
|
1422
|
+
if (refusal) {
|
|
1423
|
+
const errorMessage = refusal.message ??
|
|
1424
|
+
summarizeCodexUpstreamError(errText, "Codex refused the request");
|
|
1425
|
+
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, upstream.status);
|
|
1426
|
+
writeAttempt(account, attempt, attemptStartedAt, upstream.status, {
|
|
1427
|
+
errorType: "invalid_request_error",
|
|
1428
|
+
errorCode: refusal.code,
|
|
1429
|
+
errorMessage,
|
|
1430
|
+
retryable: false,
|
|
1431
|
+
});
|
|
1432
|
+
return {
|
|
1433
|
+
lastFailure: {
|
|
1434
|
+
errorType: "invalid_request_error",
|
|
1435
|
+
errorCode: refusal.code,
|
|
1436
|
+
retryable: false,
|
|
1437
|
+
},
|
|
1438
|
+
lastErrorStatus: refusal.status,
|
|
1439
|
+
lastErrorCode: refusal.code,
|
|
1440
|
+
lastErrorMessage: errorMessage,
|
|
1441
|
+
refusedRequest: true,
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1392
1444
|
// 401/403 → try a forced token refresh once, then rotate.
|
|
1393
1445
|
if ((upstream.status === 401 || upstream.status === 403) &&
|
|
1394
1446
|
!authRetried &&
|
|
@@ -1486,9 +1538,10 @@ async function handleCodexUpstreamFailure(rt, args) {
|
|
|
1486
1538
|
}
|
|
1487
1539
|
// Other non-ok → record and rotate.
|
|
1488
1540
|
if (upstream.status === 401 || upstream.status === 403) {
|
|
1489
|
-
// Reached
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1541
|
+
// Reached when the account has no refresh token to retry with, or when
|
|
1542
|
+
// the retry after a refresh was rejected again. Either way it will fail
|
|
1543
|
+
// identically on the next request, so park it briefly instead of letting
|
|
1544
|
+
// it stay first in line with unknown quota.
|
|
1492
1545
|
await saveAccountCooldown(account.key, Date.now() + CODEX_AUTH_COOLDOWN_MS, "auth").catch(() => undefined);
|
|
1493
1546
|
}
|
|
1494
1547
|
const errorMessage = summarizeCodexUpstreamError(errText, "Codex error");
|
|
@@ -1733,7 +1786,8 @@ loopbackPort, internalDispatch) {
|
|
|
1733
1786
|
let lastErrorCode;
|
|
1734
1787
|
let lastFailure = { errorType: "all_accounts_failed" };
|
|
1735
1788
|
let lastAttemptedAccount;
|
|
1736
|
-
|
|
1789
|
+
let requestRefused = false;
|
|
1790
|
+
accountLoop: for (const account of eligible) {
|
|
1737
1791
|
try {
|
|
1738
1792
|
tracer?.setAccountSelection({
|
|
1739
1793
|
strategy: "codex-quota-order",
|
|
@@ -1859,6 +1913,11 @@ loopbackPort, internalDispatch) {
|
|
|
1859
1913
|
}
|
|
1860
1914
|
({ lastFailure, lastErrorStatus, lastErrorCode, lastErrorMessage } =
|
|
1861
1915
|
failure);
|
|
1916
|
+
if (failure.refusedRequest) {
|
|
1917
|
+
// Every other account would be refused the same way.
|
|
1918
|
+
requestRefused = true;
|
|
1919
|
+
break accountLoop;
|
|
1920
|
+
}
|
|
1862
1921
|
break;
|
|
1863
1922
|
}
|
|
1864
1923
|
}
|
|
@@ -1874,7 +1933,7 @@ loopbackPort, internalDispatch) {
|
|
|
1874
1933
|
...lastFailure,
|
|
1875
1934
|
errorMessage: lastFailure.errorMessage ?? lastErrorMessage,
|
|
1876
1935
|
});
|
|
1877
|
-
return buildCodexErrorResponse(lastErrorStatus, lastErrorMessage);
|
|
1936
|
+
return buildCodexErrorResponse(lastErrorStatus, lastErrorMessage, requestRefused ? lastErrorCode : undefined);
|
|
1878
1937
|
};
|
|
1879
1938
|
try {
|
|
1880
1939
|
return await dispatch();
|
|
@@ -29,8 +29,17 @@ export { createTranscribeRoutes } from "./transcribeRoutes.js";
|
|
|
29
29
|
export declare function createAllRoutes(basePath?: string, options?: CreateRoutesOptions): RouteGroup[];
|
|
30
30
|
/**
|
|
31
31
|
* Register all routes with a server adapter
|
|
32
|
+
*
|
|
33
|
+
* With `options.perUserAuth` this also turns on the adapter's
|
|
34
|
+
* `carryMiddlewareState` (`enableMiddlewareStateCarry()`): the agent routes can
|
|
35
|
+
* only take identity from `ctx.user` if the adapter hands the handler what
|
|
36
|
+
* authentication middleware set. An adapter without that method must carry
|
|
37
|
+
* state itself; otherwise the routes see a user only where the adapter happens
|
|
38
|
+
* to share its context (Hono, when the client sent its own `X-Request-ID`), and
|
|
39
|
+
* every other request runs as anonymous.
|
|
32
40
|
*/
|
|
33
41
|
export declare function registerAllRoutes(adapter: {
|
|
34
42
|
registerRouteGroup: (group: RouteGroup) => void;
|
|
35
43
|
listRoutes?: () => RouteDefinition[];
|
|
44
|
+
enableMiddlewareStateCarry?: () => void;
|
|
36
45
|
}, basePath?: string, options?: CreateRoutesOptions): void;
|
|
@@ -41,7 +41,10 @@ export { createTranscribeRoutes } from "./transcribeRoutes.js";
|
|
|
41
41
|
*/
|
|
42
42
|
export function createAllRoutes(basePath = "/api", options) {
|
|
43
43
|
const routes = [
|
|
44
|
-
createAgentRoutes(basePath,
|
|
44
|
+
createAgentRoutes(basePath, {
|
|
45
|
+
...options?.agent,
|
|
46
|
+
perUserAuth: options?.perUserAuth,
|
|
47
|
+
}),
|
|
45
48
|
createToolRoutes(basePath),
|
|
46
49
|
createMCPRoutes(basePath),
|
|
47
50
|
createMemoryRoutes(basePath),
|
|
@@ -76,8 +79,19 @@ export function createAllRoutes(basePath = "/api", options) {
|
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
78
81
|
* Register all routes with a server adapter
|
|
82
|
+
*
|
|
83
|
+
* With `options.perUserAuth` this also turns on the adapter's
|
|
84
|
+
* `carryMiddlewareState` (`enableMiddlewareStateCarry()`): the agent routes can
|
|
85
|
+
* only take identity from `ctx.user` if the adapter hands the handler what
|
|
86
|
+
* authentication middleware set. An adapter without that method must carry
|
|
87
|
+
* state itself; otherwise the routes see a user only where the adapter happens
|
|
88
|
+
* to share its context (Hono, when the client sent its own `X-Request-ID`), and
|
|
89
|
+
* every other request runs as anonymous.
|
|
79
90
|
*/
|
|
80
91
|
export function registerAllRoutes(adapter, basePath = "/api", options) {
|
|
92
|
+
if (options?.perUserAuth) {
|
|
93
|
+
adapter.enableMiddlewareStateCarry?.();
|
|
94
|
+
}
|
|
81
95
|
// If adapter has listRoutes and getRoutes not provided, use adapter's listRoutes
|
|
82
96
|
const routeOptions = {
|
|
83
97
|
...options,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which route-handler contexts received the state the adapters now carry from
|
|
3
|
+
* middleware to the handler.
|
|
4
|
+
*
|
|
5
|
+
* Before the adapters carried that state, a handler normally got a fresh
|
|
6
|
+
* context with no `ctx.user` and no `ctx.session`. The one exception was Hono
|
|
7
|
+
* when the client sent its own `X-Request-ID`: Hono kept the middleware's
|
|
8
|
+
* context in a store keyed by that header, so the handler found it again, user
|
|
9
|
+
* included. The agent routes' default (`perUserAuth` unset) must not start
|
|
10
|
+
* trusting `ctx.user` on the requests that never had it, so the built-in
|
|
11
|
+
* adapters mark the contexts that received the carry, and the agent routes
|
|
12
|
+
* treat a marked context as the fresh one it used to be: identity from the body.
|
|
13
|
+
*
|
|
14
|
+
* Everything else keeps the earlier rule, an authenticated `ctx.user` wins: a
|
|
15
|
+
* Hono request with a caller-supplied `X-Request-ID` (never marked), a custom
|
|
16
|
+
* adapter that carries `ctx.user` itself, and a route handler called directly
|
|
17
|
+
* with a context the caller built.
|
|
18
|
+
*
|
|
19
|
+
* It is an internal fact about how the context was built, not part of
|
|
20
|
+
* `ServerContext`, and it is not exported from the package.
|
|
21
|
+
*
|
|
22
|
+
* @module server/utils/carriedContextMark
|
|
23
|
+
*/
|
|
24
|
+
import type { ServerContext } from "../../types/index.js";
|
|
25
|
+
/** Record that this context received state carried from middleware. */
|
|
26
|
+
export declare function markContextCarried(ctx: ServerContext): void;
|
|
27
|
+
/** `true` when `markContextCarried` was called for this context. */
|
|
28
|
+
export declare function isContextCarried(ctx: ServerContext): boolean;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which route-handler contexts received the state the adapters now carry from
|
|
3
|
+
* middleware to the handler.
|
|
4
|
+
*
|
|
5
|
+
* Before the adapters carried that state, a handler normally got a fresh
|
|
6
|
+
* context with no `ctx.user` and no `ctx.session`. The one exception was Hono
|
|
7
|
+
* when the client sent its own `X-Request-ID`: Hono kept the middleware's
|
|
8
|
+
* context in a store keyed by that header, so the handler found it again, user
|
|
9
|
+
* included. The agent routes' default (`perUserAuth` unset) must not start
|
|
10
|
+
* trusting `ctx.user` on the requests that never had it, so the built-in
|
|
11
|
+
* adapters mark the contexts that received the carry, and the agent routes
|
|
12
|
+
* treat a marked context as the fresh one it used to be: identity from the body.
|
|
13
|
+
*
|
|
14
|
+
* Everything else keeps the earlier rule, an authenticated `ctx.user` wins: a
|
|
15
|
+
* Hono request with a caller-supplied `X-Request-ID` (never marked), a custom
|
|
16
|
+
* adapter that carries `ctx.user` itself, and a route handler called directly
|
|
17
|
+
* with a context the caller built.
|
|
18
|
+
*
|
|
19
|
+
* It is an internal fact about how the context was built, not part of
|
|
20
|
+
* `ServerContext`, and it is not exported from the package.
|
|
21
|
+
*
|
|
22
|
+
* @module server/utils/carriedContextMark
|
|
23
|
+
*/
|
|
24
|
+
const receivedCarry = new WeakSet();
|
|
25
|
+
/** Record that this context received state carried from middleware. */
|
|
26
|
+
export function markContextCarried(ctx) {
|
|
27
|
+
receivedCarry.add(ctx);
|
|
28
|
+
}
|
|
29
|
+
/** `true` when `markContextCarried` was called for this context. */
|
|
30
|
+
export function isContextCarried(ctx) {
|
|
31
|
+
return receivedCarry.has(ctx);
|
|
32
|
+
}
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -1068,6 +1068,10 @@ export type CodexRotationFailure = {
|
|
|
1068
1068
|
lastErrorStatus: number;
|
|
1069
1069
|
lastErrorCode: string | undefined;
|
|
1070
1070
|
lastErrorMessage: string;
|
|
1071
|
+
/** The upstream refused the request itself (a policy or invalid-request
|
|
1072
|
+
* code), not the account: no other account would answer differently, so the
|
|
1073
|
+
* pool ends the request here instead of rotating. */
|
|
1074
|
+
refusedRequest?: boolean;
|
|
1071
1075
|
};
|
|
1072
1076
|
/** Minimal persistence contract needed by Codex rotating-token refreshes. */
|
|
1073
1077
|
export type CodexRefreshTokenStore = {
|