@sriinnu/kosha-discovery 0.5.0
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/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/aliases.d.ts +51 -0
- package/dist/aliases.d.ts.map +1 -0
- package/dist/aliases.js +115 -0
- package/dist/aliases.js.map +1 -0
- package/dist/cache.d.ts +57 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +123 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli-commands.d.ts +142 -0
- package/dist/cli-commands.d.ts.map +1 -0
- package/dist/cli-commands.js +746 -0
- package/dist/cli-commands.js.map +1 -0
- package/dist/cli-format.d.ts +119 -0
- package/dist/cli-format.d.ts.map +1 -0
- package/dist/cli-format.js +171 -0
- package/dist/cli-format.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +143 -0
- package/dist/cli.js.map +1 -0
- package/dist/credentials/index.d.ts +2 -0
- package/dist/credentials/index.d.ts.map +1 -0
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/index.js.map +1 -0
- package/dist/credentials/resolver.d.ts +170 -0
- package/dist/credentials/resolver.d.ts.map +1 -0
- package/dist/credentials/resolver.js +597 -0
- package/dist/credentials/resolver.js.map +1 -0
- package/dist/discovery/anthropic.d.ts +49 -0
- package/dist/discovery/anthropic.d.ts.map +1 -0
- package/dist/discovery/anthropic.js +101 -0
- package/dist/discovery/anthropic.js.map +1 -0
- package/dist/discovery/base.d.ts +59 -0
- package/dist/discovery/base.d.ts.map +1 -0
- package/dist/discovery/base.js +123 -0
- package/dist/discovery/base.js.map +1 -0
- package/dist/discovery/bedrock.d.ts +122 -0
- package/dist/discovery/bedrock.d.ts.map +1 -0
- package/dist/discovery/bedrock.js +356 -0
- package/dist/discovery/bedrock.js.map +1 -0
- package/dist/discovery/cerebras.d.ts +43 -0
- package/dist/discovery/cerebras.d.ts.map +1 -0
- package/dist/discovery/cerebras.js +65 -0
- package/dist/discovery/cerebras.js.map +1 -0
- package/dist/discovery/cohere.d.ts +57 -0
- package/dist/discovery/cohere.d.ts.map +1 -0
- package/dist/discovery/cohere.js +82 -0
- package/dist/discovery/cohere.js.map +1 -0
- package/dist/discovery/deepinfra.d.ts +41 -0
- package/dist/discovery/deepinfra.d.ts.map +1 -0
- package/dist/discovery/deepinfra.js +86 -0
- package/dist/discovery/deepinfra.js.map +1 -0
- package/dist/discovery/deepseek.d.ts +17 -0
- package/dist/discovery/deepseek.d.ts.map +1 -0
- package/dist/discovery/deepseek.js +45 -0
- package/dist/discovery/deepseek.js.map +1 -0
- package/dist/discovery/fireworks.d.ts +48 -0
- package/dist/discovery/fireworks.d.ts.map +1 -0
- package/dist/discovery/fireworks.js +101 -0
- package/dist/discovery/fireworks.js.map +1 -0
- package/dist/discovery/glm.d.ts +17 -0
- package/dist/discovery/glm.d.ts.map +1 -0
- package/dist/discovery/glm.js +52 -0
- package/dist/discovery/glm.js.map +1 -0
- package/dist/discovery/google.d.ts +53 -0
- package/dist/discovery/google.d.ts.map +1 -0
- package/dist/discovery/google.js +122 -0
- package/dist/discovery/google.js.map +1 -0
- package/dist/discovery/groq.d.ts +44 -0
- package/dist/discovery/groq.d.ts.map +1 -0
- package/dist/discovery/groq.js +89 -0
- package/dist/discovery/groq.js.map +1 -0
- package/dist/discovery/index.d.ts +48 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +116 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/llama-cpp.d.ts +45 -0
- package/dist/discovery/llama-cpp.d.ts.map +1 -0
- package/dist/discovery/llama-cpp.js +137 -0
- package/dist/discovery/llama-cpp.js.map +1 -0
- package/dist/discovery/minimax.d.ts +16 -0
- package/dist/discovery/minimax.d.ts.map +1 -0
- package/dist/discovery/minimax.js +56 -0
- package/dist/discovery/minimax.js.map +1 -0
- package/dist/discovery/mistral.d.ts +45 -0
- package/dist/discovery/mistral.d.ts.map +1 -0
- package/dist/discovery/mistral.js +82 -0
- package/dist/discovery/mistral.js.map +1 -0
- package/dist/discovery/moonshot.d.ts +16 -0
- package/dist/discovery/moonshot.d.ts.map +1 -0
- package/dist/discovery/moonshot.js +46 -0
- package/dist/discovery/moonshot.js.map +1 -0
- package/dist/discovery/nvidia.d.ts +60 -0
- package/dist/discovery/nvidia.d.ts.map +1 -0
- package/dist/discovery/nvidia.js +145 -0
- package/dist/discovery/nvidia.js.map +1 -0
- package/dist/discovery/ollama.d.ts +57 -0
- package/dist/discovery/ollama.d.ts.map +1 -0
- package/dist/discovery/ollama.js +151 -0
- package/dist/discovery/ollama.js.map +1 -0
- package/dist/discovery/openai-compatible.d.ts +126 -0
- package/dist/discovery/openai-compatible.d.ts.map +1 -0
- package/dist/discovery/openai-compatible.js +161 -0
- package/dist/discovery/openai-compatible.js.map +1 -0
- package/dist/discovery/openai.d.ts +62 -0
- package/dist/discovery/openai.d.ts.map +1 -0
- package/dist/discovery/openai.js +157 -0
- package/dist/discovery/openai.js.map +1 -0
- package/dist/discovery/openrouter.d.ts +78 -0
- package/dist/discovery/openrouter.d.ts.map +1 -0
- package/dist/discovery/openrouter.js +184 -0
- package/dist/discovery/openrouter.js.map +1 -0
- package/dist/discovery/perplexity.d.ts +66 -0
- package/dist/discovery/perplexity.d.ts.map +1 -0
- package/dist/discovery/perplexity.js +86 -0
- package/dist/discovery/perplexity.js.map +1 -0
- package/dist/discovery/together.d.ts +41 -0
- package/dist/discovery/together.d.ts.map +1 -0
- package/dist/discovery/together.js +88 -0
- package/dist/discovery/together.js.map +1 -0
- package/dist/discovery/vertex.d.ts +146 -0
- package/dist/discovery/vertex.d.ts.map +1 -0
- package/dist/discovery/vertex.js +371 -0
- package/dist/discovery/vertex.js.map +1 -0
- package/dist/discovery/zai.d.ts +17 -0
- package/dist/discovery/zai.d.ts.map +1 -0
- package/dist/discovery/zai.js +51 -0
- package/dist/discovery/zai.js.map +1 -0
- package/dist/discovery-contract.d.ts +186 -0
- package/dist/discovery-contract.d.ts.map +1 -0
- package/dist/discovery-contract.js +115 -0
- package/dist/discovery-contract.js.map +1 -0
- package/dist/discovery-routes.d.ts +14 -0
- package/dist/discovery-routes.d.ts.map +1 -0
- package/dist/discovery-routes.js +98 -0
- package/dist/discovery-routes.js.map +1 -0
- package/dist/enrichment/index.d.ts +2 -0
- package/dist/enrichment/index.d.ts.map +1 -0
- package/dist/enrichment/index.js +2 -0
- package/dist/enrichment/index.js.map +1 -0
- package/dist/enrichment/litellm.d.ts +52 -0
- package/dist/enrichment/litellm.d.ts.map +1 -0
- package/dist/enrichment/litellm.js +205 -0
- package/dist/enrichment/litellm.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/normalize.d.ts +64 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +166 -0
- package/dist/normalize.js.map +1 -0
- package/dist/provider-catalog.d.ts +62 -0
- package/dist/provider-catalog.d.ts.map +1 -0
- package/dist/provider-catalog.js +331 -0
- package/dist/provider-catalog.js.map +1 -0
- package/dist/registry-delta.d.ts +17 -0
- package/dist/registry-delta.d.ts.map +1 -0
- package/dist/registry-delta.js +74 -0
- package/dist/registry-delta.js.map +1 -0
- package/dist/registry-discovery.d.ts +66 -0
- package/dist/registry-discovery.d.ts.map +1 -0
- package/dist/registry-discovery.js +339 -0
- package/dist/registry-discovery.js.map +1 -0
- package/dist/registry-query.d.ts +74 -0
- package/dist/registry-query.d.ts.map +1 -0
- package/dist/registry-query.js +310 -0
- package/dist/registry-query.js.map +1 -0
- package/dist/registry-runtime.d.ts +48 -0
- package/dist/registry-runtime.d.ts.map +1 -0
- package/dist/registry-runtime.js +255 -0
- package/dist/registry-runtime.js.map +1 -0
- package/dist/registry-selection.d.ts +31 -0
- package/dist/registry-selection.d.ts.map +1 -0
- package/dist/registry-selection.js +167 -0
- package/dist/registry-selection.js.map +1 -0
- package/dist/registry-state.d.ts +92 -0
- package/dist/registry-state.d.ts.map +1 -0
- package/dist/registry-state.js +35 -0
- package/dist/registry-state.js.map +1 -0
- package/dist/registry.d.ts +127 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +248 -0
- package/dist/registry.js.map +1 -0
- package/dist/resilience.d.ts +212 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +292 -0
- package/dist/resilience.js.map +1 -0
- package/dist/security.d.ts +44 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +184 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +41 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +320 -0
- package/dist/server.js.map +1 -0
- package/dist/shell-safe.d.ts +15 -0
- package/dist/shell-safe.d.ts.map +1 -0
- package/dist/shell-safe.js +20 -0
- package/dist/shell-safe.js.map +1 -0
- package/dist/types.d.ts +365 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/logo.svg +81 -0
- package/package.json +85 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kosha-discovery — Resilience primitives.
|
|
3
|
+
*
|
|
4
|
+
* Provides a {@link CircuitBreaker} for per-provider fault isolation,
|
|
5
|
+
* a {@link HealthTracker} that manages breakers for all known providers,
|
|
6
|
+
* and a {@link StaleCachePolicy} that implements stale-while-revalidate
|
|
7
|
+
* semantics on top of {@link KoshaCache}.
|
|
8
|
+
*
|
|
9
|
+
* The module is self-contained with no side-effects on import.
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// CircuitBreaker
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/**
|
|
16
|
+
* Per-provider circuit breaker with three states.
|
|
17
|
+
*
|
|
18
|
+
* - **closed** — Normal operation. All requests are allowed. Consecutive
|
|
19
|
+
* failures are counted; once the {@link CircuitBreakerOptions.failureThreshold}
|
|
20
|
+
* is reached the circuit transitions to `open`.
|
|
21
|
+
* - **open** — Requests are rejected immediately (canExecute returns false).
|
|
22
|
+
* After {@link CircuitBreakerOptions.resetTimeoutMs} has elapsed the circuit
|
|
23
|
+
* transitions to `half-open` so a single probe request can be attempted.
|
|
24
|
+
* - **half-open** — Exactly one request is let through. A success closes the
|
|
25
|
+
* circuit; a failure re-opens it and resets the timeout.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const cb = new CircuitBreaker("anthropic", { failureThreshold: 5 });
|
|
29
|
+
* if (cb.canExecute()) {
|
|
30
|
+
* try {
|
|
31
|
+
* const result = await callApi();
|
|
32
|
+
* cb.onSuccess();
|
|
33
|
+
* } catch (err) {
|
|
34
|
+
* cb.onFailure(err.message);
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
export class CircuitBreaker {
|
|
39
|
+
providerId;
|
|
40
|
+
options;
|
|
41
|
+
state = "closed";
|
|
42
|
+
failureCount = 0;
|
|
43
|
+
successCount = 0;
|
|
44
|
+
lastFailureTime = 0;
|
|
45
|
+
lastSuccessTime = 0;
|
|
46
|
+
lastError;
|
|
47
|
+
/** Resolved threshold: consecutive failures before opening. */
|
|
48
|
+
failureThreshold;
|
|
49
|
+
/** Resolved open-state duration before allowing a probe. */
|
|
50
|
+
resetTimeoutMs;
|
|
51
|
+
/** Resolved success count in half-open needed to close. */
|
|
52
|
+
halfOpenSuccessThreshold;
|
|
53
|
+
constructor(providerId, options = {}) {
|
|
54
|
+
this.providerId = providerId;
|
|
55
|
+
this.options = options;
|
|
56
|
+
this.failureThreshold = options.failureThreshold ?? 3;
|
|
57
|
+
this.resetTimeoutMs = options.resetTimeoutMs ?? 60_000;
|
|
58
|
+
this.halfOpenSuccessThreshold = options.halfOpenSuccessThreshold ?? 1;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Check whether a request should be allowed through.
|
|
62
|
+
*
|
|
63
|
+
* - `closed` → always true.
|
|
64
|
+
* - `open` → false, unless the reset timeout has elapsed, in which case
|
|
65
|
+
* the circuit transitions to `half-open` and returns true for the probe.
|
|
66
|
+
* - `half-open` → true (the probe request is already in flight).
|
|
67
|
+
*/
|
|
68
|
+
canExecute() {
|
|
69
|
+
if (this.state === "closed") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (this.state === "open") {
|
|
73
|
+
const elapsed = Date.now() - this.lastFailureTime;
|
|
74
|
+
if (elapsed >= this.resetTimeoutMs) {
|
|
75
|
+
// Transition to half-open: allow a single probe
|
|
76
|
+
this.state = "half-open";
|
|
77
|
+
this.successCount = 0;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
// half-open: allow the probe through
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Record a successful API call.
|
|
87
|
+
*
|
|
88
|
+
* In `half-open` state, once enough successes accumulate (per
|
|
89
|
+
* {@link CircuitBreakerOptions.halfOpenSuccessThreshold}) the circuit closes.
|
|
90
|
+
* In `closed` state the failure counter is reset.
|
|
91
|
+
*/
|
|
92
|
+
onSuccess() {
|
|
93
|
+
this.lastSuccessTime = Date.now();
|
|
94
|
+
if (this.state === "half-open") {
|
|
95
|
+
this.successCount++;
|
|
96
|
+
if (this.successCount >= this.halfOpenSuccessThreshold) {
|
|
97
|
+
this.transitionToClosed();
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// In closed state reset the failure counter on any success
|
|
102
|
+
if (this.state === "closed") {
|
|
103
|
+
this.failureCount = 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Record a failed API call.
|
|
108
|
+
*
|
|
109
|
+
* In `closed` state, increments the failure counter and opens the circuit
|
|
110
|
+
* when the threshold is reached. In `half-open` state, immediately
|
|
111
|
+
* re-opens the circuit.
|
|
112
|
+
*
|
|
113
|
+
* @param error - Optional error message to store for diagnostics.
|
|
114
|
+
*/
|
|
115
|
+
onFailure(error) {
|
|
116
|
+
this.lastFailureTime = Date.now();
|
|
117
|
+
this.lastError = error;
|
|
118
|
+
if (this.state === "half-open") {
|
|
119
|
+
// Probe failed — reopen immediately
|
|
120
|
+
this.transitionToOpen();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (this.state === "closed") {
|
|
124
|
+
this.failureCount++;
|
|
125
|
+
if (this.failureCount >= this.failureThreshold) {
|
|
126
|
+
this.transitionToOpen();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Return a point-in-time health snapshot for this provider's circuit.
|
|
132
|
+
*/
|
|
133
|
+
health() {
|
|
134
|
+
return {
|
|
135
|
+
providerId: this.providerId,
|
|
136
|
+
state: this.state,
|
|
137
|
+
failureCount: this.failureCount,
|
|
138
|
+
lastFailureTime: this.lastFailureTime,
|
|
139
|
+
lastError: this.lastError,
|
|
140
|
+
lastSuccessTime: this.lastSuccessTime,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Force the circuit back to `closed` state, resetting all counters.
|
|
145
|
+
* Useful for manual recovery or test teardown.
|
|
146
|
+
*/
|
|
147
|
+
reset() {
|
|
148
|
+
this.transitionToClosed();
|
|
149
|
+
this.lastFailureTime = 0;
|
|
150
|
+
this.lastSuccessTime = 0;
|
|
151
|
+
this.lastError = undefined;
|
|
152
|
+
}
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Private helpers
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
transitionToOpen() {
|
|
157
|
+
this.state = "open";
|
|
158
|
+
this.successCount = 0;
|
|
159
|
+
}
|
|
160
|
+
transitionToClosed() {
|
|
161
|
+
this.state = "closed";
|
|
162
|
+
this.failureCount = 0;
|
|
163
|
+
this.successCount = 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// HealthTracker
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
/**
|
|
170
|
+
* Manages {@link CircuitBreaker} instances for all tracked providers.
|
|
171
|
+
*
|
|
172
|
+
* Breakers are created lazily on first access via {@link breaker}.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const tracker = new HealthTracker();
|
|
176
|
+
* const cb = tracker.breaker("anthropic");
|
|
177
|
+
* if (cb.canExecute()) { ... }
|
|
178
|
+
*/
|
|
179
|
+
export class HealthTracker {
|
|
180
|
+
breakers = new Map();
|
|
181
|
+
/**
|
|
182
|
+
* Retrieve the {@link CircuitBreaker} for the given provider, creating
|
|
183
|
+
* one with default options if it does not yet exist.
|
|
184
|
+
*
|
|
185
|
+
* @param providerId - Provider slug (e.g. `"anthropic"`).
|
|
186
|
+
* @param options - Options forwarded to a newly created breaker only.
|
|
187
|
+
*/
|
|
188
|
+
breaker(providerId, options) {
|
|
189
|
+
let cb = this.breakers.get(providerId);
|
|
190
|
+
if (!cb) {
|
|
191
|
+
cb = new CircuitBreaker(providerId, options);
|
|
192
|
+
this.breakers.set(providerId, cb);
|
|
193
|
+
}
|
|
194
|
+
return cb;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Return health snapshots for every tracked provider, sorted by provider ID.
|
|
198
|
+
*/
|
|
199
|
+
healthReport() {
|
|
200
|
+
return Array.from(this.breakers.values())
|
|
201
|
+
.map((cb) => cb.health())
|
|
202
|
+
.sort((a, b) => a.providerId.localeCompare(b.providerId));
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Return provider IDs whose circuit is `closed` or `half-open`
|
|
206
|
+
* (i.e. requests are currently being allowed through).
|
|
207
|
+
*/
|
|
208
|
+
availableProviders() {
|
|
209
|
+
return Array.from(this.breakers.values())
|
|
210
|
+
.filter((cb) => cb.canExecute())
|
|
211
|
+
.map((cb) => cb.providerId)
|
|
212
|
+
.sort();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Return provider IDs whose circuit is `open`
|
|
216
|
+
* (i.e. requests are currently being rejected).
|
|
217
|
+
*/
|
|
218
|
+
downProviders() {
|
|
219
|
+
return Array.from(this.breakers.values())
|
|
220
|
+
.filter((cb) => !cb.canExecute())
|
|
221
|
+
.map((cb) => cb.providerId)
|
|
222
|
+
.sort();
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Reset all tracked circuit breakers to `closed` state.
|
|
226
|
+
*/
|
|
227
|
+
resetAll() {
|
|
228
|
+
for (const cb of this.breakers.values()) {
|
|
229
|
+
cb.reset();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// StaleCachePolicy
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
/**
|
|
237
|
+
* Utility that wraps {@link KoshaCache} reads to implement
|
|
238
|
+
* stale-while-revalidate semantics.
|
|
239
|
+
*
|
|
240
|
+
* Unlike the registry's normal cache path (which returns `null` for expired
|
|
241
|
+
* entries), this policy always returns whatever was cached along with a
|
|
242
|
+
* `stale` flag. This lets callers serve the old data immediately while
|
|
243
|
+
* triggering a background refresh.
|
|
244
|
+
*
|
|
245
|
+
* Returns `null` only when the cache has never held a value for the key.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* const result = await StaleCachePolicy.getWithStale<ModelCard[]>(cache, "provider_anthropic");
|
|
249
|
+
* if (result) {
|
|
250
|
+
* serveToClient(result.data); // always fast
|
|
251
|
+
* if (result.stale) triggerBackgroundRefresh();
|
|
252
|
+
* }
|
|
253
|
+
*/
|
|
254
|
+
export class StaleCachePolicy {
|
|
255
|
+
/**
|
|
256
|
+
* Fetch a cached value regardless of its expiry, annotating the result
|
|
257
|
+
* with staleness metadata.
|
|
258
|
+
*
|
|
259
|
+
* @param cache - The {@link KoshaCache} instance to read from.
|
|
260
|
+
* @param key - Cache key to look up.
|
|
261
|
+
* @returns A {@link StaleResult} when any cached value exists, or `null`
|
|
262
|
+
* when the key has never been written.
|
|
263
|
+
*/
|
|
264
|
+
static async getWithStale(cache, key) {
|
|
265
|
+
// Use the raw get() which returns the entry regardless of TTL.
|
|
266
|
+
// TTL checking is done separately so we can set the stale flag.
|
|
267
|
+
const entry = await cache.get(key);
|
|
268
|
+
if (entry === null) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
const age = now - entry.timestamp;
|
|
273
|
+
// We need to determine staleness. KoshaCache.isExpired requires a TTL,
|
|
274
|
+
// but StaleCachePolicy is TTL-agnostic — we expose the raw age and let
|
|
275
|
+
// the caller decide. We set stale=true when age > 0 to signal that the
|
|
276
|
+
// entry is not brand-new; callers that want TTL-based staleness should
|
|
277
|
+
// compare age against their own threshold.
|
|
278
|
+
//
|
|
279
|
+
// A more useful signal: mark stale when the entry is older than the
|
|
280
|
+
// default 24 h cache TTL used by the registry. Callers with custom TTLs
|
|
281
|
+
// can compare age directly.
|
|
282
|
+
const DEFAULT_TTL_MS = 86_400_000;
|
|
283
|
+
const stale = age > DEFAULT_TTL_MS;
|
|
284
|
+
return {
|
|
285
|
+
data: entry.data,
|
|
286
|
+
stale,
|
|
287
|
+
age,
|
|
288
|
+
cachedAt: entry.timestamp,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=resilience.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resilience.js","sourceRoot":"","sources":["../src/resilience.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgEH,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,cAAc;IAeL;IAA4B;IAdzC,KAAK,GAAiB,QAAQ,CAAC;IAC/B,YAAY,GAAG,CAAC,CAAC;IACjB,YAAY,GAAG,CAAC,CAAC;IACjB,eAAe,GAAG,CAAC,CAAC;IACpB,eAAe,GAAG,CAAC,CAAC;IACpB,SAAS,CAAU;IAE3B,+DAA+D;IAC9C,gBAAgB,CAAS;IAC1C,4DAA4D;IAC3C,cAAc,CAAS;IACxC,2DAA2D;IAC1C,wBAAwB,CAAS;IAElD,YAAqB,UAAkB,EAAU,UAAiC,EAAE;QAA/D,eAAU,GAAV,UAAU,CAAQ;QAAU,YAAO,GAAP,OAAO,CAA4B;QACnF,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,CAAC,CAAC;QACtD,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,MAAM,CAAC;QACvD,IAAI,CAAC,wBAAwB,GAAG,OAAO,CAAC,wBAAwB,IAAI,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;;OAOG;IACH,UAAU;QACT,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;YAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe,CAAC;YAClD,IAAI,OAAO,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;gBACpC,gDAAgD;gBAChD,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC;gBACzB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;gBACtB,OAAO,IAAI,CAAC;YACb,CAAC;YACD,OAAO,KAAK,CAAC;QACd,CAAC;QAED,qCAAqC;QACrC,OAAO,IAAI,CAAC;IACb,CAAC;IAED;;;;;;OAMG;IACH,SAAS;QACR,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAElC,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAChC,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,wBAAwB,EAAE,CAAC;gBACxD,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC3B,CAAC;YACD,OAAO;QACR,CAAC;QAED,2DAA2D;QAC3D,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC7B,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACvB,CAAC;IACF,CAAC;IAED;;;;;;;;OAQG;IACH,SAAS,CAAC,KAAc;QACvB,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAClC,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QAEvB,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAChC,oCAAoC;YACpC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACxB,OAAO;QACR,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC7B,IAAI,CAAC,YAAY,EAAE,CAAC;YACpB,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBAChD,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACzB,CAAC;QACF,CAAC;IACF,CAAC;IAED;;OAEG;IACH,MAAM;QACL,OAAO;YACN,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,eAAe,EAAE,IAAI,CAAC,eAAe;SACrC,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK;QACJ,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC1B,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;IAC5B,CAAC;IAED,8EAA8E;IAC9E,kBAAkB;IAClB,8EAA8E;IAEtE,gBAAgB;QACvB,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC;QACpB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;IACvB,CAAC;IAEO,kBAAkB;QACzB,IAAI,CAAC,KAAK,GAAG,QAAQ,CAAC;QACtB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;QACtB,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC;IACvB,CAAC;CACD;AAED,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E;;;;;;;;;GASG;AACH,MAAM,OAAO,aAAa;IACjB,QAAQ,GAAG,IAAI,GAAG,EAA0B,CAAC;IAErD;;;;;;OAMG;IACH,OAAO,CAAC,UAAkB,EAAE,OAA+B;QAC1D,IAAI,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACvC,IAAI,CAAC,EAAE,EAAE,CAAC;YACT,EAAE,GAAG,IAAI,cAAc,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC7C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;QACnC,CAAC;QACD,OAAO,EAAE,CAAC;IACX,CAAC;IAED;;OAEG;IACH,YAAY;QACX,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;aACvC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC;aACxB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED;;;OAGG;IACH,kBAAkB;QACjB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;aACvC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC;aAC/B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC;aAC1B,IAAI,EAAE,CAAC;IACV,CAAC;IAED;;;OAGG;IACH,aAAa;QACZ,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC;aACvC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC;aAChC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,CAAC;aAC1B,IAAI,EAAE,CAAC;IACV,CAAC;IAED;;OAEG;IACH,QAAQ;QACP,KAAK,MAAM,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YACzC,EAAE,CAAC,KAAK,EAAE,CAAC;QACZ,CAAC;IACF,CAAC;CACD;AAED,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,gBAAgB;IAC5B;;;;;;;;OAQG;IACH,MAAM,CAAC,KAAK,CAAC,YAAY,CAAI,KAAiB,EAAE,GAAW;QAC1D,+DAA+D;QAC/D,gEAAgE;QAChE,MAAM,KAAK,GAAG,MAAM,KAAK,CAAC,GAAG,CAAI,GAAG,CAAC,CAAC;QACtC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACpB,OAAO,IAAI,CAAC;QACb,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,GAAG,GAAG,GAAG,GAAG,KAAK,CAAC,SAAS,CAAC;QAElC,uEAAuE;QACvE,uEAAuE;QACvE,uEAAuE;QACvE,uEAAuE;QACvE,2CAA2C;QAC3C,EAAE;QACF,oEAAoE;QACpE,wEAAwE;QACxE,4BAA4B;QAC5B,MAAM,cAAc,GAAG,UAAU,CAAC;QAClC,MAAM,KAAK,GAAG,GAAG,GAAG,cAAc,CAAC;QAEnC,OAAO;YACN,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,KAAK;YACL,GAAG;YACH,QAAQ,EAAE,KAAK,CAAC,SAAS;SACzB,CAAC;IACH,CAAC;CACD"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kosha-discovery — Security guardrails for external data.
|
|
3
|
+
*
|
|
4
|
+
* Centralised defences against supply-chain data injection.
|
|
5
|
+
* Every external data ingestion point (HTTP responses, CLI output,
|
|
6
|
+
* cache reads) MUST pass through {@link assertCleanPayload} before
|
|
7
|
+
* the parsed payload is used.
|
|
8
|
+
*
|
|
9
|
+
* Threat catalogue:
|
|
10
|
+
* - **base64** Encoded credential exfiltration (LiteLLM incident)
|
|
11
|
+
* - **credential_leak** Leaked API keys / tokens (OpenAI, AWS, GitHub, Slack, Google, etc.)
|
|
12
|
+
* - **script_injection** XSS / HTML injection via `<script>`, `javascript:`, event handlers
|
|
13
|
+
* - **shell_injection** Command injection via `$(…)`, backtick blocks, pipe/chain to shells
|
|
14
|
+
* - **data_uri** Executable data URIs (`data:text/html`, `data:application/…`)
|
|
15
|
+
* - **null_byte** Null-byte injection to bypass string validators
|
|
16
|
+
* - **proto_pollution** Prototype pollution via `__proto__` keys
|
|
17
|
+
* - **hex_payload** Long hex-encoded binary blobs (potential obfuscated payloads)
|
|
18
|
+
* - **oversized_string** Unreasonably long strings for model metadata (>2048 chars)
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
export interface ThreatHit {
|
|
23
|
+
threat: string;
|
|
24
|
+
path: string;
|
|
25
|
+
value: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Deep-scan a parsed JSON payload for security threats in keys and string
|
|
29
|
+
* values at any nesting depth.
|
|
30
|
+
*
|
|
31
|
+
* @returns The first {@link ThreatHit} found, or `undefined` if clean.
|
|
32
|
+
*/
|
|
33
|
+
export declare function scanPayload(obj: unknown, path?: string): ThreatHit | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Throw if the given payload contains **any** security threat — base64,
|
|
36
|
+
* leaked credentials, script injection, shell injection, data URIs,
|
|
37
|
+
* null bytes, prototype pollution keys, hex blobs, or oversized strings.
|
|
38
|
+
*
|
|
39
|
+
* @param data - Parsed JSON payload to scan.
|
|
40
|
+
* @param source - Human-readable label for error messages (e.g. "litellm", "OpenAI API").
|
|
41
|
+
* @throws {Error} with the threat name and offending path.
|
|
42
|
+
*/
|
|
43
|
+
export declare function assertCleanPayload(data: unknown, source: string): void;
|
|
44
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAiIH,MAAM,WAAW,SAAS;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACd;AAMD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,SAAK,GAAG,SAAS,GAAG,SAAS,CA8B1E;AAMD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAOtE"}
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kosha-discovery — Security guardrails for external data.
|
|
3
|
+
*
|
|
4
|
+
* Centralised defences against supply-chain data injection.
|
|
5
|
+
* Every external data ingestion point (HTTP responses, CLI output,
|
|
6
|
+
* cache reads) MUST pass through {@link assertCleanPayload} before
|
|
7
|
+
* the parsed payload is used.
|
|
8
|
+
*
|
|
9
|
+
* Threat catalogue:
|
|
10
|
+
* - **base64** Encoded credential exfiltration (LiteLLM incident)
|
|
11
|
+
* - **credential_leak** Leaked API keys / tokens (OpenAI, AWS, GitHub, Slack, Google, etc.)
|
|
12
|
+
* - **script_injection** XSS / HTML injection via `<script>`, `javascript:`, event handlers
|
|
13
|
+
* - **shell_injection** Command injection via `$(…)`, backtick blocks, pipe/chain to shells
|
|
14
|
+
* - **data_uri** Executable data URIs (`data:text/html`, `data:application/…`)
|
|
15
|
+
* - **null_byte** Null-byte injection to bypass string validators
|
|
16
|
+
* - **proto_pollution** Prototype pollution via `__proto__` keys
|
|
17
|
+
* - **hex_payload** Long hex-encoded binary blobs (potential obfuscated payloads)
|
|
18
|
+
* - **oversized_string** Unreasonably long strings for model metadata (>2048 chars)
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
/** 32+ chars of pure base64 alphabet with optional `=` padding. */
|
|
23
|
+
const BASE64_PATTERN = /^[A-Za-z0-9+/]{32,}={0,2}$/;
|
|
24
|
+
/** Known credential prefixes — each must be followed by enough chars to be a real key. */
|
|
25
|
+
const CREDENTIAL_PATTERNS = [
|
|
26
|
+
/\bsk-[A-Za-z0-9]{20,}/, // OpenAI API keys
|
|
27
|
+
/\bsk-proj-[A-Za-z0-9]{20,}/, // OpenAI project keys
|
|
28
|
+
/\bAKIA[A-Z0-9]{16}/, // AWS access key IDs
|
|
29
|
+
/\bghp_[A-Za-z0-9]{36,}/, // GitHub personal access tokens
|
|
30
|
+
/\bgho_[A-Za-z0-9]{36,}/, // GitHub OAuth tokens
|
|
31
|
+
/\bghs_[A-Za-z0-9]{36,}/, // GitHub app installation tokens
|
|
32
|
+
/\bxoxb-[A-Za-z0-9\-]{24,}/, // Slack bot tokens
|
|
33
|
+
/\bxoxp-[A-Za-z0-9\-]{24,}/, // Slack user tokens
|
|
34
|
+
/\bAIza[A-Za-z0-9_\-]{30,}/, // Google API keys
|
|
35
|
+
/\bya29\.[A-Za-z0-9_\-]{50,}/, // Google OAuth access tokens
|
|
36
|
+
/\bglpat-[A-Za-z0-9\-]{20,}/, // GitLab personal access tokens
|
|
37
|
+
/\bnpm_[A-Za-z0-9]{36,}/, // npm tokens
|
|
38
|
+
/\bpypi-[A-Za-z0-9]{50,}/, // PyPI tokens
|
|
39
|
+
/\bhf_[A-Za-z0-9]{30,}/, // Hugging Face tokens
|
|
40
|
+
/\bBearer\s+[A-Za-z0-9._\-]{40,}/, // Generic bearer tokens in data
|
|
41
|
+
];
|
|
42
|
+
/** Script / HTML injection patterns (case-insensitive). */
|
|
43
|
+
const SCRIPT_PATTERN = /<script[\s>]/i;
|
|
44
|
+
const EVENT_HANDLER_PATTERN = /\bon\w+\s*=\s*["'`]/i;
|
|
45
|
+
const JAVASCRIPT_URI_PATTERN = /javascript\s*:/i;
|
|
46
|
+
/** Shell injection — command substitution and chaining to exfiltration tools. */
|
|
47
|
+
const SHELL_INJECTION_PATTERNS = [
|
|
48
|
+
/\$\([^)]+\)/, // $(command)
|
|
49
|
+
/`[^`]{2,}`/, // `command` (backtick execution, 2+ chars to avoid markdown)
|
|
50
|
+
/[;|&]\s*(curl|wget|nc|bash|sh|python|node|ruby|perl|php)\b/i, // chain to dangerous commands
|
|
51
|
+
];
|
|
52
|
+
/** Data URIs that can carry executable content. */
|
|
53
|
+
const DATA_URI_PATTERN = /^data:(text\/html|application\/)/i;
|
|
54
|
+
/** Null byte in any form. */
|
|
55
|
+
const NULL_BYTE_PATTERN = /\x00|\\x00|\\u0000|%00/;
|
|
56
|
+
/** Long hex-only blobs (64+ hex chars) — potential obfuscated payloads. */
|
|
57
|
+
const HEX_BLOB_PATTERN = /^(0x)?[0-9a-fA-F]{64,}$/;
|
|
58
|
+
/** Prototype pollution key — `__proto__` is the only key that directly
|
|
59
|
+
* triggers pollution on `JSON.parse` output when merged naively.
|
|
60
|
+
* `constructor` and `prototype` are common legitimate JSON keys and
|
|
61
|
+
* are intentionally not flagged to avoid false positives. */
|
|
62
|
+
const PROTO_POLLUTION_KEY = "__proto__";
|
|
63
|
+
/** Maximum reasonable string length for model metadata values. */
|
|
64
|
+
const MAX_STRING_LENGTH = 2048;
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Threat registry — evaluated once per string value during scan.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Ordered most-specific first → broadest last. This ensures that a leaked
|
|
69
|
+
// API key (which is also valid base64) reports "credential_leak" rather than
|
|
70
|
+
// the generic "base64" catch-all.
|
|
71
|
+
const VALUE_THREATS = [
|
|
72
|
+
{
|
|
73
|
+
name: "null_byte",
|
|
74
|
+
test: (v) => NULL_BYTE_PATTERN.test(v),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "credential_leak",
|
|
78
|
+
test: (v) => CREDENTIAL_PATTERNS.some((p) => p.test(v)),
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "script_injection",
|
|
82
|
+
test: (v) => SCRIPT_PATTERN.test(v) || EVENT_HANDLER_PATTERN.test(v) || JAVASCRIPT_URI_PATTERN.test(v),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: "shell_injection",
|
|
86
|
+
test: (v) => SHELL_INJECTION_PATTERNS.some((p) => p.test(v)),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "data_uri",
|
|
90
|
+
test: (v) => DATA_URI_PATTERN.test(v),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "oversized_string",
|
|
94
|
+
test: (v) => v.length > MAX_STRING_LENGTH,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "hex_payload",
|
|
98
|
+
test: (v) => HEX_BLOB_PATTERN.test(v),
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "base64",
|
|
102
|
+
test: (v) => BASE64_PATTERN.test(v),
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
/** Threats checked against object keys specifically. */
|
|
106
|
+
const KEY_THREATS = [
|
|
107
|
+
{
|
|
108
|
+
name: "null_byte",
|
|
109
|
+
test: (v) => NULL_BYTE_PATTERN.test(v),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "proto_pollution",
|
|
113
|
+
test: (v) => v === PROTO_POLLUTION_KEY,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "credential_leak",
|
|
117
|
+
test: (v) => CREDENTIAL_PATTERNS.some((p) => p.test(v)),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "base64",
|
|
121
|
+
test: (v) => BASE64_PATTERN.test(v),
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
// Deep scanner
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
/**
|
|
128
|
+
* Deep-scan a parsed JSON payload for security threats in keys and string
|
|
129
|
+
* values at any nesting depth.
|
|
130
|
+
*
|
|
131
|
+
* @returns The first {@link ThreatHit} found, or `undefined` if clean.
|
|
132
|
+
*/
|
|
133
|
+
export function scanPayload(obj, path = "") {
|
|
134
|
+
if (typeof obj === "string") {
|
|
135
|
+
for (const t of VALUE_THREATS) {
|
|
136
|
+
if (t.test(obj)) {
|
|
137
|
+
return { threat: t.name, path, value: obj.length > 80 ? `${obj.slice(0, 80)}…` : obj };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(obj)) {
|
|
143
|
+
for (let i = 0; i < obj.length; i++) {
|
|
144
|
+
const hit = scanPayload(obj[i], `${path}[${i}]`);
|
|
145
|
+
if (hit)
|
|
146
|
+
return hit;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
if (obj !== null && typeof obj === "object") {
|
|
151
|
+
for (const key of Object.keys(obj)) {
|
|
152
|
+
// Check the key itself
|
|
153
|
+
for (const t of KEY_THREATS) {
|
|
154
|
+
if (t.test(key)) {
|
|
155
|
+
return { threat: t.name, path: `${path}.${key}`, value: key };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Recurse into the value
|
|
159
|
+
const hit = scanPayload(obj[key], `${path}.${key}`);
|
|
160
|
+
if (hit)
|
|
161
|
+
return hit;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Public assertion API
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
/**
|
|
170
|
+
* Throw if the given payload contains **any** security threat — base64,
|
|
171
|
+
* leaked credentials, script injection, shell injection, data URIs,
|
|
172
|
+
* null bytes, prototype pollution keys, hex blobs, or oversized strings.
|
|
173
|
+
*
|
|
174
|
+
* @param data - Parsed JSON payload to scan.
|
|
175
|
+
* @param source - Human-readable label for error messages (e.g. "litellm", "OpenAI API").
|
|
176
|
+
* @throws {Error} with the threat name and offending path.
|
|
177
|
+
*/
|
|
178
|
+
export function assertCleanPayload(data, source) {
|
|
179
|
+
const hit = scanPayload(data);
|
|
180
|
+
if (hit) {
|
|
181
|
+
throw new Error(`Rejected ${source} data: ${hit.threat} detected at "${hit.path}" — refusing to load potentially compromised payload`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kosha-discovery — HTTP API server (Hono-based).
|
|
3
|
+
*
|
|
4
|
+
* Exposes the model registry over a lightweight REST API so that
|
|
5
|
+
* editor extensions, scripts, and other tools can query discovered
|
|
6
|
+
* models without importing the library directly.
|
|
7
|
+
*
|
|
8
|
+
* Routes:
|
|
9
|
+
* GET /api/models — List models (query: ?provider, ?originProvider, ?mode, ?capability)
|
|
10
|
+
* GET /api/models/cheapest — Cheapest eligible models for a role/capability
|
|
11
|
+
* GET /api/models/:idOrAlias/routes — All provider routes with preferred/direct metadata
|
|
12
|
+
* GET /api/models/:idOrAlias — Get a single model by ID or alias (+ baseUrl/version)
|
|
13
|
+
* GET /api/roles — Provider->model->roles matrix
|
|
14
|
+
* GET /api/providers — List all providers (summary)
|
|
15
|
+
* GET /api/providers/:id — Get a single provider with its models
|
|
16
|
+
* POST /api/refresh — Trigger re-discovery (body: { provider?: string })
|
|
17
|
+
* GET /api/resolve/:alias — Resolve an alias to its canonical model ID
|
|
18
|
+
* GET /api/discovery-errors — Errors from last discovery pass
|
|
19
|
+
* GET /health — Health check
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { Hono } from "hono";
|
|
23
|
+
import { ModelRegistry } from "./registry.js";
|
|
24
|
+
/**
|
|
25
|
+
* Create a Hono application wired to the given {@link ModelRegistry}.
|
|
26
|
+
*
|
|
27
|
+
* The returned app is not yet listening — call `serve()` or mount it
|
|
28
|
+
* inside another Hono app to start accepting requests.
|
|
29
|
+
*
|
|
30
|
+
* @param registry - A pre-populated (or lazy) ModelRegistry instance.
|
|
31
|
+
* @returns A configured Hono app with all kosha REST routes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createServer(registry: ModelRegistry): Hono;
|
|
34
|
+
/**
|
|
35
|
+
* Boot a standalone kosha API server.
|
|
36
|
+
*
|
|
37
|
+
* Runs full discovery, then starts an HTTP listener on the given port.
|
|
38
|
+
* @param port - TCP port to bind (default `3000`, overridable via `PORT` env var).
|
|
39
|
+
*/
|
|
40
|
+
export declare function startServer(port?: number): Promise<void>;
|
|
41
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAI5B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAiC9C;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAwP1D;AAMD;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,IAAI,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB5D"}
|