offrouter-core 0.2.0 → 0.2.2

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.
Files changed (139) hide show
  1. package/dist/audit.d.ts +137 -0
  2. package/dist/audit.d.ts.map +1 -0
  3. package/dist/audit.js +320 -0
  4. package/dist/audit.js.map +1 -0
  5. package/dist/auth/credential-chain.d.ts +84 -0
  6. package/dist/auth/credential-chain.d.ts.map +1 -0
  7. package/dist/auth/credential-chain.js +150 -0
  8. package/dist/auth/credential-chain.js.map +1 -0
  9. package/dist/auth/index.d.ts +10 -0
  10. package/dist/auth/index.d.ts.map +1 -0
  11. package/dist/auth/index.js +7 -0
  12. package/dist/auth/index.js.map +1 -0
  13. package/dist/auth/keychain.d.ts +60 -0
  14. package/dist/auth/keychain.d.ts.map +1 -0
  15. package/dist/auth/keychain.js +104 -0
  16. package/dist/auth/keychain.js.map +1 -0
  17. package/dist/auth/oauth-pkce.d.ts +138 -0
  18. package/dist/auth/oauth-pkce.d.ts.map +1 -0
  19. package/dist/auth/oauth-pkce.js +375 -0
  20. package/dist/auth/oauth-pkce.js.map +1 -0
  21. package/dist/auth/oauth-server.d.ts +36 -0
  22. package/dist/auth/oauth-server.d.ts.map +1 -0
  23. package/dist/auth/oauth-server.js +210 -0
  24. package/dist/auth/oauth-server.js.map +1 -0
  25. package/dist/config.d.ts +76 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +355 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/delegation.d.ts +123 -0
  30. package/dist/delegation.d.ts.map +1 -0
  31. package/dist/delegation.js +455 -0
  32. package/dist/delegation.js.map +1 -0
  33. package/dist/index.d.ts +41 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +26 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/policy.d.ts +45 -0
  38. package/dist/policy.d.ts.map +1 -0
  39. package/dist/policy.js +318 -0
  40. package/dist/policy.js.map +1 -0
  41. package/{src/protocol.ts → dist/protocol.d.ts} +2 -2
  42. package/dist/protocol.d.ts.map +1 -0
  43. package/dist/protocol.js +6 -0
  44. package/dist/protocol.js.map +1 -0
  45. package/dist/providers/adapter.d.ts +137 -0
  46. package/dist/providers/adapter.d.ts.map +1 -0
  47. package/dist/providers/adapter.js +163 -0
  48. package/dist/providers/adapter.js.map +1 -0
  49. package/dist/providers/catalog-data.d.ts +128 -0
  50. package/dist/providers/catalog-data.d.ts.map +1 -0
  51. package/dist/providers/catalog-data.js +397 -0
  52. package/dist/providers/catalog-data.js.map +1 -0
  53. package/dist/providers/catalog.d.ts +63 -0
  54. package/dist/providers/catalog.d.ts.map +1 -0
  55. package/dist/providers/catalog.js +394 -0
  56. package/dist/providers/catalog.js.map +1 -0
  57. package/dist/providers/fake.d.ts +46 -0
  58. package/dist/providers/fake.d.ts.map +1 -0
  59. package/dist/providers/fake.js +234 -0
  60. package/dist/providers/fake.js.map +1 -0
  61. package/dist/providers/openai-compatible.d.ts +65 -0
  62. package/dist/providers/openai-compatible.d.ts.map +1 -0
  63. package/dist/providers/openai-compatible.js +434 -0
  64. package/dist/providers/openai-compatible.js.map +1 -0
  65. package/dist/proxy/responses-mapper.d.ts +366 -0
  66. package/dist/proxy/responses-mapper.d.ts.map +1 -0
  67. package/dist/proxy/responses-mapper.js +517 -0
  68. package/dist/proxy/responses-mapper.js.map +1 -0
  69. package/dist/proxy/responses-server.d.ts +29 -0
  70. package/dist/proxy/responses-server.d.ts.map +1 -0
  71. package/dist/proxy/responses-server.js +360 -0
  72. package/dist/proxy/responses-server.js.map +1 -0
  73. package/dist/router.d.ts +18 -0
  74. package/dist/router.d.ts.map +1 -0
  75. package/dist/router.js +296 -0
  76. package/dist/router.js.map +1 -0
  77. package/dist/schemas.d.ts +560 -0
  78. package/dist/schemas.d.ts.map +1 -0
  79. package/{src/schemas.ts → dist/schemas.js} +83 -103
  80. package/dist/schemas.js.map +1 -0
  81. package/dist/secrets.d.ts +112 -0
  82. package/dist/secrets.d.ts.map +1 -0
  83. package/dist/secrets.js +326 -0
  84. package/dist/secrets.js.map +1 -0
  85. package/dist/types.d.ts +117 -0
  86. package/dist/types.d.ts.map +1 -0
  87. package/dist/types.js +6 -0
  88. package/dist/types.js.map +1 -0
  89. package/dist/usage-file.d.ts +59 -0
  90. package/dist/usage-file.d.ts.map +1 -0
  91. package/dist/usage-file.js +316 -0
  92. package/dist/usage-file.js.map +1 -0
  93. package/dist/usage.d.ts +235 -0
  94. package/dist/usage.d.ts.map +1 -0
  95. package/dist/usage.js +517 -0
  96. package/dist/usage.js.map +1 -0
  97. package/package.json +9 -4
  98. package/src/audit.test.ts +0 -302
  99. package/src/audit.ts +0 -553
  100. package/src/auth/credential-chain.test.ts +0 -326
  101. package/src/auth/credential-chain.ts +0 -235
  102. package/src/auth/index.ts +0 -45
  103. package/src/auth/keychain.test.ts +0 -265
  104. package/src/auth/keychain.ts +0 -168
  105. package/src/auth/oauth-pkce.test.ts +0 -329
  106. package/src/auth/oauth-pkce.ts +0 -571
  107. package/src/auth/oauth-server.test.ts +0 -83
  108. package/src/auth/oauth-server.ts +0 -296
  109. package/src/config.test.ts +0 -479
  110. package/src/config.ts +0 -505
  111. package/src/delegation.test.ts +0 -368
  112. package/src/delegation.ts +0 -605
  113. package/src/index.ts +0 -280
  114. package/src/policy.test.ts +0 -634
  115. package/src/policy.ts +0 -420
  116. package/src/providers/adapter.test.ts +0 -293
  117. package/src/providers/adapter.ts +0 -328
  118. package/src/providers/catalog-data.test.ts +0 -258
  119. package/src/providers/catalog-data.ts +0 -498
  120. package/src/providers/catalog.test.ts +0 -84
  121. package/src/providers/catalog.ts +0 -483
  122. package/src/providers/fake.ts +0 -312
  123. package/src/providers/openai-compatible.test.ts +0 -366
  124. package/src/providers/openai-compatible.ts +0 -554
  125. package/src/proxy/responses-mapper.test.ts +0 -290
  126. package/src/proxy/responses-mapper.ts +0 -736
  127. package/src/proxy/responses-server.test.ts +0 -322
  128. package/src/proxy/responses-server.ts +0 -469
  129. package/src/router.test.ts +0 -699
  130. package/src/router.ts +0 -352
  131. package/src/schemas.test.ts +0 -291
  132. package/src/secrets.test.ts +0 -271
  133. package/src/secrets.ts +0 -461
  134. package/src/types.ts +0 -173
  135. package/src/usage-file.test.ts +0 -243
  136. package/src/usage-file.ts +0 -435
  137. package/src/usage.test.ts +0 -335
  138. package/src/usage.ts +0 -859
  139. package/tsconfig.json +0 -9
package/src/audit.test.ts DELETED
@@ -1,302 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { ROUTE_PROTOCOL_VERSION } from "./protocol.js";
3
- import { InMemoryAuditStore, type AuditRecord } from "./audit.js";
4
- import type { RouteDecision, RouteRequest } from "./types.js";
5
-
6
- const RAW_PROMPT =
7
- "super secret user intent with sk-ant-api03-abcdefghijklmnopqrstuv embedded";
8
- const PROMPT_DIGEST = "sha256:deadbeefcafebabe";
9
-
10
- function baseRequest(overrides: Partial<RouteRequest> = {}): RouteRequest {
11
- return {
12
- protocolVersion: ROUTE_PROTOCOL_VERSION,
13
- requestId: "req_1",
14
- harness: { kind: "claude", profile: "claude-personal" },
15
- task: {
16
- promptPreview: RAW_PROMPT,
17
- promptDigest: PROMPT_DIGEST,
18
- kind: "explain",
19
- risk: "low",
20
- },
21
- workspace: { cwd: "/tmp/project", trusted: true },
22
- constraints: { subscriptionFirst: true, allowApiKeyFallback: true },
23
- ...overrides,
24
- };
25
- }
26
-
27
- function baseDecision(
28
- overrides: Partial<RouteDecision> = {},
29
- ): RouteDecision {
30
- return {
31
- protocolVersion: ROUTE_PROTOCOL_VERSION,
32
- decisionId: "dec_1",
33
- requestId: "req_1",
34
- route: {
35
- provider: "claude",
36
- model: "sonnet",
37
- authTier: "subscription",
38
- authScope: "first-party",
39
- },
40
- reason: "subscription_preferred",
41
- explanation: "Selected healthy subscription candidate.",
42
- fallbackChain: [
43
- {
44
- provider: "claude",
45
- model: "sonnet",
46
- authTier: "subscription",
47
- reason: "primary",
48
- },
49
- ],
50
- auditSummary: `route=claude/sonnet tier=subscription profile=claude-personal digest=${PROMPT_DIGEST}`,
51
- blocked: false,
52
- needsConfiguration: false,
53
- ...overrides,
54
- };
55
- }
56
-
57
- function collectText(record: AuditRecord): string {
58
- return [
59
- record.recordId,
60
- record.kind,
61
- record.requestId,
62
- record.decisionId,
63
- record.profile,
64
- record.promptDigest,
65
- record.promptPreviewRedacted,
66
- record.auditSummary,
67
- record.reason,
68
- record.explanation,
69
- record.apiKeyFallbackReason,
70
- ...(record.policyDenials?.map((d) => d.message) ?? []),
71
- JSON.stringify(record.route),
72
- JSON.stringify(record.fallbackChain),
73
- ]
74
- .filter((s): s is string => typeof s === "string")
75
- .join("\n");
76
- }
77
-
78
- describe("InMemoryAuditStore", () => {
79
- it("appends a redacted decision with digest, not raw prompt text", async () => {
80
- const store = new InMemoryAuditStore();
81
- const request = baseRequest();
82
- const decision = baseDecision();
83
-
84
- const record = await store.appendDecision(request, decision);
85
-
86
- expect(record.promptDigest).toBe(PROMPT_DIGEST);
87
- expect(record.auditSummary).toContain(PROMPT_DIGEST);
88
- expect(record.promptPreviewRedacted).toBeUndefined();
89
- expect(collectText(record)).not.toContain(RAW_PROMPT);
90
- expect(collectText(record)).not.toContain(
91
- "sk-ant-api03-abcdefghijklmnopqrstuv",
92
- );
93
-
94
- const listed = await store.list({ requestId: "req_1" });
95
- expect(listed).toHaveLength(1);
96
- expect(listed[0]?.promptDigest).toBe(PROMPT_DIGEST);
97
- expect(collectText(listed[0]!)).not.toContain(RAW_PROMPT);
98
- });
99
-
100
- it("records API-key fallback reason when paid capacity is selected", async () => {
101
- const store = new InMemoryAuditStore();
102
- const request = baseRequest({ requestId: "req_fallback" });
103
- const decision = baseDecision({
104
- decisionId: "dec_fallback",
105
- requestId: "req_fallback",
106
- route: {
107
- provider: "openai",
108
- model: "gpt-4o-mini",
109
- authTier: "api-key",
110
- authScope: "third-party",
111
- },
112
- reason: "api_key_fallback",
113
- explanation:
114
- "Selected API-key candidate openai/gpt-4o-mini after subscription capacity was exhausted.",
115
- fallbackChain: [
116
- {
117
- provider: "openai",
118
- model: "gpt-4o-mini",
119
- authTier: "api-key",
120
- reason: "api_key_fallback",
121
- },
122
- {
123
- provider: "claude",
124
- model: "sonnet",
125
- authTier: "subscription",
126
- reason: "subscription_exhausted",
127
- },
128
- ],
129
- auditSummary: `route=openai/gpt-4o-mini tier=api-key profile=claude-personal digest=${PROMPT_DIGEST}`,
130
- });
131
-
132
- const record = await store.appendDecision(request, decision);
133
-
134
- expect(record.kind).toBe("api_key_fallback");
135
- expect(record.apiKeyFallbackReason).toBe("api_key_fallback");
136
- expect(record.route?.authTier).toBe("api-key");
137
- expect(record.explanation).toMatch(/API-key|subscription/i);
138
- expect(collectText(record)).not.toContain(RAW_PROMPT);
139
-
140
- // Explicit recorder path also works.
141
- const explicit = await store.recordApiKeyFallback({
142
- requestId: "req_fallback",
143
- decisionId: "dec_fallback",
144
- reason: "subscription_exhausted",
145
- explanation: "Subscription quota exhausted; using paid API-key route.",
146
- route: decision.route,
147
- promptDigest: PROMPT_DIGEST,
148
- });
149
- expect(explicit.kind).toBe("api_key_fallback");
150
- expect(explicit.apiKeyFallbackReason).toBe("subscription_exhausted");
151
- });
152
-
153
- it("redacts explicit API-key fallback reason fields", async () => {
154
- const secret = "sk-fallback-reason-secret-abcdefghijklmnopqrstuvwxyz";
155
- const store = new InMemoryAuditStore({ knownSecrets: [secret] });
156
-
157
- const record = await store.recordApiKeyFallback({
158
- requestId: "req_fallback_secret",
159
- decisionId: "dec_fallback_secret",
160
- reason: `subscription exhausted after provider returned ${secret}`,
161
- explanation: `fallback activated after ${secret}`,
162
- promptDigest: "sha256:fallback-secret",
163
- });
164
-
165
- expect(record.reason).not.toContain(secret);
166
- expect(record.explanation).not.toContain(secret);
167
- expect(record.apiKeyFallbackReason).not.toContain(secret);
168
- expect(JSON.stringify(record)).not.toContain(secret);
169
- expect(record.apiKeyFallbackReason).toContain("[REDACTED]");
170
- });
171
-
172
- it("tracks cancellation state for an in-process request", async () => {
173
- const store = new InMemoryAuditStore();
174
- const request = baseRequest({ requestId: "req_cancel" });
175
- const decision = baseDecision({
176
- decisionId: "dec_cancel",
177
- requestId: "req_cancel",
178
- });
179
-
180
- await store.appendDecision(request, decision);
181
- await expect(store.getCancellationState("req_cancel")).resolves.toBe(
182
- "active",
183
- );
184
-
185
- const cancelled = await store.markCancelled("req_cancel", {
186
- reason: "user_cancelled",
187
- });
188
- expect(cancelled?.kind).toBe("cancellation");
189
- expect(cancelled?.cancellationState).toBe("cancelled");
190
- expect(cancelled?.cancelledAt).toBeTruthy();
191
- await expect(store.getCancellationState("req_cancel")).resolves.toBe(
192
- "cancelled",
193
- );
194
-
195
- // Completed does not override cancelled.
196
- const after = await store.markCompleted("req_cancel");
197
- expect(after?.cancellationState).toBe("cancelled");
198
- });
199
-
200
- it("marks completed on successful finish without leaking prompt", async () => {
201
- const store = new InMemoryAuditStore();
202
- const request = baseRequest({ requestId: "req_done" });
203
- await store.appendDecision(
204
- request,
205
- baseDecision({ decisionId: "dec_done", requestId: "req_done" }),
206
- );
207
-
208
- const done = await store.markCompleted("req_done");
209
- expect(done?.cancellationState).toBe("completed");
210
- expect(collectText(done!)).not.toContain(RAW_PROMPT);
211
- });
212
-
213
- it("never leaks raw prompt or secrets by default (including toJSON)", async () => {
214
- const secret = "sk-super-secret-value-never-in-audit-012345";
215
- const store = new InMemoryAuditStore({ knownSecrets: [secret] });
216
- const prompt = `please use ${secret} and do bad things`;
217
- const request = baseRequest({
218
- requestId: "req_secret",
219
- task: {
220
- promptPreview: prompt,
221
- promptDigest: "sha256:secrets",
222
- kind: "code",
223
- risk: "high",
224
- },
225
- });
226
- const decision = baseDecision({
227
- decisionId: "dec_secret",
228
- requestId: "req_secret",
229
- explanation: `Failed with key ${secret} mid-flight`,
230
- auditSummary: `digest=sha256:secrets secret=${secret}`,
231
- });
232
-
233
- const record = await store.appendDecision(request, decision);
234
- const blob = collectText(record);
235
- expect(blob).not.toContain(secret);
236
- expect(blob).not.toContain(prompt);
237
- expect(blob).toMatch(/\[REDACTED\]/);
238
-
239
- const json = JSON.stringify(store.toJSON());
240
- expect(json).not.toContain(secret);
241
- expect(json).not.toContain(prompt);
242
- expect(json).not.toContain(RAW_PROMPT);
243
- // Digests are fine.
244
- expect(store.toJSON()).toMatchObject({
245
- kind: "in-memory-audit",
246
- digests: expect.arrayContaining(["sha256:secrets"]),
247
- });
248
- });
249
-
250
- it("stores opt-in redacted previews only, truncated and scrubbed", async () => {
251
- const store = new InMemoryAuditStore();
252
- const prompt =
253
- "sk-ant-api03-abcdefghijklmnopqrstuv please analyze the large attached source listing for memory leaks";
254
- const request = baseRequest({
255
- requestId: "req_preview",
256
- task: {
257
- promptPreview: prompt,
258
- promptDigest: "sha256:preview",
259
- kind: "review",
260
- risk: "medium",
261
- },
262
- });
263
-
264
- const record = await store.appendDecision(
265
- request,
266
- baseDecision({
267
- decisionId: "dec_preview",
268
- requestId: "req_preview",
269
- auditSummary: "digest=sha256:preview",
270
- }),
271
- { includePromptPreview: true, previewMaxChars: 40 },
272
- );
273
-
274
- expect(record.promptPreviewRedacted).toBeDefined();
275
- expect(record.promptPreviewRedacted!.length).toBeLessThanOrEqual(40);
276
- expect(record.promptPreviewRedacted).not.toContain(
277
- "sk-ant-api03-abcdefghijklmnopqrstuv",
278
- );
279
- expect(record.promptPreviewRedacted).toMatch(/\[REDACTED\]/);
280
- // Full raw prompt still absent.
281
- expect(collectText(record)).not.toContain(prompt);
282
- });
283
-
284
- it("serializes concurrent appends without losing records", async () => {
285
- const store = new InMemoryAuditStore();
286
- const n = 20;
287
- await Promise.all(
288
- Array.from({ length: n }, (_, i) =>
289
- store.appendDecision(
290
- baseRequest({ requestId: `req_conc_${i}` }),
291
- baseDecision({
292
- decisionId: `dec_conc_${i}`,
293
- requestId: `req_conc_${i}`,
294
- auditSummary: `digest=${PROMPT_DIGEST} n=${i}`,
295
- }),
296
- ),
297
- ),
298
- );
299
- const all = await store.list();
300
- expect(all).toHaveLength(n);
301
- });
302
- });