@tormentalabs/claude-code-wire-compat 0.1.0-rc.16 → 0.1.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/CHANGELOG.md +132 -1
- package/README.md +53 -1
- package/dist/build-request.d.ts.map +1 -1
- package/dist/build-request.js +164 -26
- package/dist/build-request.js.map +1 -1
- package/dist/contracts.d.ts +103 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/redaction.d.ts +11 -0
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +11 -0
- package/dist/redaction.js.map +1 -1
- package/dist/request-body.d.ts.map +1 -1
- package/dist/request-body.js +74 -19
- package/dist/request-body.js.map +1 -1
- package/dist/system-prompt.d.ts +12 -5
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +29 -13
- package/dist/system-prompt.js.map +1 -1
- package/package.json +2 -1
- package/src/anti-verbosity.ts +219 -0
- package/src/beta-registry.ts +140 -0
- package/src/betas.ts +219 -0
- package/src/build-request.ts +1655 -0
- package/src/contracts.ts +1233 -0
- package/src/count-tokens.ts +84 -0
- package/src/fingerprint.ts +85 -0
- package/src/headers.ts +442 -0
- package/src/index.ts +62 -0
- package/src/metadata.ts +331 -0
- package/src/model-capabilities.ts +295 -0
- package/src/model-identity.ts +45 -0
- package/src/models.ts +46 -0
- package/src/profiles/claude-code-2.1.195.ts +154 -0
- package/src/redaction.ts +521 -0
- package/src/request-body.ts +1924 -0
- package/src/sha256.ts +114 -0
- package/src/system-prompt.ts +222 -0
- package/src/thinking.ts +266 -0
- package/src/unicode.ts +24 -0
package/src/redaction.ts
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ClaudeCodeCapabilityDecisions,
|
|
5
|
+
ClaudeCodeModelFamily,
|
|
6
|
+
ClaudeCodeProtocolProfile,
|
|
7
|
+
ClaudeCodeRequestInput,
|
|
8
|
+
HeaderPair,
|
|
9
|
+
RedactedRequestEvidence,
|
|
10
|
+
} from "./contracts.js";
|
|
11
|
+
import { ClaudeCodeWireError } from "./contracts.js";
|
|
12
|
+
import { classifySurrogateAt } from "./unicode.js";
|
|
13
|
+
|
|
14
|
+
/** Excludes values used only while validating and assembling the request. */
|
|
15
|
+
export type NormalizedRequestInput = Omit<
|
|
16
|
+
ClaudeCodeRequestInput,
|
|
17
|
+
"clientRequestId" | "crypto" | "profileOverride"
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export interface BuildRedactedEvidenceInput {
|
|
21
|
+
readonly profile: ClaudeCodeProtocolProfile;
|
|
22
|
+
/** Supplies the validated effective profile when an override is active. */
|
|
23
|
+
readonly effectiveProfile?: ClaudeCodeProtocolProfile;
|
|
24
|
+
readonly request: NormalizedRequestInput;
|
|
25
|
+
readonly modelFamily: ClaudeCodeModelFamily;
|
|
26
|
+
readonly logicalHeaders: readonly HeaderPair[];
|
|
27
|
+
readonly betaFeatures: readonly string[];
|
|
28
|
+
readonly body: string;
|
|
29
|
+
/**
|
|
30
|
+
* Carries the names discarded by `extraHeaderPolicy: "dropConflicting"`.
|
|
31
|
+
*
|
|
32
|
+
* Supplied only under that policy, so evidence for every other request keeps
|
|
33
|
+
* its original shape.
|
|
34
|
+
*/
|
|
35
|
+
readonly droppedExtraHeaderNames?: readonly string[];
|
|
36
|
+
readonly suppressedBetaNames?: readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Carries the number of caller system blocks the canonical system actually
|
|
39
|
+
* EMITTED, which is not the raw length of `request.system`: adjacent caller
|
|
40
|
+
* blocks sharing a `cache_control` merge into one, and a block byte-identical
|
|
41
|
+
* to the pinned identity text is dropped.
|
|
42
|
+
*
|
|
43
|
+
* The parser asserts `systemBlockCount === body.system.length - <canonical>`,
|
|
44
|
+
* so the raw length made every merged request unparseable by this package.
|
|
45
|
+
*/
|
|
46
|
+
readonly emittedSystemBlockCount?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Set only when `suppressBillingBlock` removed the billing block, so evidence
|
|
49
|
+
* for every request that ignores the seam keeps its original shape.
|
|
50
|
+
*/
|
|
51
|
+
readonly billingBlockSuppressed?: true;
|
|
52
|
+
/**
|
|
53
|
+
* Set only when the root `suppressIdentityBlock` removed the identity block,
|
|
54
|
+
* so evidence for every request that ignores the seam keeps its shape.
|
|
55
|
+
*/
|
|
56
|
+
readonly identityBlockSuppressed?: true;
|
|
57
|
+
/**
|
|
58
|
+
* Set only when `preserveThinkingBlockCacheControl` was active AND at least
|
|
59
|
+
* one emitted reasoning block actually carried `cache_control`, so evidence
|
|
60
|
+
* for every request that ignores the seam keeps its shape.
|
|
61
|
+
*/
|
|
62
|
+
readonly thinkingBlockCacheControlPreserved?: true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const MAX_INPUT_DEPTH = 100;
|
|
66
|
+
const MAX_INPUT_SIZE = 1_000_000;
|
|
67
|
+
const ENDPOINT = "https://api.anthropic.com/v1/messages?beta=true";
|
|
68
|
+
const FORBIDDEN_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
69
|
+
const SAFE_ERROR_CODES = new Set([
|
|
70
|
+
"INVALID_INPUT",
|
|
71
|
+
"INVALID_IDENTITY",
|
|
72
|
+
"UNSUPPORTED_CAPABILITY",
|
|
73
|
+
"INVALID_THINKING",
|
|
74
|
+
"INVALID_EFFORT",
|
|
75
|
+
"FORBIDDEN_HEADER",
|
|
76
|
+
"DUPLICATE_HEADER",
|
|
77
|
+
"HEADER_INJECTION",
|
|
78
|
+
"INVALID_UNICODE",
|
|
79
|
+
"INPUT_TOO_DEEP",
|
|
80
|
+
"INPUT_TOO_LARGE",
|
|
81
|
+
"CYCLIC_INPUT",
|
|
82
|
+
"CRYPTO_UNAVAILABLE",
|
|
83
|
+
"REDACTION_FAILURE",
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
type SafePrimitive = string | number | boolean;
|
|
87
|
+
|
|
88
|
+
type TraversalEntry =
|
|
89
|
+
| {
|
|
90
|
+
readonly kind: "visit";
|
|
91
|
+
readonly value: unknown;
|
|
92
|
+
readonly depth: number;
|
|
93
|
+
}
|
|
94
|
+
| { readonly kind: "leave"; readonly value: object };
|
|
95
|
+
|
|
96
|
+
function wireError(
|
|
97
|
+
code:
|
|
98
|
+
| "INVALID_INPUT"
|
|
99
|
+
| "INVALID_UNICODE"
|
|
100
|
+
| "INPUT_TOO_DEEP"
|
|
101
|
+
| "INPUT_TOO_LARGE"
|
|
102
|
+
| "CYCLIC_INPUT"
|
|
103
|
+
| "CRYPTO_UNAVAILABLE"
|
|
104
|
+
| "REDACTION_FAILURE",
|
|
105
|
+
safeDetails: Readonly<Record<string, SafePrimitive>> = {},
|
|
106
|
+
): ClaudeCodeWireError {
|
|
107
|
+
return new ClaudeCodeWireError(code, safeDetails);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isValidUnicode(value: string): boolean {
|
|
111
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
112
|
+
const classification = classifySurrogateAt(value, index);
|
|
113
|
+
if (classification === "loneSurrogate") return false;
|
|
114
|
+
if (classification === "surrogatePair") index += 1;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function measureString(value: string, encoder: TextEncoder): number {
|
|
120
|
+
if (!isValidUnicode(value)) throw wireError("INVALID_UNICODE");
|
|
121
|
+
return encoder.encode(value).byteLength;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function validateInputGraph(value: unknown, encoder: TextEncoder): void {
|
|
125
|
+
const active = new WeakSet();
|
|
126
|
+
const completed = new WeakSet();
|
|
127
|
+
const stack: TraversalEntry[] = [{ kind: "visit", value, depth: 0 }];
|
|
128
|
+
let aggregateSize = 0;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
// Deliberately make an empty stack the normal loop exit, avoiding an
|
|
132
|
+
// untestable defensive branch after a separate length check.
|
|
133
|
+
let entry: TraversalEntry | undefined;
|
|
134
|
+
while ((entry = stack.pop()) !== undefined) {
|
|
135
|
+
if (entry.kind === "leave") {
|
|
136
|
+
active.delete(entry.value);
|
|
137
|
+
completed.add(entry.value);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const current = entry.value;
|
|
142
|
+
if (entry.depth > MAX_INPUT_DEPTH) {
|
|
143
|
+
throw wireError("INPUT_TOO_DEEP", { maximumDepth: MAX_INPUT_DEPTH });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof current === "string") {
|
|
147
|
+
aggregateSize += measureString(current, encoder);
|
|
148
|
+
} else if (
|
|
149
|
+
current === null ||
|
|
150
|
+
typeof current === "number" ||
|
|
151
|
+
typeof current === "boolean" ||
|
|
152
|
+
typeof current === "undefined"
|
|
153
|
+
) {
|
|
154
|
+
aggregateSize += 1;
|
|
155
|
+
} else if (typeof current !== "object") {
|
|
156
|
+
throw wireError("INVALID_INPUT");
|
|
157
|
+
} else {
|
|
158
|
+
if (active.has(current)) throw wireError("CYCLIC_INPUT");
|
|
159
|
+
if (completed.has(current)) continue;
|
|
160
|
+
|
|
161
|
+
const prototype: unknown = Object.getPrototypeOf(current);
|
|
162
|
+
if (
|
|
163
|
+
prototype !== null &&
|
|
164
|
+
prototype !== Object.prototype &&
|
|
165
|
+
prototype !== Array.prototype
|
|
166
|
+
) {
|
|
167
|
+
throw wireError("INVALID_INPUT");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
active.add(current);
|
|
171
|
+
stack.push({ kind: "leave", value: current });
|
|
172
|
+
|
|
173
|
+
const keys = Reflect.ownKeys(current);
|
|
174
|
+
aggregateSize += keys.length;
|
|
175
|
+
for (const key of keys) {
|
|
176
|
+
if (typeof key !== "string") throw wireError("INVALID_INPUT");
|
|
177
|
+
if (FORBIDDEN_KEYS.has(key)) throw wireError("INVALID_INPUT");
|
|
178
|
+
|
|
179
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
180
|
+
if (descriptor === undefined) throw wireError("INVALID_INPUT");
|
|
181
|
+
if (!("value" in descriptor)) throw wireError("INVALID_INPUT");
|
|
182
|
+
if (!descriptor.enumerable) continue;
|
|
183
|
+
|
|
184
|
+
aggregateSize += measureString(key, encoder);
|
|
185
|
+
const child: unknown = descriptor.value;
|
|
186
|
+
stack.push({
|
|
187
|
+
kind: "visit",
|
|
188
|
+
value: child,
|
|
189
|
+
depth: entry.depth + 1,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (aggregateSize > MAX_INPUT_SIZE) {
|
|
195
|
+
throw wireError("INPUT_TOO_LARGE", { maximumSize: MAX_INPUT_SIZE });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (error: unknown) {
|
|
199
|
+
if (error instanceof ClaudeCodeWireError) throw error;
|
|
200
|
+
throw wireError("REDACTION_FAILURE");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isCryptoProvider(value: unknown): value is Pick<Crypto, "subtle"> {
|
|
205
|
+
if (typeof value !== "object" || value === null) return false;
|
|
206
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, "subtle");
|
|
207
|
+
if (descriptor !== undefined && "value" in descriptor) {
|
|
208
|
+
return typeof descriptor.value === "object" && descriptor.value !== null;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
return typeof Reflect.get(value, "subtle") === "object";
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function getDefaultCrypto(): Pick<Crypto, "subtle"> {
|
|
218
|
+
let candidate: unknown;
|
|
219
|
+
try {
|
|
220
|
+
candidate = globalThis.crypto;
|
|
221
|
+
} catch {
|
|
222
|
+
throw wireError("CRYPTO_UNAVAILABLE");
|
|
223
|
+
}
|
|
224
|
+
if (!isCryptoProvider(candidate)) throw wireError("CRYPTO_UNAVAILABLE");
|
|
225
|
+
return candidate;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function selectCryptoProvider(
|
|
229
|
+
injected: Pick<Crypto, "subtle"> | undefined,
|
|
230
|
+
): Pick<Crypto, "subtle"> {
|
|
231
|
+
if (injected !== undefined) return injected;
|
|
232
|
+
return getDefaultCrypto();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function toHex(bytes: Uint8Array): string {
|
|
236
|
+
let result = "";
|
|
237
|
+
for (const byte of bytes) result += byte.toString(16).padStart(2, "0");
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function capabilityDecisions(
|
|
242
|
+
input: BuildRedactedEvidenceInput,
|
|
243
|
+
): ClaudeCodeCapabilityDecisions {
|
|
244
|
+
const requested = input.request.capabilities;
|
|
245
|
+
const use1MContext = input.request.betaOverrides?.use1MContext;
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
// Package extension: emitted only when the caller stated a decision, so
|
|
248
|
+
// evidence for requests without `betaOverrides` keeps its original shape.
|
|
249
|
+
...(use1MContext === undefined ? {} : { use1MContext }),
|
|
250
|
+
thinking: requested?.thinking ?? false,
|
|
251
|
+
adaptiveThinking: requested?.adaptiveThinking ?? false,
|
|
252
|
+
effort: requested?.effort ?? false,
|
|
253
|
+
interleavedThinking: requested?.interleavedThinking ?? false,
|
|
254
|
+
maxEffort: requested?.maxEffort ?? false,
|
|
255
|
+
xhighEffort: requested?.xhighEffort ?? false,
|
|
256
|
+
contextManagement: requested?.contextManagement ?? false,
|
|
257
|
+
temperature: requested?.temperature ?? false,
|
|
258
|
+
rejectsDisabledThinking: requested?.rejectsDisabledThinking ?? false,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function readOwnValue(value: unknown, key: string): unknown {
|
|
263
|
+
if (typeof value !== "object" || value === null) {
|
|
264
|
+
throw wireError("INVALID_INPUT");
|
|
265
|
+
}
|
|
266
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
267
|
+
if (descriptor === undefined || !("value" in descriptor)) {
|
|
268
|
+
throw wireError("INVALID_INPUT");
|
|
269
|
+
}
|
|
270
|
+
const result: unknown = descriptor.value;
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function assertEvidenceSources(value: unknown): void {
|
|
275
|
+
if (typeof value !== "object" || value === null) {
|
|
276
|
+
throw wireError("INVALID_INPUT");
|
|
277
|
+
}
|
|
278
|
+
const profile = readOwnValue(value, "profile");
|
|
279
|
+
const profileId = readOwnValue(profile, "id");
|
|
280
|
+
const endpoint = readOwnValue(profile, "endpoint");
|
|
281
|
+
const effectiveProfile = Object.hasOwn(value, "effectiveProfile")
|
|
282
|
+
? readOwnValue(value, "effectiveProfile")
|
|
283
|
+
: undefined;
|
|
284
|
+
const modelFamily = readOwnValue(value, "modelFamily");
|
|
285
|
+
const logicalHeaders = readOwnValue(value, "logicalHeaders");
|
|
286
|
+
const betaFeatures = readOwnValue(value, "betaFeatures");
|
|
287
|
+
|
|
288
|
+
if (profileId !== "claude-code-2.1.195-sdk-0.94.0" || endpoint !== ENDPOINT) {
|
|
289
|
+
throw wireError("INVALID_INPUT");
|
|
290
|
+
}
|
|
291
|
+
if (effectiveProfile !== undefined) {
|
|
292
|
+
const effectiveId = readOwnValue(effectiveProfile, "id");
|
|
293
|
+
if (
|
|
294
|
+
typeof effectiveId !== "string" ||
|
|
295
|
+
effectiveId.length === 0 ||
|
|
296
|
+
readOwnValue(effectiveProfile, "endpoint") !== ENDPOINT ||
|
|
297
|
+
readOwnValue(effectiveProfile, "provider") !== "anthropic" ||
|
|
298
|
+
readOwnValue(effectiveProfile, "anthropicVersion") !== "2023-06-01"
|
|
299
|
+
) {
|
|
300
|
+
throw wireError("INVALID_INPUT");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (
|
|
304
|
+
modelFamily !== "haiku" &&
|
|
305
|
+
modelFamily !== "sonnet" &&
|
|
306
|
+
modelFamily !== "opus" &&
|
|
307
|
+
modelFamily !== "fable" &&
|
|
308
|
+
modelFamily !== "mythos" &&
|
|
309
|
+
modelFamily !== "unknown"
|
|
310
|
+
) {
|
|
311
|
+
throw wireError("INVALID_INPUT");
|
|
312
|
+
}
|
|
313
|
+
if (!Array.isArray(logicalHeaders) || !Array.isArray(betaFeatures)) {
|
|
314
|
+
throw wireError("INVALID_INPUT");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function containsCredential(
|
|
319
|
+
value: string,
|
|
320
|
+
credentials: readonly string[],
|
|
321
|
+
): boolean {
|
|
322
|
+
for (const credential of credentials) {
|
|
323
|
+
if (credential.length > 0 && value.includes(credential)) return true;
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function collectCredentialValues(
|
|
329
|
+
input: BuildRedactedEvidenceInput,
|
|
330
|
+
): readonly string[] {
|
|
331
|
+
const credentials = [input.request.accessToken];
|
|
332
|
+
for (const header of input.logicalHeaders) {
|
|
333
|
+
const candidate: unknown = header;
|
|
334
|
+
if (!Array.isArray(candidate) || candidate.length !== 2) {
|
|
335
|
+
throw wireError("INVALID_INPUT");
|
|
336
|
+
}
|
|
337
|
+
const name = header[0];
|
|
338
|
+
const value = header[1];
|
|
339
|
+
if (typeof name !== "string" || typeof value !== "string") {
|
|
340
|
+
throw wireError("INVALID_INPUT");
|
|
341
|
+
}
|
|
342
|
+
if (name.toLowerCase() === "authorization") {
|
|
343
|
+
const separator = value.indexOf(" ");
|
|
344
|
+
credentials.push(separator === -1 ? value : value.slice(separator + 1));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return credentials;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function toSafeErrorDetails(
|
|
351
|
+
value: unknown,
|
|
352
|
+
): Readonly<Record<string, string | number | boolean>> {
|
|
353
|
+
const details: Record<string, SafePrimitive> = {};
|
|
354
|
+
if (!(value instanceof ClaudeCodeWireError)) return Object.freeze(details);
|
|
355
|
+
|
|
356
|
+
let code: unknown;
|
|
357
|
+
let safeDetails: unknown;
|
|
358
|
+
try {
|
|
359
|
+
code = readOwnValue(value, "code");
|
|
360
|
+
safeDetails = readOwnValue(value, "safeDetails");
|
|
361
|
+
} catch {
|
|
362
|
+
return Object.freeze(details);
|
|
363
|
+
}
|
|
364
|
+
if (typeof code !== "string" || !SAFE_ERROR_CODES.has(code)) {
|
|
365
|
+
return Object.freeze(details);
|
|
366
|
+
}
|
|
367
|
+
details["code"] = code;
|
|
368
|
+
if (typeof safeDetails !== "object" || safeDetails === null) {
|
|
369
|
+
return Object.freeze(details);
|
|
370
|
+
}
|
|
371
|
+
const numericKeys = [
|
|
372
|
+
"bodyByteLength",
|
|
373
|
+
"messageCount",
|
|
374
|
+
"systemBlockCount",
|
|
375
|
+
"logicalHeaderCount",
|
|
376
|
+
"betaFeatureCount",
|
|
377
|
+
"maximumDepth",
|
|
378
|
+
"maximumSize",
|
|
379
|
+
] as const;
|
|
380
|
+
const booleanKeys = ["hasSystem", "hasTools"] as const;
|
|
381
|
+
|
|
382
|
+
for (const key of numericKeys) {
|
|
383
|
+
const descriptor = Object.getOwnPropertyDescriptor(safeDetails, key);
|
|
384
|
+
const detail: unknown =
|
|
385
|
+
descriptor !== undefined && "value" in descriptor
|
|
386
|
+
? descriptor.value
|
|
387
|
+
: undefined;
|
|
388
|
+
if (typeof detail === "number" && Number.isFinite(detail)) {
|
|
389
|
+
details[key] = detail;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
for (const key of booleanKeys) {
|
|
393
|
+
const descriptor = Object.getOwnPropertyDescriptor(safeDetails, key);
|
|
394
|
+
const detail: unknown =
|
|
395
|
+
descriptor !== undefined && "value" in descriptor
|
|
396
|
+
? descriptor.value
|
|
397
|
+
: undefined;
|
|
398
|
+
if (typeof detail === "boolean") details[key] = detail;
|
|
399
|
+
}
|
|
400
|
+
return Object.freeze(details);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function buildRedactedEvidence(
|
|
404
|
+
input: BuildRedactedEvidenceInput,
|
|
405
|
+
cryptoProvider?: Pick<Crypto, "subtle">,
|
|
406
|
+
): Promise<RedactedRequestEvidence> {
|
|
407
|
+
const encoder = new TextEncoder();
|
|
408
|
+
validateInputGraph(input, encoder);
|
|
409
|
+
assertEvidenceSources(input);
|
|
410
|
+
|
|
411
|
+
if (typeof input.body !== "string") throw wireError("INVALID_INPUT");
|
|
412
|
+
const bodyBytes = encoder.encode(input.body);
|
|
413
|
+
const credentials = collectCredentialValues(input);
|
|
414
|
+
const logicalHeaderNames: string[] = [];
|
|
415
|
+
for (const header of input.logicalHeaders) {
|
|
416
|
+
const name = header[0];
|
|
417
|
+
if (containsCredential(name, credentials)) throw wireError("INVALID_INPUT");
|
|
418
|
+
logicalHeaderNames.push(name);
|
|
419
|
+
}
|
|
420
|
+
const betaFeatures: string[] = [];
|
|
421
|
+
for (const feature of input.betaFeatures) {
|
|
422
|
+
if (containsCredential(feature, credentials))
|
|
423
|
+
throw wireError("INVALID_INPUT");
|
|
424
|
+
betaFeatures.push(feature);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Dropped names are caller-controlled text that lands in evidence. They get
|
|
428
|
+
// the same credential screening as the header names that did reach the wire.
|
|
429
|
+
const droppedExtraHeaderNames: string[] = [];
|
|
430
|
+
for (const name of input.droppedExtraHeaderNames ?? []) {
|
|
431
|
+
if (containsCredential(name, credentials)) throw wireError("INVALID_INPUT");
|
|
432
|
+
droppedExtraHeaderNames.push(name);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Suppressed names never reached the wire, but they are caller-controlled
|
|
436
|
+
// text landing in evidence, so they get the same credential screening.
|
|
437
|
+
const suppressedBetaNames: string[] = [];
|
|
438
|
+
for (const name of input.suppressedBetaNames ?? []) {
|
|
439
|
+
if (containsCredential(name, credentials)) throw wireError("INVALID_INPUT");
|
|
440
|
+
suppressedBetaNames.push(name);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// The emitted count is authoritative when supplied; the raw caller length is
|
|
444
|
+
// only a fallback for callers that assemble evidence without a built system.
|
|
445
|
+
const systemBlockCount =
|
|
446
|
+
input.emittedSystemBlockCount ?? input.request.system?.length ?? 0;
|
|
447
|
+
|
|
448
|
+
const provider = selectCryptoProvider(cryptoProvider);
|
|
449
|
+
if (!isCryptoProvider(provider)) throw wireError("CRYPTO_UNAVAILABLE");
|
|
450
|
+
|
|
451
|
+
let digest: ArrayBuffer;
|
|
452
|
+
try {
|
|
453
|
+
digest = await provider.subtle.digest("SHA-256", bodyBytes);
|
|
454
|
+
} catch {
|
|
455
|
+
throw wireError("REDACTION_FAILURE", {
|
|
456
|
+
bodyByteLength: bodyBytes.byteLength,
|
|
457
|
+
messageCount: input.request.messages.length,
|
|
458
|
+
systemBlockCount,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let digestBytes: Uint8Array;
|
|
463
|
+
try {
|
|
464
|
+
digestBytes = new Uint8Array(digest);
|
|
465
|
+
} catch {
|
|
466
|
+
throw wireError("REDACTION_FAILURE", {
|
|
467
|
+
bodyByteLength: bodyBytes.byteLength,
|
|
468
|
+
messageCount: input.request.messages.length,
|
|
469
|
+
systemBlockCount,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
if (digestBytes.byteLength !== 32) {
|
|
473
|
+
throw wireError("REDACTION_FAILURE", {
|
|
474
|
+
bodyByteLength: bodyBytes.byteLength,
|
|
475
|
+
messageCount: input.request.messages.length,
|
|
476
|
+
systemBlockCount,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const bodySha256 = toHex(digestBytes);
|
|
480
|
+
|
|
481
|
+
const evidence: RedactedRequestEvidence = {
|
|
482
|
+
profileId: input.effectiveProfile?.id ?? input.profile.id,
|
|
483
|
+
url: ENDPOINT,
|
|
484
|
+
method: "POST",
|
|
485
|
+
modelFamily: input.modelFamily,
|
|
486
|
+
logicalHeaderNames: Object.freeze(logicalHeaderNames),
|
|
487
|
+
betaFeatures: Object.freeze(betaFeatures),
|
|
488
|
+
bodySha256,
|
|
489
|
+
bodyByteLength: bodyBytes.byteLength,
|
|
490
|
+
messageCount: input.request.messages.length,
|
|
491
|
+
systemBlockCount,
|
|
492
|
+
capabilityDecisions: capabilityDecisions(input),
|
|
493
|
+
// Package extension: emitted only when the caller opted into
|
|
494
|
+
// `dropConflicting`, so evidence for every other request is unchanged.
|
|
495
|
+
...(input.droppedExtraHeaderNames === undefined
|
|
496
|
+
? {}
|
|
497
|
+
: { droppedExtraHeaderNames: Object.freeze(droppedExtraHeaderNames) }),
|
|
498
|
+
// Package extension: emitted only when the suppression seam removed at
|
|
499
|
+
// least one identifier, so evidence for every other request is unchanged.
|
|
500
|
+
...(suppressedBetaNames.length === 0
|
|
501
|
+
? {}
|
|
502
|
+
: { suppressedBetaNames: Object.freeze(suppressedBetaNames) }),
|
|
503
|
+
// Package extension: emitted only when the billing block was actually
|
|
504
|
+
// suppressed, so evidence for every other request is unchanged.
|
|
505
|
+
...(input.billingBlockSuppressed === true
|
|
506
|
+
? { billingBlockSuppressed: true }
|
|
507
|
+
: {}),
|
|
508
|
+
// Package extension: emitted only when the identity block was actually
|
|
509
|
+
// suppressed, so evidence for every other request is unchanged.
|
|
510
|
+
...(input.identityBlockSuppressed === true
|
|
511
|
+
? { identityBlockSuppressed: true }
|
|
512
|
+
: {}),
|
|
513
|
+
// Package extension: emitted only when the seam was active AND a reasoning
|
|
514
|
+
// block actually carried the marker, so evidence for every other request is
|
|
515
|
+
// unchanged.
|
|
516
|
+
...(input.thinkingBlockCacheControlPreserved === true
|
|
517
|
+
? { thinkingBlockCacheControlPreserved: true }
|
|
518
|
+
: {}),
|
|
519
|
+
};
|
|
520
|
+
return Object.freeze(evidence);
|
|
521
|
+
}
|