mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-27 01:22:10 +03:00
Compare commits
8 Commits
chore/file
...
fix/dedup-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2423ce3b35 | ||
|
|
91aeca0440 | ||
|
|
49749eb0d0 | ||
|
|
8d17110082 | ||
|
|
1ee4818224 | ||
|
|
6afc7ddab0 | ||
|
|
71fd806bb0 | ||
|
|
adedc351d7 |
@@ -435,7 +435,7 @@
|
||||
"src/shared/components/analytics/charts.tsx": 1346,
|
||||
"src/shared/services/cliRuntime.ts": 1459,
|
||||
"src/sse/handlers/chat.ts": 2493,
|
||||
"src/sse/services/auth.ts": 3376,
|
||||
"src/sse/services/auth.ts": 3432,
|
||||
"_rebaseline_2026_08_24_lasterror_provider_error_detail": "PR (ntdat812) own growth: src/sse/services/auth.ts 3344->3346 (+2). One line is the import of describeUpstreamFailure from @/shared/utils/upstreamError, which replaces the string-only collapse `typeof errorText === \"string\" ? errorText.slice(0, 100) : \"Provider error\"` at the single markAccountUnavailable chokepoint (net 0 lines there) — the logic itself lives in upstreamError.ts, next to the extractErrorMessage it reuses, so nothing else moved into this file. The second line is the repo's own lint-staged prettier pass splitting a pre-existing two-statements-on-one-line at getProviderCredentials (`invalidateManagedLease(...); log.warn(...)`); it re-applies on any commit that touches this file, so it is not separable from the change. Covered by tests/unit/provider-error-detail-lastError.test.ts.",
|
||||
"_rebaseline_2026_08_23_11186_synced_inventory_routing": "PR #11186 (pacocartones) own growth: src/sse/services/auth.ts 3260->3337 (+77, loadAdvertisedModelsForSelfHostedConnections + the modelNotAdvertised candidate-filter predicate — pins chat routing to the connection whose synced inventory actually advertises the model, fixing spurious model-not-found on multi-host self-hosted setups; at the existing credential-selection chokepoint, not extractable without splitting the selection flow). Covered by tests/unit/chat-routing-synced-inventory-11089.test.ts. Owner pre-authorized baseline bumps 2026-08-22.",
|
||||
"tests/unit/account-fallback-service.test.ts": 2044,
|
||||
@@ -459,7 +459,7 @@
|
||||
"_rebaseline_2026_08_22_11156_enter_check_disabled": "PR #11156 (rqzbeh) own growth: AddApiKeyModal.tsx 1080->1082 (+2, Enter keydown handler now mirrors the isCheckDisabled condition — owner-requested post-merge polish from #11056; the rest of the diff is Prettier reflow). Covered by tests/unit/ui/add-api-key-modal-enter-key.test.tsx (jsdom render test, Enter dispatch assertions).",
|
||||
"src/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections.ts": 1051,
|
||||
"src/shared/components/ModelSelectModal.tsx": 1138,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1321,
|
||||
"src/shared/constants/providers/apikey/gateways.ts": 1330,
|
||||
"open-sse/vendor/codex-chatgpt-web/bridge.ts": 1387,
|
||||
"_rebaseline_2026_08_11_v3850_merge_storm_provider_registry": "DRIFT do merge-storm 2026-08-11 (99 PRs mergeados no release/v3.8.50). AddApiKeyModal.tsx (PR #8949 ChatGPT Web provider) e useProviderConnections.ts/ModelSelectModal.tsx (PRs #9011 combo test-all, #9499 image combos) = UI nova legitima acima do cap; gateways.ts = god-file de catalogo de providers que cresceu com PRs #9009/#9421/#9468/#9594 (qualquer split arriscaria corromper o merge de novo — o proprio PR #9421 quebrou o arquivo); bridge.ts (PR #8949) = ponte Chromium vendored; proxyFetch.ts 1207->1220 = drift herdado de merges. Owner autorizou rebaseline com anotacao (2026-08-11).",
|
||||
"src/lib/modelCapabilities.ts": 1072,
|
||||
@@ -480,7 +480,8 @@
|
||||
"src/lib/guardrails/videoBridgeRuntime.ts": 1009,
|
||||
"_rebaseline_2026_08_24_video_bridge_fu02_fu07_sampler": "PRs #11344 (FU-02 one-frame scene-aware determinism) + #11381 (FU-07 opt-in segment_aware structural sampling) own growth: videoBridgeRuntime.ts <1000->1009, +9 (sum of both boarded together in the same merge-batch). #11344 adds the deterministic one-frame midpoint fallback + policyEffective=uniform report at the existing scene_aware seam; #11381 adds the bounded local-only FFmpeg structural pre-analysis pass (scene/freeze/blur/exposure/SI-TI) and its budget-reallocation logic. Covered by tests/unit/guardrails/videoBridgeSampler.test.ts, tests/unit/guardrails/videoBridgeFu07StructuralSampling.test.ts, tests/integration/video-bridge-sampler-ffmpeg.test.ts. Owner pre-authorized rebaseline for legitimate PR growth (2026-08-19 directive).",
|
||||
"open-sse/services/autoCombo/virtualFactory.ts": 1130,
|
||||
"src/lib/cloudflaredTunnel.ts": 1078
|
||||
"src/lib/cloudflaredTunnel.ts": 1078,
|
||||
"src/shared/components/RequestLoggerDetail.tsx": 1018
|
||||
},
|
||||
"_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.",
|
||||
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
|
||||
@@ -650,5 +651,6 @@
|
||||
"_rebaseline_2026_08_20_8338_cursor_image_provider": "PR (reimplementation of #8338, @valvesss): imageRegistry.ts 1019->1033 = new cursor IMAGE_PROVIDERS entry (Cursor plan image generation via Agent CLI), +14 lines of declarative provider metadata. Same god-registry no-split rationale as prior imageRegistry/gateways rebaselines.",
|
||||
"_rebaseline_2026_08_20_imageregistry_1034": "imageRegistry.ts 1033->1034: +1 line drift between #10842 (cursor image provider, froze at 1033) and its actual merged state on release (measured 1034) — trivial rebaseline, not a new feature.",
|
||||
"_rebaseline_2026_08_25_11146_subscription_first_auto": "PR #11146 (@yourspraveen, subscription-first auto groupings auto/subscription+auto/thrifty): open-sse/services/autoCombo/virtualFactory.ts is a NEW file in this PR landing at 1128 lines (+2 margin) — two opt-in flat auto ids built on the established auto/best-free pattern (connectionBillingCatalog + subscriptionLadder pure functions). Frozen at merge size per owner-authorized rebaseline directive (2026-08-19, merge-batch Step 4); no further growth without split rationale.",
|
||||
"_rebaseline_2026_08_26_mergebatch_v3851_batch1": "/merge-batch 2026-08-26 (v3.8.51): three legitimate growths from this batch. #11448 src/app/api/providers/[id]/test/route.ts 1237->1262 (auto-test-on-create wiring). #11495 src/sse/services/auth.ts 3346->3376 (web-cookie health-sweep verify-only path). #11561 src/lib/cloudflaredTunnel.ts new named-tunnel mode, lands at 1078 (+78 over the 1000 new-file cap) for the CLOUDFLARED_CONFIG named-tunnel flow (login->create->route dns config parsing + readiness detection). Owner-authorized rebaseline per merge-batch Step 4 (2026-08-19 directive); no further growth without split rationale."
|
||||
"_rebaseline_2026_08_26_mergebatch_v3851_batch1": "/merge-batch 2026-08-26 (v3.8.51): three legitimate growths from this batch. #11448 src/app/api/providers/[id]/test/route.ts 1237->1262 (auto-test-on-create wiring). #11495 src/sse/services/auth.ts 3346->3376 (web-cookie health-sweep verify-only path). #11561 src/lib/cloudflaredTunnel.ts new named-tunnel mode, lands at 1078 (+78 over the 1000 new-file cap) for the CLOUDFLARED_CONFIG named-tunnel flow (login->create->route dns config parsing + readiness detection). Owner-authorized rebaseline per merge-batch Step 4 (2026-08-19 directive); no further growth without split rationale.",
|
||||
"_rebaseline_2026_08_26_mergebatch_v3851_batch2": "/merge-batch 2026-08-26 (v3.8.51) batch 2: three legitimate growths. #11083 src/shared/components/RequestLoggerDetail.tsx new-file cap, lands at 1018 (+18 over 1000) — copy-all button for request detail modal. #11631 src/shared/constants/providers/apikey/gateways.ts 1321->1330 (1min.ai gateway entry). #11628 src/sse/services/auth.ts 3376->3432 (credential-health isolation from model failures). Owner-authorized rebaseline per merge-batch Step 4 (2026-08-19 directive); no further growth without split rationale."
|
||||
}
|
||||
|
||||
@@ -90,13 +90,19 @@ async function resolveZaiBrowserAttachments(
|
||||
> {
|
||||
try {
|
||||
// Browser-page upload: keep the original bytes/mimeType (no Cursor wire prep).
|
||||
// EncodedImage.mimeType is optional on the wire type, but every producer
|
||||
// reachable here (decodeDataUrl / fetchImageBytes) validates an image/*
|
||||
// string before pushing; the fallback only satisfies the attachment type.
|
||||
const images = await resolveCursorImages(imageUrls, { prepareForWire: false });
|
||||
return {
|
||||
attachments: images.map((image, index) => ({
|
||||
name: zaiImageFileName(image.mimeType, index),
|
||||
mimeType: image.mimeType,
|
||||
buffer: image.data,
|
||||
})),
|
||||
attachments: images.map((image, index) => {
|
||||
const mimeType = image.mimeType ?? "image/jpeg";
|
||||
return {
|
||||
name: zaiImageFileName(mimeType, index),
|
||||
mimeType,
|
||||
buffer: image.data,
|
||||
};
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
const message =
|
||||
|
||||
@@ -2984,7 +2984,10 @@ export async function handleChatCore({
|
||||
|
||||
const dedupRequestBody = { ...translatedBody, model: `${provider}/${model}`, stream };
|
||||
const dedupEnabled = shouldDeduplicate(dedupRequestBody);
|
||||
const dedupHash = dedupEnabled ? computeRequestHash(dedupRequestBody) : null;
|
||||
// Namespaced by the calling API key: dedup hands the SAME response object to
|
||||
// every joiner, so a shared hash across keys is a cross-principal response
|
||||
// leak (GHSA-6c7w-56xp-wpc6).
|
||||
const dedupHash = dedupEnabled ? computeRequestHash(dedupRequestBody, apiKeyInfo?.id) : null;
|
||||
|
||||
const executeProviderRequest = async (modelToCall = effectiveModel, allowDedup = false) => {
|
||||
const execute = async () => {
|
||||
|
||||
@@ -129,8 +129,26 @@ function extractSystemContent(body: Record<string, unknown>): unknown {
|
||||
* `translatedBody`), so the body shape here is whatever the target provider
|
||||
* format produced — see `extractPromptContent`/`extractSystemContent` for the
|
||||
* full list of shapes this must cover (#10249, #10438).
|
||||
*
|
||||
* `tenantId` (the calling API key's id) namespaces the hash. Dedup shares ONE
|
||||
* upstream call, and therefore one response, between everyone landing on the
|
||||
* same hash — so the hash has to answer "who is asking", not just "what is
|
||||
* being asked". Without it, two distinct API keys issuing the same request
|
||||
* joined the same in-flight promise: the response was produced with the
|
||||
* initiator's provider connection, under the initiator's per-key policy
|
||||
* (allowedConnections / allowedModels), billed to the initiator, and handed to
|
||||
* a different authenticated principal (GHSA-6c7w-56xp-wpc6).
|
||||
*
|
||||
* It is a PLAINTEXT prefix rather than digest input, matching
|
||||
* `semanticCache.generateSignature` (#3740): the id is an internal namespace
|
||||
* key, not a credential, and keeping it out of the digest avoids the
|
||||
* false-positive CodeQL js/insufficient-password-hash on a cache/dedup key.
|
||||
*
|
||||
* Omitting `tenantId` keeps the un-namespaced hash. Keyless local-first
|
||||
* deployments have no tenant boundary to preserve, and every such install would
|
||||
* otherwise silently lose dedup.
|
||||
*/
|
||||
export function computeRequestHash(requestBody: unknown): string {
|
||||
export function computeRequestHash(requestBody: unknown, tenantId?: string | null): string {
|
||||
const body = requestBody as Record<string, unknown>;
|
||||
const canonical = {
|
||||
model: body.model ?? null,
|
||||
@@ -145,7 +163,8 @@ export function computeRequestHash(requestBody: unknown): string {
|
||||
frequency_penalty: body.frequency_penalty ?? null,
|
||||
presence_penalty: body.presence_penalty ?? null,
|
||||
};
|
||||
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
|
||||
const digest = createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
|
||||
return tenantId ? `${tenantId}.${digest}` : digest;
|
||||
}
|
||||
|
||||
/** Determine whether a request should be deduplicated */
|
||||
|
||||
@@ -15,6 +15,7 @@ interface ErrorResponseBody {
|
||||
message: string;
|
||||
type?: string;
|
||||
code?: string;
|
||||
reason?: string;
|
||||
};
|
||||
upstream_details?: Record<string, unknown> | null; // sanitized upstream provider body
|
||||
}
|
||||
@@ -108,6 +109,7 @@ export function sanitizeUpstreamDetails(value: unknown, depth = 0): unknown {
|
||||
export type ErrorBodyClassification = {
|
||||
type?: string;
|
||||
code?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,6 +134,7 @@ export function buildErrorBody(
|
||||
message: safeMessage,
|
||||
type: classification?.type ?? errorInfo.type,
|
||||
code: classification?.code ?? errorInfo.code,
|
||||
reason: classification?.reason,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
121
package-lock.json
generated
121
package-lock.json
generated
@@ -86,7 +86,6 @@
|
||||
"undici": "^8.10.0",
|
||||
"update-notifier": "^7.3.1",
|
||||
"uuid": "^14.0.0",
|
||||
"wreq-js": "3.1.0",
|
||||
"ws": "^8.21.3",
|
||||
"xxhash-wasm": "^1.1.0",
|
||||
"yazl": "^3.3.1",
|
||||
@@ -110,7 +109,7 @@
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.6",
|
||||
"@types/better-sqlite3": "^9.6.0",
|
||||
"@types/bun": "*",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
@@ -161,7 +160,7 @@
|
||||
"better-sqlite3": "^13.0.2",
|
||||
"js-tiktoken": "^1.0.20",
|
||||
"keytar": "^7.9.0",
|
||||
"onnxruntime-node": "1.27.0",
|
||||
"onnxruntime-node": "1.24.3",
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tls-client-node": "^0.2.0",
|
||||
"wreq-js": "^3.1.0"
|
||||
@@ -15121,6 +15120,14 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/boolean": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz",
|
||||
"integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/bottleneck": {
|
||||
"version": "2.19.5",
|
||||
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
|
||||
@@ -18206,6 +18213,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
|
||||
"integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
@@ -19051,6 +19065,13 @@
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/es6-promisify": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz",
|
||||
@@ -21857,16 +21878,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/global-agent": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz",
|
||||
"integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz",
|
||||
"integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"globalthis": "^1.0.2",
|
||||
"matcher": "^4.0.0",
|
||||
"semver": "^7.3.5",
|
||||
"serialize-error": "^8.1.0"
|
||||
"boolean": "^3.0.1",
|
||||
"es6-error": "^4.1.1",
|
||||
"matcher": "^3.0.0",
|
||||
"roarr": "^2.15.3",
|
||||
"semver": "^7.3.2",
|
||||
"serialize-error": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
@@ -25424,6 +25447,13 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -27292,9 +27322,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz",
|
||||
"integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
"integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -27302,9 +27332,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/material-symbols": {
|
||||
@@ -30277,16 +30304,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-common": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.27.0.tgz",
|
||||
"integrity": "sha512-3KxL5wIVqa8Ex08jxSzncm9CMgw8CjOFyOQ7SxvG9o0cVLlhTNKXyIQuTbtX4tGPJEf73OER2xrjt4HJSBL4ow==",
|
||||
"version": "1.24.3",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz",
|
||||
"integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/onnxruntime-node": {
|
||||
"version": "1.27.0",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.27.0.tgz",
|
||||
"integrity": "sha512-QEzGwrvNBgv4uPVdnbHsOGG4G6T96mdlcFI8aAKPjMU8wOPpVocPXb6k3QGkaZagVTv2G9Bnnbo6Z3JdXr1fQw==",
|
||||
"version": "1.24.3",
|
||||
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz",
|
||||
"integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -30297,8 +30324,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"global-agent": "^4.1.3",
|
||||
"onnxruntime-common": "1.27.0"
|
||||
"global-agent": "^3.0.0",
|
||||
"onnxruntime-common": "1.24.3"
|
||||
}
|
||||
},
|
||||
"node_modules/onnxruntime-web": {
|
||||
@@ -33975,6 +34002,24 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/roarr": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||
"integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"boolean": "^3.0.1",
|
||||
"detect-node": "^2.0.4",
|
||||
"globalthis": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"semver-compare": "^1.0.0",
|
||||
"sprintf-js": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/robot3": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/robot3/-/robot3-0.4.1.tgz",
|
||||
@@ -34334,6 +34379,13 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
@@ -34361,13 +34413,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-error": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz",
|
||||
"integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
|
||||
"integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"type-fest": "^0.20.2"
|
||||
"type-fest": "^0.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -34377,9 +34429,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-error/node_modules/type-fest": {
|
||||
"version": "0.20.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
|
||||
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
@@ -35149,6 +35201,13 @@
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/sql.js": {
|
||||
"version": "1.14.2",
|
||||
"resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.14.2.tgz",
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
"postinstall": "node scripts/build/postinstall.mjs",
|
||||
"uninstall": "node scripts/build/uninstall.mjs",
|
||||
"uninstall:full": "node scripts/build/uninstall.mjs --full",
|
||||
"prepare": "husky",
|
||||
"prepare": "node -e \"try{require.resolve('husky')}catch(e){process.exit(0)};require('child_process').execSync('husky',{stdio:'inherit'})\"",
|
||||
"system-info": "node scripts/dev/system-info.mjs",
|
||||
"build:cli-api": "node --import tsx/esm scripts/cli/generate-api-commands.mjs",
|
||||
"postbuild": "node scripts/build/colocate-standalone.mjs",
|
||||
@@ -347,7 +347,7 @@
|
||||
"better-sqlite3": "^13.0.2",
|
||||
"js-tiktoken": "^1.0.20",
|
||||
"keytar": "^7.9.0",
|
||||
"onnxruntime-node": "1.27.0",
|
||||
"onnxruntime-node": "1.24.3",
|
||||
"sqlite-vec": "^0.1.9",
|
||||
"tls-client-node": "^0.2.0",
|
||||
"wreq-js": "^3.1.0"
|
||||
@@ -436,7 +436,7 @@
|
||||
"unrs-resolver": true
|
||||
},
|
||||
"overrides": {
|
||||
"onnxruntime-node": "1.27.0",
|
||||
"onnxruntime-node": "1.24.3",
|
||||
"fast-xml-parser": "^5.10.1",
|
||||
"sharp": "^0.35.3",
|
||||
"postcss": "^8.5.18",
|
||||
|
||||
@@ -19,12 +19,20 @@
|
||||
*/
|
||||
|
||||
const KEY = process.env.NVIDIA_API_KEY ?? "";
|
||||
const BASE_URL = process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
|
||||
const BASE_URL =
|
||||
process.env.NVIDIA_BASE_URL || "https://integrate.api.nvidia.com/v1/chat/completions";
|
||||
const MODEL = process.env.NVIDIA_MODEL || "openai/gpt-oss-120b";
|
||||
|
||||
// Neutralize CR/LF before logging so env-derived values (NVIDIA_MODEL, etc.)
|
||||
// cannot forge extra log lines (S5145 log injection).
|
||||
const line = (s = "") => console.log(String(s).replace(/[\r\n]+/g, " "));
|
||||
// cannot forge extra log lines (S5145 log injection). Also strip any raw
|
||||
// occurrence of the API key so an upstream error/response that echoes it
|
||||
// back (e.g. inside err.stack or a validation result) never reaches the
|
||||
// terminal in clear text (js/clear-text-logging, CWE-312/532).
|
||||
const line = (s = "") => {
|
||||
let out = String(s).replace(/[\r\n]+/g, " ");
|
||||
if (KEY) out = out.split(KEY).join("[REDACTED]");
|
||||
console.log(out);
|
||||
};
|
||||
const hr = () => line("─".repeat(72));
|
||||
|
||||
function show(label: string, value: unknown) {
|
||||
@@ -52,8 +60,13 @@ async function partA() {
|
||||
});
|
||||
line(" ✅ validateProviderApiKey retornou (sem crash):");
|
||||
show("resultado", result);
|
||||
if (typeof (result as any)?.error === "string" && (result as any).error.includes("startsWith")) {
|
||||
line(" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação.");
|
||||
if (
|
||||
typeof (result as any)?.error === "string" &&
|
||||
(result as any).error.includes("startsWith")
|
||||
) {
|
||||
line(
|
||||
" ⚠️ A mensagem de erro contém 'startsWith' → crash CAPTURADO dentro do try/catch da validação."
|
||||
);
|
||||
}
|
||||
} catch (err: any) {
|
||||
line(" ❌ validateProviderApiKey LANÇOU (crash não tratado):");
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
statSync,
|
||||
chmodSync,
|
||||
} from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { join, dirname, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { assembleStandalone } from "./assembleStandalone.mjs";
|
||||
@@ -35,6 +35,12 @@ import {
|
||||
APP_STAGING_REMOVAL_PATHS,
|
||||
findUnexpectedArtifactPaths,
|
||||
} from "./pack-artifact-policy.ts";
|
||||
import {
|
||||
collectWorkspaceVersions,
|
||||
findPackageJsonFiles,
|
||||
hasWorkspaceProtocol,
|
||||
resolvePackageJsonWorkspaceProtocols,
|
||||
} from "./resolveWorkspaceProtocols.ts";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -707,6 +713,33 @@ if (remainingUnexpectedFiles.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// -- Step 11: Resolve workspace: protocol dependencies -----------------
|
||||
// npm/pnpm workspace protocol specifiers (workspace:*, workspace:^, ...)
|
||||
// are meaningless to the npm registry and make `npm install -g omniroute`
|
||||
// fail with EUNSUPPORTEDPROTOCOL. Rewrite any that leaked into published
|
||||
// package.json files to the concrete workspace package version.
|
||||
// Only touch files inside the staged dist/ tree; workspace member source
|
||||
// package.json files must never be mutated by the publish step.
|
||||
const workspaceVersions = collectWorkspaceVersions(ROOT);
|
||||
const publishablePackageJsonDirs = [DIST_DIR];
|
||||
const publishablePackageJsonPaths = publishablePackageJsonDirs
|
||||
.flatMap((dir) => (existsSync(dir) ? findPackageJsonFiles(dir) : []))
|
||||
.filter((filePath) => existsSync(filePath));
|
||||
|
||||
for (const pkgJsonPath of publishablePackageJsonPaths) {
|
||||
let pkg: Record<string, unknown>;
|
||||
try {
|
||||
pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8")) as Record<string, unknown>;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!hasWorkspaceProtocol(pkg)) continue;
|
||||
|
||||
const resolved = resolvePackageJsonWorkspaceProtocols(pkg, workspaceVersions);
|
||||
writeFileSync(pkgJsonPath, JSON.stringify(resolved, null, 2) + "\n");
|
||||
console.log(` [resolved] Resolved workspace: protocols in ${relative(ROOT, pkgJsonPath)}`);
|
||||
}
|
||||
|
||||
// ── Done ───────────────────────────────────────────────────
|
||||
const distPkg = join(DIST_DIR, "package.json");
|
||||
if (existsSync(distPkg)) {
|
||||
|
||||
228
scripts/build/resolveWorkspaceProtocols.ts
Normal file
228
scripts/build/resolveWorkspaceProtocols.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Resolve pnpm/npm workspace protocol dependencies to concrete semver versions.
|
||||
*
|
||||
* The npm registry clients cannot parse `workspace:` specifiers. During prepublish
|
||||
* we rewrite any `workspace:*`, `workspace:^`, `workspace:~` (or explicit
|
||||
* `workspace:<range>`) dependency declarations to the matching workspace package's
|
||||
* actual version before npm pack/publish sees them.
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
const WORKSPACE_PROTOCOL_RE = /^workspace:/;
|
||||
|
||||
const DEPENDENCY_FIELDS = [
|
||||
"dependencies",
|
||||
"devDependencies",
|
||||
"peerDependencies",
|
||||
"optionalDependencies",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Parse a simple workspace glob entry into concrete directories relative to a root.
|
||||
* Supports entries like "packages/*" and literal directory names like "open-sse".
|
||||
*/
|
||||
function expandWorkspaceEntry(root: string, entry: string): string[] {
|
||||
const trimmed = entry.trim();
|
||||
if (!trimmed) return [];
|
||||
if (!trimmed.endsWith("/*")) {
|
||||
const dir = join(root, trimmed);
|
||||
try {
|
||||
return statSync(dir).isDirectory() ? [dir] : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
const parent = join(root, trimmed.slice(0, -2));
|
||||
let entries: string[] = [];
|
||||
try {
|
||||
entries = readdirSync(parent);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return entries
|
||||
.map((name) => join(parent, name))
|
||||
.filter((dir) => {
|
||||
try {
|
||||
return statSync(dir).isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the root package.json and, if present, pnpm-workspace.yaml to discover
|
||||
* workspace member directories. Returns a map of package name -> version.
|
||||
*/
|
||||
export function collectWorkspaceVersions(projectRoot: string): Map<string, string> {
|
||||
const versions = new Map<string, string>();
|
||||
|
||||
const rootPkgPath = join(projectRoot, "package.json");
|
||||
let workspaceEntries: string[] = [];
|
||||
try {
|
||||
const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf8")) as {
|
||||
workspaces?: string[];
|
||||
};
|
||||
if (Array.isArray(rootPkg.workspaces)) {
|
||||
workspaceEntries.push(...rootPkg.workspaces);
|
||||
}
|
||||
} catch {
|
||||
// ignore unreadable root package.json
|
||||
}
|
||||
|
||||
const pnpmWorkspacePath = join(projectRoot, "pnpm-workspace.yaml");
|
||||
try {
|
||||
const yamlContent = readFileSync(pnpmWorkspacePath, "utf8");
|
||||
const doc = yaml.load(yamlContent) as { packages?: unknown } | null | undefined;
|
||||
if (doc && Array.isArray(doc.packages)) {
|
||||
for (const entry of doc.packages) {
|
||||
if (typeof entry === "string" && entry) {
|
||||
workspaceEntries.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore missing or malformed pnpm-workspace.yaml
|
||||
}
|
||||
|
||||
const seenDirs = new Set<string>();
|
||||
for (const entry of workspaceEntries) {
|
||||
for (const dir of expandWorkspaceEntry(projectRoot, entry)) {
|
||||
if (seenDirs.has(dir)) continue;
|
||||
seenDirs.add(dir);
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as {
|
||||
name?: string;
|
||||
version?: string;
|
||||
};
|
||||
if (pkg.name && pkg.version) {
|
||||
versions.set(pkg.name, pkg.version);
|
||||
}
|
||||
} catch {
|
||||
// skip unreadable workspace member package.json
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return versions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve workspace protocol dependencies inside a package.json object.
|
||||
*
|
||||
* Replaces `workspace:*`, `workspace:^`, `workspace:~`, `workspace:<range>`,
|
||||
* and `workspace:<packageName>` with the concrete version of the referenced
|
||||
* workspace package. Throws if a workspace specifier cannot be resolved.
|
||||
*/
|
||||
export function resolvePackageJsonWorkspaceProtocols(
|
||||
pkg: Record<string, unknown>,
|
||||
workspaceVersions: Map<string, string>
|
||||
): Record<string, unknown> {
|
||||
const resolved: Record<string, unknown> = { ...pkg };
|
||||
|
||||
for (const field of DEPENDENCY_FIELDS) {
|
||||
const deps = pkg[field];
|
||||
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
|
||||
|
||||
const resolvedDeps: Record<string, string> = {};
|
||||
let changed = false;
|
||||
for (const [depName, versionSpec] of Object.entries(deps as Record<string, unknown>)) {
|
||||
if (typeof versionSpec !== "string") {
|
||||
resolvedDeps[depName] = String(versionSpec ?? "");
|
||||
continue;
|
||||
}
|
||||
if (!WORKSPACE_PROTOCOL_RE.test(versionSpec)) {
|
||||
resolvedDeps[depName] = versionSpec;
|
||||
continue;
|
||||
}
|
||||
|
||||
const body = versionSpec.slice("workspace:".length);
|
||||
let concrete: string | undefined;
|
||||
|
||||
if (body === "*") {
|
||||
concrete = workspaceVersions.get(depName);
|
||||
} else if (body === "^") {
|
||||
const version = workspaceVersions.get(depName);
|
||||
concrete = version ? `^${version}` : undefined;
|
||||
} else if (body === "~") {
|
||||
const version = workspaceVersions.get(depName);
|
||||
concrete = version ? `~${version}` : undefined;
|
||||
} else if (body.startsWith("^") || body.startsWith("~") || /^[\d<>=]/.test(body)) {
|
||||
// Explicit range inside workspace: protocol - strip the protocol prefix.
|
||||
concrete = body;
|
||||
} else {
|
||||
// workspace:<packageName> - resolve to that package's version.
|
||||
concrete = workspaceVersions.get(body);
|
||||
}
|
||||
|
||||
if (concrete) {
|
||||
resolvedDeps[depName] = concrete;
|
||||
changed = true;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Cannot resolve workspace protocol "${versionSpec}" for dependency "${depName}". ` +
|
||||
"Make sure the referenced package is a declared workspace member with a version."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
resolved[field] = resolvedDeps;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if any dependency field in the package contains a workspace: specifier.
|
||||
*/
|
||||
export function hasWorkspaceProtocol(pkg: Record<string, unknown>): boolean {
|
||||
for (const field of DEPENDENCY_FIELDS) {
|
||||
const deps = pkg[field];
|
||||
if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
|
||||
for (const versionSpec of Object.values(deps as Record<string, unknown>)) {
|
||||
if (typeof versionSpec === "string" && WORKSPACE_PROTOCOL_RE.test(versionSpec)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively walk a directory and return every package.json path found.
|
||||
* Stops descending after maxDepth to avoid runaway recursion on deep trees.
|
||||
*/
|
||||
export function findPackageJsonFiles(dir: string, maxDepth = 10): string[] {
|
||||
const results: string[] = [];
|
||||
if (maxDepth < 0) return results;
|
||||
|
||||
let entries: string[] = [];
|
||||
try {
|
||||
entries = readdirSync(dir);
|
||||
} catch {
|
||||
return results;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry === "node_modules") continue;
|
||||
const fullPath = join(dir, entry);
|
||||
let stat;
|
||||
try {
|
||||
stat = statSync(fullPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
results.push(...findPackageJsonFiles(fullPath, maxDepth - 1));
|
||||
} else if (entry === "package.json") {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -66,8 +66,8 @@ export function structuralRejectionResponse(status: 413 | 503, maxMessages: numb
|
||||
{
|
||||
type: historyLimit ? "payload_too_large" : "server_error",
|
||||
code: historyLimit ? "chat_history_too_large" : "chat_admission_busy",
|
||||
reason: historyLimit ? "message_limit" : "structure_limit",
|
||||
}
|
||||
);
|
||||
body.error.reason = historyLimit ? "message_limit" : "structure_limit";
|
||||
return new Response(JSON.stringify(body), { status, headers });
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, before } from "node:test";
|
||||
import { describe, it, beforeAll } from "vitest";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { getDbInstance } from "../../../src/lib/db/core.ts";
|
||||
@@ -51,7 +51,7 @@ function seed(provider: string, modelId: string, caps: {
|
||||
).run(provider, modelId, caps.tool_call ?? null, caps.reasoning ?? null, caps.limit_context ?? null);
|
||||
}
|
||||
|
||||
before(() => {
|
||||
beforeAll(() => {
|
||||
ensureTable();
|
||||
// Force both module caches to re-read after our seeds.
|
||||
invalidateCapabilitiesCache();
|
||||
|
||||
269
tests/unit/build/pack-no-workspace-protocol.test.ts
Normal file
269
tests/unit/build/pack-no-workspace-protocol.test.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {
|
||||
collectWorkspaceVersions,
|
||||
resolvePackageJsonWorkspaceProtocols,
|
||||
hasWorkspaceProtocol,
|
||||
findPackageJsonFiles,
|
||||
} from "../../../scripts/build/resolveWorkspaceProtocols.ts";
|
||||
|
||||
function tmpDir(prefix: string): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||
}
|
||||
|
||||
function writeJson(filePath: string, data: unknown): void {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
function readJson(filePath: string): Record<string, unknown> {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
test("resolvePackageJsonWorkspaceProtocols replaces workspace:*, workspace:^, workspace:~", () => {
|
||||
const versions = new Map([
|
||||
["@omniroute/open-sse", "3.8.51"],
|
||||
["@omniroute/shared", "1.2.3"],
|
||||
]);
|
||||
|
||||
const resolved = resolvePackageJsonWorkspaceProtocols(
|
||||
{
|
||||
name: "omniroute",
|
||||
version: "3.8.51",
|
||||
dependencies: {
|
||||
"@omniroute/open-sse": "workspace:^",
|
||||
"@omniroute/shared": "workspace:*",
|
||||
lodash: "^4.17.0",
|
||||
},
|
||||
devDependencies: {
|
||||
"@omniroute/open-sse": "workspace:~",
|
||||
},
|
||||
peerDependencies: {
|
||||
"@omniroute/shared": "workspace:1.2.3",
|
||||
},
|
||||
optionalDependencies: {
|
||||
"@omniroute/open-sse": "workspace:>=3.0.0",
|
||||
},
|
||||
},
|
||||
versions
|
||||
);
|
||||
|
||||
assert.equal((resolved.dependencies as Record<string, string>)["@omniroute/open-sse"], "^3.8.51");
|
||||
assert.equal((resolved.dependencies as Record<string, string>)["@omniroute/shared"], "1.2.3");
|
||||
assert.equal((resolved.dependencies as Record<string, string>).lodash, "^4.17.0");
|
||||
assert.equal(
|
||||
(resolved.devDependencies as Record<string, string>)["@omniroute/open-sse"],
|
||||
"~3.8.51"
|
||||
);
|
||||
assert.equal((resolved.peerDependencies as Record<string, string>)["@omniroute/shared"], "1.2.3");
|
||||
assert.equal(
|
||||
(resolved.optionalDependencies as Record<string, string>)["@omniroute/open-sse"],
|
||||
">=3.0.0"
|
||||
);
|
||||
});
|
||||
|
||||
test("resolvePackageJsonWorkspaceProtocols leaves non-workspace specs untouched", () => {
|
||||
const resolved = resolvePackageJsonWorkspaceProtocols(
|
||||
{
|
||||
name: "x",
|
||||
dependencies: {
|
||||
a: "^1.0.0",
|
||||
b: "file:../b",
|
||||
c: "npm:alias@1.0.0",
|
||||
},
|
||||
},
|
||||
new Map()
|
||||
);
|
||||
|
||||
assert.equal((resolved.dependencies as Record<string, string>).a, "^1.0.0");
|
||||
assert.equal((resolved.dependencies as Record<string, string>).b, "file:../b");
|
||||
assert.equal((resolved.dependencies as Record<string, string>).c, "npm:alias@1.0.0");
|
||||
assert.equal(hasWorkspaceProtocol(resolved), false);
|
||||
});
|
||||
|
||||
test("resolvePackageJsonWorkspaceProtocols throws for unresolvable workspace protocol", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
resolvePackageJsonWorkspaceProtocols(
|
||||
{
|
||||
name: "x",
|
||||
dependencies: {
|
||||
"@missing/pkg": "workspace:^",
|
||||
},
|
||||
},
|
||||
new Map()
|
||||
),
|
||||
/Cannot resolve workspace protocol/
|
||||
);
|
||||
});
|
||||
|
||||
test("collectWorkspaceVersions reads npm workspaces and pnpm-workspace.yaml", () => {
|
||||
const root = tmpDir("workspace-versions-");
|
||||
|
||||
writeJson(path.join(root, "package.json"), {
|
||||
name: "root",
|
||||
version: "0.0.0",
|
||||
workspaces: ["packages/*", "open-sse"],
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.join(root, "packages", "a"), { recursive: true });
|
||||
writeJson(path.join(root, "packages", "a", "package.json"), {
|
||||
name: "@scope/a",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.join(root, "open-sse"), { recursive: true });
|
||||
writeJson(path.join(root, "open-sse", "package.json"), {
|
||||
name: "@scope/open-sse",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
// pnpm-workspace.yaml adds an extra directory not in npm workspaces.
|
||||
fs.mkdirSync(path.join(root, "packages", "b"), { recursive: true });
|
||||
writeJson(path.join(root, "packages", "b", "package.json"), {
|
||||
name: "@scope/b",
|
||||
version: "3.0.0",
|
||||
});
|
||||
fs.writeFileSync(path.join(root, "pnpm-workspace.yaml"), "packages:\n - 'packages/*'\n");
|
||||
|
||||
const versions = collectWorkspaceVersions(root);
|
||||
assert.equal(versions.get("@scope/a"), "1.0.0");
|
||||
assert.equal(versions.get("@scope/open-sse"), "2.0.0");
|
||||
assert.equal(versions.get("@scope/b"), "3.0.0");
|
||||
});
|
||||
|
||||
test("findPackageJsonFiles skips node_modules and respects maxDepth", () => {
|
||||
const root = tmpDir("pkg-json-files-");
|
||||
fs.mkdirSync(path.join(root, "a"), { recursive: true });
|
||||
writeJson(path.join(root, "a", "package.json"), {});
|
||||
fs.mkdirSync(path.join(root, "node_modules", "x"), { recursive: true });
|
||||
writeJson(path.join(root, "node_modules", "x", "package.json"), {});
|
||||
|
||||
const files = findPackageJsonFiles(root);
|
||||
assert.equal(files.length, 1);
|
||||
assert.ok(files[0].endsWith(path.join("a", "package.json")));
|
||||
|
||||
// Build a deep tree and confirm maxDepth bounds the walk.
|
||||
const deep = tmpDir("pkg-json-deep-");
|
||||
let current = deep;
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
current = path.join(current, `level${i}`);
|
||||
fs.mkdirSync(current, { recursive: true });
|
||||
}
|
||||
writeJson(path.join(current, "package.json"), {});
|
||||
assert.equal(findPackageJsonFiles(deep, 10).length, 0);
|
||||
assert.equal(findPackageJsonFiles(deep, 12).length, 1);
|
||||
});
|
||||
|
||||
test("collectWorkspaceVersions parses pnpm-workspace.yaml with js-yaml", () => {
|
||||
const root = tmpDir("pnpm-yaml-");
|
||||
|
||||
writeJson(path.join(root, "package.json"), { name: "root", version: "0.0.0" });
|
||||
|
||||
// Flow-style array, nested quotes, comments inside the packages list, and an
|
||||
// unrelated top-level key before packages are all valid YAML that the old line
|
||||
// scanner could not handle.
|
||||
fs.writeFileSync(
|
||||
path.join(root, "pnpm-workspace.yaml"),
|
||||
"preferWorkspacePackages: true\n" +
|
||||
"packages:\n" +
|
||||
' - "packages/*"\n' +
|
||||
" - 'apps/*'\n" +
|
||||
" # comment inside the list\n" +
|
||||
" - open-sse\n"
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.join(root, "packages", "a"), { recursive: true });
|
||||
writeJson(path.join(root, "packages", "a", "package.json"), {
|
||||
name: "@scope/a",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.join(root, "apps", "web"), { recursive: true });
|
||||
writeJson(path.join(root, "apps", "web", "package.json"), {
|
||||
name: "@scope/web",
|
||||
version: "2.0.0",
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.join(root, "open-sse"), { recursive: true });
|
||||
writeJson(path.join(root, "open-sse", "package.json"), {
|
||||
name: "@scope/open-sse",
|
||||
version: "3.0.0",
|
||||
});
|
||||
|
||||
const versions = collectWorkspaceVersions(root);
|
||||
assert.equal(versions.get("@scope/a"), "1.0.0");
|
||||
assert.equal(versions.get("@scope/web"), "2.0.0");
|
||||
assert.equal(versions.get("@scope/open-sse"), "3.0.0");
|
||||
});
|
||||
|
||||
test("prepublish Step 11 fixture resolves workspace: protocols in dist package.json files", () => {
|
||||
const root = tmpDir("prepublish-step11-");
|
||||
const distDir = path.join(root, "dist");
|
||||
|
||||
// Workspace member source files contain a workspace: specifier (simulating the
|
||||
// monorepo source). They must NOT be mutated by the publish step.
|
||||
fs.mkdirSync(path.join(root, "packages", "shared"), { recursive: true });
|
||||
const sourcePkgPath = path.join(root, "packages", "shared", "package.json");
|
||||
writeJson(sourcePkgPath, {
|
||||
name: "@scope/shared",
|
||||
version: "1.2.3",
|
||||
dependencies: {
|
||||
"@scope/other": "workspace:*",
|
||||
},
|
||||
});
|
||||
|
||||
fs.mkdirSync(path.join(root, "packages", "other"), { recursive: true });
|
||||
writeJson(path.join(root, "packages", "other", "package.json"), {
|
||||
name: "@scope/other",
|
||||
version: "4.5.6",
|
||||
});
|
||||
|
||||
writeJson(path.join(root, "package.json"), {
|
||||
name: "root",
|
||||
version: "0.0.0",
|
||||
workspaces: ["packages/*"],
|
||||
});
|
||||
|
||||
// The staged dist/ package.json contains workspace: specifiers that leaked
|
||||
// into the publish artifact and must be rewritten to concrete versions.
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
const distPkgPath = path.join(distDir, "package.json");
|
||||
writeJson(distPkgPath, {
|
||||
name: "omniroute",
|
||||
version: "3.8.51",
|
||||
dependencies: {
|
||||
"@scope/shared": "workspace:^",
|
||||
"@scope/other": "workspace:*",
|
||||
lodash: "^4.17.0",
|
||||
},
|
||||
});
|
||||
|
||||
// This is the same logic prepublish.ts Step 11 runs, scoped to the fixture.
|
||||
const workspaceVersions = collectWorkspaceVersions(root);
|
||||
const publishablePackageJsonPaths = findPackageJsonFiles(distDir).filter((filePath) =>
|
||||
fs.existsSync(filePath)
|
||||
);
|
||||
|
||||
for (const pkgJsonPath of publishablePackageJsonPaths) {
|
||||
const pkg = readJson(pkgJsonPath);
|
||||
if (!hasWorkspaceProtocol(pkg)) continue;
|
||||
const resolved = resolvePackageJsonWorkspaceProtocols(pkg, workspaceVersions);
|
||||
fs.writeFileSync(pkgJsonPath, JSON.stringify(resolved, null, 2) + "\n");
|
||||
}
|
||||
|
||||
// dist/package.json must have concrete versions.
|
||||
const distPkg = readJson(distPkgPath);
|
||||
assert.equal((distPkg.dependencies as Record<string, string>)["@scope/shared"], "^1.2.3");
|
||||
assert.equal((distPkg.dependencies as Record<string, string>)["@scope/other"], "4.5.6");
|
||||
assert.equal((distPkg.dependencies as Record<string, string>).lodash, "^4.17.0");
|
||||
assert.equal(hasWorkspaceProtocol(distPkg), false);
|
||||
|
||||
// Source package.json must remain untouched.
|
||||
const sourcePkg = readJson(sourcePkgPath);
|
||||
assert.equal((sourcePkg.dependencies as Record<string, string>)["@scope/other"], "workspace:*");
|
||||
});
|
||||
35
tests/unit/chat-admission-rejection-reason.test.ts
Normal file
35
tests/unit/chat-admission-rejection-reason.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
structuralRejectionResponse,
|
||||
} from "../../src/shared/middleware/chatAdmissionResponses.ts";
|
||||
|
||||
// Pins the machine-readable error.reason contract of the chat admission
|
||||
// structural rejections (#TS2339 regression guard): buildErrorBody now owns
|
||||
// the reason field via ErrorBodyClassification, so the response bodies keep
|
||||
// carrying it without post-construction mutation of an untyped field.
|
||||
test("structuralRejectionResponse 413 carries reason=message_limit classification", () => {
|
||||
const res = structuralRejectionResponse(413, 40);
|
||||
assert.equal(res.status, 413);
|
||||
assert.ok(!res.headers.has("Retry-After"), "413 is not retryable-by-header");
|
||||
return res.text().then((raw) => {
|
||||
const body = JSON.parse(raw);
|
||||
assert.equal(body.error.reason, "message_limit");
|
||||
assert.equal(body.error.type, "payload_too_large");
|
||||
assert.equal(body.error.code, "chat_history_too_large");
|
||||
assert.ok(!body.error.message.includes("at /"), "must not leak stack traces");
|
||||
});
|
||||
});
|
||||
|
||||
test("structuralRejectionResponse 503 carries reason=structure_limit and Retry-After", () => {
|
||||
const res = structuralRejectionResponse(503, 40);
|
||||
assert.equal(res.status, 503);
|
||||
assert.equal(res.headers.get("Retry-After"), "1");
|
||||
return res.text().then((raw) => {
|
||||
const body = JSON.parse(raw);
|
||||
assert.equal(body.error.reason, "structure_limit");
|
||||
assert.equal(body.error.type, "server_error");
|
||||
assert.equal(body.error.code, "chat_admission_busy");
|
||||
});
|
||||
});
|
||||
54
tests/unit/prepare-script-husky-guard.test.ts
Normal file
54
tests/unit/prepare-script-husky-guard.test.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { execSync } from "node:child_process";
|
||||
import { join } from "node:path";
|
||||
|
||||
describe("package.json prepare script (#11571)", () => {
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(import.meta.dirname, "../../package.json"), "utf8")
|
||||
);
|
||||
|
||||
it("has a prepare script that guards against missing husky", () => {
|
||||
const prepare = pkg.scripts?.prepare;
|
||||
assert.ok(prepare, "prepare script must exist");
|
||||
assert.ok(
|
||||
prepare.includes("require.resolve") || prepare.includes("existsSync"),
|
||||
"prepare must check if husky is available before running it"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not hard-fail when husky is absent", () => {
|
||||
const prepare = pkg.scripts?.prepare;
|
||||
assert.ok(
|
||||
!prepare.match(/^\s*husky\s*$/),
|
||||
"prepare must not be a bare 'husky' call without a guard"
|
||||
);
|
||||
});
|
||||
|
||||
it("exits cleanly without invoking husky when it is unresolvable", () => {
|
||||
const prepare = pkg.scripts?.prepare;
|
||||
assert.ok(prepare);
|
||||
assert.ok(
|
||||
!prepare.includes("&& husky") && !prepare.includes("|| husky"),
|
||||
"husky must not appear as a separate shell command after the guard — " +
|
||||
"process.exit(0) in the guard would still allow && to proceed"
|
||||
);
|
||||
});
|
||||
|
||||
it("still calls husky when available", () => {
|
||||
const prepare = pkg.scripts?.prepare;
|
||||
assert.ok(
|
||||
prepare.includes("husky"),
|
||||
"prepare must still invoke husky when it is installed"
|
||||
);
|
||||
});
|
||||
|
||||
it("exits 0 in an environment where husky is not resolvable", () => {
|
||||
const result = execSync(
|
||||
"node -e \"try{require.resolve('husky_nonexistent_pkg')}catch(e){process.exit(0)};process.exit(1)\"",
|
||||
{ encoding: "utf8", stdio: "pipe" }
|
||||
);
|
||||
assert.equal(result, "", "should produce no output and exit 0");
|
||||
});
|
||||
});
|
||||
97
tests/unit/request-dedup-tenant-isolation.test.ts
Normal file
97
tests/unit/request-dedup-tenant-isolation.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
computeRequestHash,
|
||||
deduplicate,
|
||||
clearInflight,
|
||||
} from "../../open-sse/services/requestDedup.ts";
|
||||
|
||||
// GHSA-6c7w-56xp-wpc6 follow-up. The dedup layer shares ONE upstream call — and
|
||||
// therefore one response object — between every concurrent caller landing on the
|
||||
// same hash. The hash canonicalized model + prompt + sampling params and nothing
|
||||
// about WHO was asking, so two distinct OmniRoute API keys issuing the same
|
||||
// request joined the same in-flight promise: the response was produced with the
|
||||
// initiator's provider connection, under the initiator's per-key policy, and
|
||||
// handed to a different authenticated principal.
|
||||
//
|
||||
// #10438 fixed the *prompt* half of this class (translated bodies hashing the
|
||||
// prompt as `null`). This is the *identity* half.
|
||||
|
||||
const body = {
|
||||
messages: [{ role: "user", content: "Summarize the incident report." }],
|
||||
temperature: 0,
|
||||
model: "codex/gpt-5.6-terra",
|
||||
stream: false,
|
||||
};
|
||||
|
||||
test("the same request from two different API keys does NOT share a dedup hash", () => {
|
||||
const hashKey1 = computeRequestHash(body, "apikey-1");
|
||||
const hashKey2 = computeRequestHash(body, "apikey-2");
|
||||
assert.notEqual(
|
||||
hashKey1,
|
||||
hashKey2,
|
||||
"an identical request from a different API key must not join the first key's in-flight call"
|
||||
);
|
||||
});
|
||||
|
||||
test("the same request from the same API key still dedups", () => {
|
||||
assert.equal(computeRequestHash(body, "apikey-1"), computeRequestHash(body, "apikey-1"));
|
||||
});
|
||||
|
||||
test("concurrent identical requests on two keys each get their own response", async () => {
|
||||
clearInflight();
|
||||
const hash1 = computeRequestHash(body, "apikey-1");
|
||||
const hash2 = computeRequestHash(body, "apikey-2");
|
||||
|
||||
let released!: () => void;
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
released = resolve;
|
||||
});
|
||||
|
||||
const first = deduplicate(hash1, async () => {
|
||||
await gate;
|
||||
return "RESPONSE_FOR_KEY_1";
|
||||
});
|
||||
const second = deduplicate(hash2, async () => {
|
||||
await gate;
|
||||
return "RESPONSE_FOR_KEY_2";
|
||||
});
|
||||
released();
|
||||
|
||||
const [a, b] = await Promise.all([first, second]);
|
||||
assert.equal(a.result, "RESPONSE_FOR_KEY_1");
|
||||
assert.equal(b.result, "RESPONSE_FOR_KEY_2");
|
||||
assert.equal(b.wasDeduplicated, false, "key 2 must not have joined key 1's in-flight call");
|
||||
});
|
||||
|
||||
test("an unkeyed (anonymous) caller keeps the un-namespaced hash", () => {
|
||||
// Keyless local-first deployments have no tenant boundary to preserve, so the
|
||||
// behaviour there is unchanged — and must stay stable, or every such install
|
||||
// silently loses dedup.
|
||||
const anonymous = computeRequestHash(body);
|
||||
assert.equal(computeRequestHash(body, undefined), anonymous);
|
||||
assert.equal(computeRequestHash(body, null as unknown as undefined), anonymous);
|
||||
assert.notEqual(computeRequestHash(body, "apikey-1"), anonymous);
|
||||
});
|
||||
|
||||
test("the tenant namespace cannot be forged by a colliding request body", () => {
|
||||
// The namespace is a plaintext prefix, so it must not be possible to move
|
||||
// between namespaces by crafting a body — the separator has to survive.
|
||||
const h1 = computeRequestHash(body, "a");
|
||||
const h2 = computeRequestHash(body, "a.b");
|
||||
assert.notEqual(h1, h2);
|
||||
assert.ok(h1.startsWith("a."), "namespace must prefix the digest");
|
||||
});
|
||||
|
||||
test("chatCore passes the caller's API key id into the dedup hash", async () => {
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const { fileURLToPath } = await import("node:url");
|
||||
const source = readFileSync(
|
||||
fileURLToPath(new URL("../../open-sse/handlers/chatCore.ts", import.meta.url)),
|
||||
"utf8"
|
||||
);
|
||||
assert.ok(
|
||||
/computeRequestHash\(\s*dedupRequestBody\s*,\s*apiKeyInfo\?\.id/.test(source),
|
||||
"the dedup hash must be namespaced by the calling API key (GHSA-6c7w-56xp-wpc6)"
|
||||
);
|
||||
});
|
||||
20
tests/unit/zai-web-attachment-mime-contract.test.ts
Normal file
20
tests/unit/zai-web-attachment-mime-contract.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { resolveCursorImages } from "../../open-sse/utils/cursorImages.ts";
|
||||
|
||||
// zai-web maps resolveCursorImages() output into browser-upload attachments
|
||||
// whose mimeType is REQUIRED. EncodedImage.mimeType is optional on the wire
|
||||
// type, so zai-web carries an `?? "image/jpeg"` fallback — this test pins the
|
||||
// producer contract that makes the fallback dead code in practice: every
|
||||
// image that reaches a browser upload must arrive with a concrete image/*
|
||||
// mime string (decodeDataUrl / fetchImageBytes validate it before pushing).
|
||||
const PIXEL_PNG =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
||||
|
||||
test("resolveCursorImages (prepareForWire:false) always yields a concrete image/* mimeType", async () => {
|
||||
const images = await resolveCursorImages([PIXEL_PNG], { prepareForWire: false });
|
||||
assert.equal(images.length, 1);
|
||||
assert.equal(typeof images[0]!.mimeType, "string");
|
||||
assert.match(images[0]!.mimeType as string, /^image\//);
|
||||
});
|
||||
Reference in New Issue
Block a user