@tormentalabs/claude-code-wire-compat 0.1.0-rc.17 → 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.
@@ -0,0 +1,84 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import { COUNT_TOKENS_BETAS } from "./beta-registry.js";
4
+ import type { Message, ToolDefinition } from "./contracts.js";
5
+
6
+ /** Upstream SDK `countTokens` endpoint at byte offset 224471633. */
7
+ export const COUNT_TOKENS_ENDPOINT =
8
+ "https://api.anthropic.com/v1/messages/count_tokens?beta=true" as const;
9
+
10
+ /** Upstream SDK `countTokens` beta at byte offset 224471633. */
11
+ export const TOKEN_COUNTING_BETA = "token-counting-2024-11-01" as const;
12
+
13
+ /** Upstream `PMo` used by `P5e` at byte offset 235439559. */
14
+ export const COUNT_TOKENS_THINKING_BUDGET = 1024 as const;
15
+
16
+ /** Upstream `P5e` empty-message fallback at byte offset 235439559. */
17
+ export const COUNT_TOKENS_EMPTY_MESSAGES = Object.freeze([
18
+ Object.freeze({ role: "user", content: "foo" }),
19
+ ]);
20
+
21
+ /**
22
+ * Upstream `Bkl` at byte offset 235438568.
23
+ *
24
+ * Decides whether the count-tokens body carries a `thinking` field. Note the
25
+ * two conjuncts upstream requires and this port preserves: the message role
26
+ * must be `assistant`, and its content must be an ARRAY. A thinking block on a
27
+ * user message, or an assistant message whose content is a plain string, does
28
+ * not qualify.
29
+ *
30
+ * Upstream additionally guards each message and block against being a
31
+ * non-object, because it runs on loosely typed internal history. This port is
32
+ * reached only through `canonicalCountTokensLists`, which has already proven
33
+ * every element well formed, so those guards would be unreachable branches.
34
+ * Restore them only if a caller path is ever added that bypasses that
35
+ * canonicaliser.
36
+ */
37
+ export function containsThinkingBlock(messages: readonly Message[]): boolean {
38
+ return messages.some(
39
+ (message) =>
40
+ message.role === "assistant" &&
41
+ typeof message.content !== "string" &&
42
+ message.content.some(
43
+ (block) =>
44
+ block.type === "thinking" || block.type === "redacted_thinking",
45
+ ),
46
+ );
47
+ }
48
+
49
+ /** Upstream `E2r` filtering in `P5e` at byte offset 235439559. */
50
+ export function filterCountTokensBetas(
51
+ composedBetas: readonly string[],
52
+ ): readonly string[] {
53
+ return Object.freeze(
54
+ composedBetas.filter((beta) => COUNT_TOKENS_BETAS.has(beta)),
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Upstream `P5e` wire-body construction at byte offset 235439559.
60
+ *
61
+ * Both list arguments MUST already have been canonicalised by
62
+ * `canonicalCountTokensLists`. Key order is load-bearing: the vendored SDK
63
+ * destructures `betas` out of the body and object rest preserves the
64
+ * declaration order of the survivors, leaving `model`, `messages`, `tools`,
65
+ * then an optional `thinking` on the wire.
66
+ */
67
+ export function buildCountTokensBody(
68
+ model: string,
69
+ messages: readonly Message[],
70
+ tools: readonly ToolDefinition[],
71
+ ): Readonly<Record<string, unknown>> {
72
+ const body: Record<string, unknown> = {
73
+ model,
74
+ messages: messages.length > 0 ? messages : COUNT_TOKENS_EMPTY_MESSAGES,
75
+ tools,
76
+ };
77
+ if (containsThinkingBlock(messages)) {
78
+ body["thinking"] = {
79
+ type: "enabled",
80
+ budget_tokens: COUNT_TOKENS_THINKING_BUDGET,
81
+ };
82
+ }
83
+ return Object.freeze(body);
84
+ }
@@ -0,0 +1,85 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type { TextBlock } from "./contracts.js";
4
+ import { ClaudeCodeWireError } from "./contracts.js";
5
+ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
6
+
7
+ const FINGERPRINT_PREFIX = "59cf53e54c78";
8
+
9
+ function isCryptoProvider(value: unknown): value is Pick<Crypto, "subtle"> {
10
+ if (typeof value !== "object" || value === null) {
11
+ return false;
12
+ }
13
+
14
+ const subtle: unknown = Reflect.get(value, "subtle");
15
+ return (
16
+ typeof subtle === "object" &&
17
+ subtle !== null &&
18
+ typeof Reflect.get(subtle, "digest") === "function"
19
+ );
20
+ }
21
+
22
+ function getDefaultCrypto(): Pick<Crypto, "subtle"> {
23
+ const value: unknown = Reflect.get(globalThis, "crypto");
24
+ if (!isCryptoProvider(value)) {
25
+ throw new ClaudeCodeWireError("CRYPTO_UNAVAILABLE");
26
+ }
27
+ return value;
28
+ }
29
+
30
+ export async function createBillingFingerprint(
31
+ firstUserText: string,
32
+ cliVersion: string,
33
+ crypto?: Pick<Crypto, "subtle">,
34
+ ): Promise<string> {
35
+ const cryptoProvider = crypto ?? getDefaultCrypto();
36
+ const material = `${FINGERPRINT_PREFIX}${firstUserText[4] ?? "0"}${firstUserText[7] ?? "0"}${firstUserText[20] ?? "0"}${cliVersion}`;
37
+ const bytes = new TextEncoder().encode(material);
38
+
39
+ let digest: unknown;
40
+ // Keep this try deliberately narrow so our validation errors are not self-masked.
41
+ try {
42
+ digest = await cryptoProvider.subtle.digest("SHA-256", bytes);
43
+ } catch {
44
+ throw new ClaudeCodeWireError("CRYPTO_UNAVAILABLE");
45
+ }
46
+
47
+ let digestBytes: Uint8Array;
48
+ if (digest instanceof ArrayBuffer) {
49
+ digestBytes = new Uint8Array(digest);
50
+ } else if (ArrayBuffer.isView(digest)) {
51
+ digestBytes = new Uint8Array(
52
+ digest.buffer,
53
+ digest.byteOffset,
54
+ digest.byteLength,
55
+ );
56
+ } else {
57
+ // Unvalidated digests silently corrupt billing fingerprints as "" or "000".
58
+ throw new ClaudeCodeWireError("CRYPTO_UNAVAILABLE");
59
+ }
60
+ if (digestBytes.byteLength !== 32) {
61
+ throw new ClaudeCodeWireError("CRYPTO_UNAVAILABLE");
62
+ }
63
+
64
+ return Array.from(digestBytes, (byte) => byte.toString(16).padStart(2, "0"))
65
+ .join("")
66
+ .slice(0, 3);
67
+ }
68
+
69
+ export async function createBillingBlock(
70
+ firstUserText: string,
71
+ cliVersion: string,
72
+ crypto?: Pick<Crypto, "subtle">,
73
+ ): Promise<TextBlock> {
74
+ const fingerprint = await createBillingFingerprint(
75
+ firstUserText,
76
+ cliVersion,
77
+ crypto,
78
+ );
79
+ const { entrypoint } = CLAUDE_CODE_2_1_195_PROFILE;
80
+
81
+ return {
82
+ type: "text",
83
+ text: `x-anthropic-billing-header: cc_version=${cliVersion}.${fingerprint}; cc_entrypoint=${entrypoint}; cch=00000;`,
84
+ };
85
+ }
package/src/headers.ts ADDED
@@ -0,0 +1,442 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ import type {
4
+ ClaudeCodeExtraHeaderPolicy,
5
+ ClaudeCodeProtocolProfile,
6
+ HeaderPair,
7
+ } from "./contracts.js";
8
+ import { ClaudeCodeWireError } from "./contracts.js";
9
+ import { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";
10
+
11
+ const HEADER_NAMES = Object.freeze({
12
+ anthropicBeta: "anthropic-beta",
13
+ browserAccess: "anthropic-dangerous-direct-browser-access",
14
+ anthropicVersion: "anthropic-version",
15
+ authorization: "authorization",
16
+ contentType: "content-type",
17
+ userAgent: "user-agent",
18
+ app: "x-app",
19
+ sessionId: "x-claude-code-session-id",
20
+ clientRequestId: "x-client-request-id",
21
+ arch: "x-stainless-arch",
22
+ lang: "x-stainless-lang",
23
+ os: "x-stainless-os",
24
+ packageVersion: "x-stainless-package-version",
25
+ retryCount: "x-stainless-retry-count",
26
+ runtime: "x-stainless-runtime",
27
+ runtimeVersion: "x-stainless-runtime-version",
28
+ timeout: "x-stainless-timeout",
29
+ stainlessHelper: "x-stainless-helper",
30
+ remoteContainerId: "x-claude-remote-container-id",
31
+ remoteSessionId: "x-claude-remote-session-id",
32
+ clientApp: "x-client-app",
33
+ additionalProtection: "x-anthropic-additional-protection",
34
+ } as const);
35
+
36
+ const CANONICAL_NAMES: ReadonlySet<string> = new Set(
37
+ Object.values(HEADER_NAMES),
38
+ );
39
+
40
+ interface HeaderRuntime {
41
+ readonly sessionId: string;
42
+ readonly runtime: string;
43
+ readonly runtimeVersion: string;
44
+ readonly os: string;
45
+ readonly arch: string;
46
+ }
47
+
48
+ interface ValidatedInput {
49
+ readonly accessToken: string;
50
+ readonly runtime: HeaderRuntime;
51
+ readonly clientRequestId: string;
52
+ readonly betaFeatures: readonly string[];
53
+ readonly app: "cli" | "cli-bg";
54
+ readonly stainlessRetryCount: number;
55
+ readonly stainlessHelper?: string;
56
+ readonly claudeRemoteContainerId?: string;
57
+ readonly claudeRemoteSessionId?: string;
58
+ readonly clientApp?: string;
59
+ readonly anthropicAdditionalProtection?: string;
60
+ readonly extraHeaders: readonly HeaderPair[];
61
+ readonly extraHeaderPolicy: ClaudeCodeExtraHeaderPolicy;
62
+ readonly profile: ClaudeCodeProtocolProfile;
63
+ }
64
+
65
+ /** Reports the headers placed on the wire plus what the policy discarded. */
66
+ export interface OrderedHeaderPlan {
67
+ readonly headers: readonly HeaderPair[];
68
+ /**
69
+ * Lists the lowercased names `dropConflicting` discarded, in caller order.
70
+ *
71
+ * Always empty under `strict`, which throws instead of dropping.
72
+ */
73
+ readonly droppedExtraHeaderNames: readonly string[];
74
+ }
75
+
76
+ function isRecord(value: unknown): value is Record<string, unknown> {
77
+ return typeof value === "object" && value !== null && !Array.isArray(value);
78
+ }
79
+
80
+ function hasControlCharacter(value: string): boolean {
81
+ for (const character of value) {
82
+ const codePoint = character.codePointAt(0);
83
+ if (
84
+ codePoint !== undefined &&
85
+ (codePoint <= 31 || (codePoint >= 127 && codePoint <= 159))
86
+ ) {
87
+ return true;
88
+ }
89
+ }
90
+ return false;
91
+ }
92
+
93
+ function assertHeaderText(name: string, value: string): void {
94
+ if (hasControlCharacter(name) || hasControlCharacter(value)) {
95
+ throw new ClaudeCodeWireError("HEADER_INJECTION");
96
+ }
97
+ }
98
+
99
+ function requiredString(
100
+ record: Readonly<Record<string, unknown>>,
101
+ key: string,
102
+ ): string {
103
+ const value = record[key];
104
+ if (typeof value !== "string" || value.length === 0) {
105
+ throw new ClaudeCodeWireError("INVALID_INPUT");
106
+ }
107
+ return value;
108
+ }
109
+
110
+ function parseRuntime(value: unknown): HeaderRuntime {
111
+ if (!isRecord(value)) {
112
+ throw new ClaudeCodeWireError("INVALID_INPUT");
113
+ }
114
+ return {
115
+ sessionId: requiredString(value, "sessionId"),
116
+ runtime: requiredString(value, "runtime"),
117
+ runtimeVersion: requiredString(value, "runtimeVersion"),
118
+ os: requiredString(value, "os"),
119
+ arch: requiredString(value, "arch"),
120
+ };
121
+ }
122
+
123
+ function parseBetaFeatures(value: unknown): readonly string[] {
124
+ if (!Array.isArray(value)) {
125
+ throw new ClaudeCodeWireError("INVALID_INPUT");
126
+ }
127
+ const features: string[] = [];
128
+ for (const feature of value) {
129
+ if (typeof feature !== "string" || feature.length === 0) {
130
+ throw new ClaudeCodeWireError("INVALID_INPUT");
131
+ }
132
+ features.push(feature);
133
+ }
134
+ return features;
135
+ }
136
+
137
+ function parseExtraHeaders(value: unknown): readonly HeaderPair[] {
138
+ if (!Array.isArray(value)) {
139
+ throw new ClaudeCodeWireError("INVALID_INPUT");
140
+ }
141
+ const headers: HeaderPair[] = [];
142
+ for (const candidate of value) {
143
+ if (
144
+ !Array.isArray(candidate) ||
145
+ candidate.length !== 2 ||
146
+ typeof candidate[0] !== "string" ||
147
+ typeof candidate[1] !== "string"
148
+ ) {
149
+ throw new ClaudeCodeWireError("INVALID_INPUT");
150
+ }
151
+ headers.push([candidate[0], candidate[1]]);
152
+ }
153
+ return headers;
154
+ }
155
+
156
+ function parseExtraHeaderPolicy(value: unknown): ClaudeCodeExtraHeaderPolicy {
157
+ if (value !== "strict" && value !== "dropConflicting") {
158
+ throw new ClaudeCodeWireError("INVALID_INPUT");
159
+ }
160
+ return value;
161
+ }
162
+
163
+ function parseProfile(value: unknown): ClaudeCodeProtocolProfile {
164
+ if (value !== CLAUDE_CODE_2_1_195_PROFILE) {
165
+ throw new ClaudeCodeWireError("INVALID_INPUT");
166
+ }
167
+ return CLAUDE_CODE_2_1_195_PROFILE;
168
+ }
169
+
170
+ function parseApp(value: unknown): "cli" | "cli-bg" {
171
+ if (value !== "cli" && value !== "cli-bg") {
172
+ throw new ClaudeCodeWireError("INVALID_INPUT");
173
+ }
174
+ return value;
175
+ }
176
+
177
+ function parseRetryCount(value: unknown): number {
178
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
179
+ throw new ClaudeCodeWireError("INVALID_INPUT");
180
+ }
181
+ return value;
182
+ }
183
+
184
+ function optionalHeaderString(
185
+ input: Readonly<Record<string, unknown>>,
186
+ key: string,
187
+ ): string | undefined {
188
+ const value = input[key];
189
+ if (value === undefined) return undefined;
190
+ if (typeof value !== "string" || value.length === 0) {
191
+ throw new ClaudeCodeWireError("INVALID_INPUT");
192
+ }
193
+ return value;
194
+ }
195
+
196
+ function parseInput(input: unknown): ValidatedInput {
197
+ if (!isRecord(input)) {
198
+ throw new ClaudeCodeWireError("INVALID_INPUT");
199
+ }
200
+ const stainlessHelper = optionalHeaderString(input, "stainlessHelper");
201
+ const claudeRemoteContainerId = optionalHeaderString(
202
+ input,
203
+ "claudeRemoteContainerId",
204
+ );
205
+ const claudeRemoteSessionId = optionalHeaderString(
206
+ input,
207
+ "claudeRemoteSessionId",
208
+ );
209
+ const clientApp = optionalHeaderString(input, "clientApp");
210
+ const anthropicAdditionalProtection = optionalHeaderString(
211
+ input,
212
+ "anthropicAdditionalProtection",
213
+ );
214
+ return {
215
+ accessToken: requiredString(input, "accessToken"),
216
+ runtime: parseRuntime(input["runtime"]),
217
+ clientRequestId: requiredString(input, "clientRequestId"),
218
+ betaFeatures: parseBetaFeatures(input["betaFeatures"]),
219
+ app: parseApp(input["app"] === undefined ? "cli" : input["app"]),
220
+ stainlessRetryCount: parseRetryCount(
221
+ input["stainlessRetryCount"] === undefined
222
+ ? 0
223
+ : input["stainlessRetryCount"],
224
+ ),
225
+ extraHeaders: parseExtraHeaders(input["extraHeaders"] ?? []),
226
+ extraHeaderPolicy: parseExtraHeaderPolicy(
227
+ input["extraHeaderPolicy"] ?? "strict",
228
+ ),
229
+ profile: parseProfile(input["profile"]),
230
+ ...(stainlessHelper === undefined ? {} : { stainlessHelper }),
231
+ ...(claudeRemoteContainerId === undefined
232
+ ? {}
233
+ : { claudeRemoteContainerId }),
234
+ ...(claudeRemoteSessionId === undefined ? {} : { claudeRemoteSessionId }),
235
+ ...(clientApp === undefined ? {} : { clientApp }),
236
+ ...(anthropicAdditionalProtection === undefined
237
+ ? {}
238
+ : { anthropicAdditionalProtection }),
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Names a caller may never place on the wire through `extraHeaders`.
244
+ *
245
+ * Two disjoint reasons, both non-negotiable.
246
+ *
247
+ * CREDENTIAL AND ROUTING DISCLOSURE. `x-api-key`, `cookie`, `set-cookie`,
248
+ * `proxy-*`, `forwarded` and `x-forwarded-*` carry authentication that would
249
+ * contradict the OAuth bearer this package emits, or disclose the caller's
250
+ * network topology upstream.
251
+ *
252
+ * HOP-BY-HOP AND ENTITY HEADERS (RFC 9110 section 7.6.1). `connection`,
253
+ * `transfer-encoding`, `te`, `upgrade`, `keep-alive` and `host` govern a single
254
+ * connection and belong to the transport, not to the caller. `content-length`
255
+ * is the worst of them: this package RECONSTRUCTS the request body canonically,
256
+ * so a length copied from an inbound request describes a different byte string.
257
+ * A wrong `content-length` corrupts the request SILENTLY — no local error is
258
+ * raised, the peer truncates or stalls. Blocking these is a defect fix, valid
259
+ * independently of any consumer.
260
+ */
261
+ const FORBIDDEN_HEADER_NAMES: ReadonlySet<string> = new Set([
262
+ "x-api-key",
263
+ "cookie",
264
+ "set-cookie",
265
+ "forwarded",
266
+ "content-length",
267
+ "host",
268
+ "connection",
269
+ "transfer-encoding",
270
+ "te",
271
+ "upgrade",
272
+ "keep-alive",
273
+ ]);
274
+
275
+ function isForbiddenHeader(name: string): boolean {
276
+ return (
277
+ FORBIDDEN_HEADER_NAMES.has(name) ||
278
+ name.startsWith("proxy-") ||
279
+ name.startsWith("x-forwarded-")
280
+ );
281
+ }
282
+
283
+ function safeDiagnosticName(name: string, accessToken: string): string {
284
+ return name.includes(accessToken) ? "[redacted]" : name;
285
+ }
286
+
287
+ interface ResolvedExtraHeaders {
288
+ readonly kept: readonly HeaderPair[];
289
+ readonly droppedNames: readonly string[];
290
+ }
291
+
292
+ /**
293
+ * Applies the caller policy to the supplied extra headers.
294
+ *
295
+ * `strict` is the original behaviour, unchanged: the first conflict throws, and
296
+ * nothing reaches the wire. `dropConflicting` discards the offending pair and
297
+ * records its lowercased name, so a consumer forwarding a heterogeneous host
298
+ * header map is not defeated by a single header this package owns.
299
+ *
300
+ * The relaxation covers OWNERSHIP conflicts only — a canonical name or a
301
+ * denylisted name. Two guarantees survive in both policies:
302
+ *
303
+ * - Header syntax is validated FIRST and never relaxed. A control character in
304
+ * a name or a value raises `HEADER_INJECTION` whatever the policy says;
305
+ * smuggling is never silently tolerated.
306
+ * - A caller that duplicates one of ITS OWN extra headers still gets
307
+ * `DUPLICATE_HEADER`. That collision is a caller bug, not an ownership
308
+ * conflict this package is entitled to resolve on the caller's behalf.
309
+ */
310
+ function resolveExtraHeaders(
311
+ extraHeaders: readonly HeaderPair[],
312
+ accessToken: string,
313
+ policy: ClaudeCodeExtraHeaderPolicy,
314
+ ): ResolvedExtraHeaders {
315
+ const seenExtras = new Set<string>();
316
+ const kept: HeaderPair[] = [];
317
+ const droppedNames: string[] = [];
318
+ for (const [name, value] of extraHeaders) {
319
+ assertHeaderText(name, value);
320
+ const normalizedName = name.toLowerCase();
321
+ const safeName = safeDiagnosticName(normalizedName, accessToken);
322
+ const ownershipConflict = isForbiddenHeader(normalizedName)
323
+ ? "FORBIDDEN_HEADER"
324
+ : CANONICAL_NAMES.has(normalizedName)
325
+ ? "DUPLICATE_HEADER"
326
+ : undefined;
327
+ if (ownershipConflict !== undefined) {
328
+ if (policy === "strict") {
329
+ throw new ClaudeCodeWireError(ownershipConflict, {
330
+ headerName: safeName,
331
+ });
332
+ }
333
+ droppedNames.push(normalizedName);
334
+ continue;
335
+ }
336
+ if (seenExtras.has(normalizedName)) {
337
+ throw new ClaudeCodeWireError("DUPLICATE_HEADER", {
338
+ headerName: safeName,
339
+ });
340
+ }
341
+ seenExtras.add(normalizedName);
342
+ kept.push([name, value]);
343
+ }
344
+ return { kept, droppedNames };
345
+ }
346
+
347
+ function freezePair(name: string, value: string): HeaderPair {
348
+ const pair: HeaderPair = [name, value];
349
+ return Object.freeze(pair);
350
+ }
351
+
352
+ function assertTokenIsolation(
353
+ pairs: readonly HeaderPair[],
354
+ accessToken: string,
355
+ ): void {
356
+ for (const [name, value] of pairs) {
357
+ if (name !== HEADER_NAMES.authorization && value.includes(accessToken)) {
358
+ throw new ClaudeCodeWireError("INVALID_INPUT");
359
+ }
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Builds the pinned canonical logical header list together with the audit of
365
+ * whatever the extra-header policy discarded.
366
+ *
367
+ * Transport order is not guaranteed.
368
+ */
369
+ export function buildOrderedHeaderPlan(input: unknown): OrderedHeaderPlan {
370
+ const appendExtraHeaders =
371
+ isRecord(input) &&
372
+ (Object.hasOwn(input, "app") ||
373
+ Object.hasOwn(input, "stainlessRetryCount") ||
374
+ Object.hasOwn(input, "stainlessHelper") ||
375
+ Object.hasOwn(input, "claudeRemoteContainerId") ||
376
+ Object.hasOwn(input, "claudeRemoteSessionId") ||
377
+ Object.hasOwn(input, "clientApp") ||
378
+ Object.hasOwn(input, "anthropicAdditionalProtection"));
379
+ const validated = parseInput(input);
380
+ const resolvedExtras = resolveExtraHeaders(
381
+ validated.extraHeaders,
382
+ validated.accessToken,
383
+ validated.extraHeaderPolicy,
384
+ );
385
+
386
+ const beta = validated.betaFeatures.join(",");
387
+ const values = [
388
+ [HEADER_NAMES.anthropicBeta, beta],
389
+ [HEADER_NAMES.browserAccess, "true"],
390
+ [HEADER_NAMES.anthropicVersion, validated.profile.anthropicVersion],
391
+ [HEADER_NAMES.authorization, `Bearer ${validated.accessToken}`],
392
+ [HEADER_NAMES.contentType, "application/json"],
393
+ [HEADER_NAMES.userAgent, validated.profile.userAgent],
394
+ [HEADER_NAMES.app, validated.app],
395
+ [HEADER_NAMES.sessionId, validated.runtime.sessionId],
396
+ [HEADER_NAMES.clientRequestId, validated.clientRequestId],
397
+ [HEADER_NAMES.arch, validated.runtime.arch],
398
+ [HEADER_NAMES.lang, "js"],
399
+ [HEADER_NAMES.os, validated.runtime.os],
400
+ [HEADER_NAMES.packageVersion, validated.profile.sdkVersion],
401
+ [HEADER_NAMES.retryCount, String(validated.stainlessRetryCount)],
402
+ [HEADER_NAMES.runtime, validated.runtime.runtime],
403
+ [HEADER_NAMES.runtimeVersion, validated.runtime.runtimeVersion],
404
+ [HEADER_NAMES.timeout, "600"],
405
+ ] as const;
406
+
407
+ const pairs: HeaderPair[] = [];
408
+ for (const [name, value] of values) {
409
+ assertHeaderText(name, value);
410
+ pairs.push(freezePair(name, value));
411
+ }
412
+ const dynamicValues = [
413
+ [HEADER_NAMES.stainlessHelper, validated.stainlessHelper],
414
+ [HEADER_NAMES.remoteContainerId, validated.claudeRemoteContainerId],
415
+ [HEADER_NAMES.remoteSessionId, validated.claudeRemoteSessionId],
416
+ [HEADER_NAMES.clientApp, validated.clientApp],
417
+ [
418
+ HEADER_NAMES.additionalProtection,
419
+ validated.anthropicAdditionalProtection,
420
+ ],
421
+ ] as const;
422
+ for (const [name, value] of dynamicValues) {
423
+ if (value === undefined) continue;
424
+ assertHeaderText(name, value);
425
+ pairs.push(freezePair(name, value));
426
+ }
427
+ if (appendExtraHeaders) {
428
+ for (const [name, value] of resolvedExtras.kept) {
429
+ pairs.push(freezePair(name, value));
430
+ }
431
+ }
432
+ assertTokenIsolation(pairs, validated.accessToken);
433
+ return Object.freeze({
434
+ headers: Object.freeze(pairs),
435
+ droppedExtraHeaderNames: Object.freeze([...resolvedExtras.droppedNames]),
436
+ });
437
+ }
438
+
439
+ /** Builds the pinned canonical logical header list. Transport order is not guaranteed. */
440
+ export function buildOrderedHeaders(input: unknown): readonly HeaderPair[] {
441
+ return buildOrderedHeaderPlan(input).headers;
442
+ }
package/src/index.ts ADDED
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+
3
+ /**
4
+ * Public entry point for the Claude Code wire compatibility package.
5
+ *
6
+ * Only the surfaces listed below are public. The Wave 2 implementation
7
+ * Internal protocol modules remain private; only the documented builder and
8
+ * parser are exported here.
9
+ *
10
+ * Importing this module has no side effects. It reads no environment, opens
11
+ * no network connection, touches no clock or random source, and holds no
12
+ * mutable module-level state.
13
+ */
14
+
15
+ export type {
16
+ AntiVerbosityPolicy,
17
+ AntiVerbositySection,
18
+ BuiltClaudeCodeCountTokensRequest,
19
+ BuiltClaudeCodeRequest,
20
+ ClaudeCodeBetaOverrides,
21
+ ClaudeCodeBetaPolicy,
22
+ ClaudeCodeCapabilities,
23
+ ClaudeCodeCapabilityDecisions,
24
+ ClaudeCodeCatalogueEntry,
25
+ ClaudeCodeEffort,
26
+ ClaudeCodeExtraHeaderPolicy,
27
+ ClaudeCodeMetadataOverrides,
28
+ ClaudeCodeModelFamily,
29
+ ClaudeCodeProtocolProfile,
30
+ ClaudeCodeCountTokensInput,
31
+ ClaudeCodeRequestInput,
32
+ ClaudeCodeRuntimeIdentity,
33
+ ClaudeCodeWireErrorCode,
34
+ HeaderPair,
35
+ JsonPrimitive,
36
+ JsonValue,
37
+ Message,
38
+ MessageContent,
39
+ RedactedRequestEvidence,
40
+ SystemInput,
41
+ TextBlock,
42
+ ThinkingDisplay,
43
+ ToolDefinition,
44
+ ToolResultBlock,
45
+ ToolUseBlock,
46
+ } from "./contracts.js";
47
+
48
+ export { ClaudeCodeWireError } from "./contracts.js";
49
+
50
+ export {
51
+ DEFAULT_ANTI_VERBOSITY_POLICY,
52
+ antiVerbosityText,
53
+ selectAntiVerbositySection,
54
+ } from "./anti-verbosity.js";
55
+
56
+ export {
57
+ buildClaudeCodeCountTokensRequest,
58
+ buildClaudeCodeRequest,
59
+ parseBuiltClaudeCodeRequest,
60
+ } from "./build-request.js";
61
+
62
+ export { CLAUDE_CODE_2_1_195_PROFILE } from "./profiles/claude-code-2.1.195.js";