@witqq/agent-sdk 0.3.1 → 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/README.md CHANGED
@@ -225,6 +225,7 @@ for await (const event of agent.streamWithContext(messages)) {
225
225
  | `permission_response` | `toolName`, `decision` | Permission decision made |
226
226
  | `ask_user` | `request` | User input requested |
227
227
  | `ask_user_response` | `answer` | User response received |
228
+ | `session_info` | `sessionId`, `transcriptPath?`, `backend` | CLI session metadata (streaming only) |
228
229
  | `usage_update` | `promptTokens`, `completionTokens`, `model?`, `backend?` | Token usage with metadata |
229
230
  | `heartbeat` | — | Keepalive signal during long operations |
230
231
  | `error` | `error`, `recoverable` | Error during execution |
@@ -263,6 +264,39 @@ for await (const event of agent.stream("Run a long analysis")) {
263
264
 
264
265
  When `heartbeatInterval` is set, heartbeat events are emitted during streaming gaps (e.g., while a tool executes). No heartbeats are emitted when backend events flow continuously. The timer is cleaned up when the stream completes, errors, or is aborted.
265
266
 
267
+ ## Persistent Sessions (CLI Backends)
268
+
269
+ CLI backends (Copilot, Claude) create a fresh subprocess session per `run()`/`stream()` call by default. Set `sessionMode: "persistent"` to reuse the same CLI session across calls — the CLI backend maintains conversation history natively:
270
+
271
+ ```typescript
272
+ const agent = service.createAgent({
273
+ systemPrompt: "You are a helpful assistant.",
274
+ sessionMode: "persistent", // reuse CLI session across calls
275
+ });
276
+
277
+ await agent.run("My name is Alice");
278
+ const result = await agent.run("What is my name?");
279
+ // result.output contains "Alice" — history maintained by CLI
280
+
281
+ console.log(agent.sessionId); // CLI session ID for external tracking
282
+ agent.dispose(); // destroys the persistent session
283
+ ```
284
+
285
+ In persistent mode, if a session encounters an error, it is automatically cleared and recreated on the next call. The `sessionId` property exposes the CLI session ID for logging or external storage.
286
+
287
+ ### Interrupting Running Operations
288
+
289
+ Call `interrupt()` to gracefully stop a running operation. For CLI backends, this calls the SDK's interrupt/abort method on the active session:
290
+
291
+ ```typescript
292
+ // In another context (e.g., timeout handler)
293
+ await agent.interrupt();
294
+ ```
295
+
296
+ Default (`"per-call"`): each call creates and destroys a fresh session. Multi-message context is passed via prompt augmentation through `runWithContext()`/`streamWithContext()`.
297
+
298
+ API-based backends (Vercel AI) ignore `sessionMode` — they are stateless by design.
299
+
266
300
  ## Backend-Specific Options
267
301
 
268
302
  ### Copilot
@@ -276,6 +310,7 @@ const service = createCopilotService({
276
310
  workingDirectory: process.cwd(),
277
311
  githubToken: "ghp_...", // optional, alternative to useLoggedInUser
278
312
  cliArgs: ["--allow-all"], // extra CLI flags for the subprocess
313
+ env: { PATH: "/custom/bin" }, // custom env vars for subprocess
279
314
  });
280
315
  ```
281
316
 
@@ -304,11 +339,14 @@ const service = createClaudeService({
304
339
  cliPath: "/path/to/claude", // optional custom CLI path
305
340
  workingDirectory: process.cwd(),
306
341
  maxTurns: 10,
342
+ env: { CLAUDE_CONFIG_DIR: "/custom/config" }, // custom env vars for subprocess
307
343
  });
308
344
  ```
309
345
 
310
346
  `supervisor.onAskUser` is not supported by the Claude backend; a warning is emitted if set.
311
347
 
348
+ When `supervisor.onPermission` is set, the Claude backend automatically sets `permissionMode: "default"` so the CLI invokes the callback instead of using built-in rules.
349
+
312
350
  ### Vercel AI (OpenRouter / OpenAI-compatible)
313
351
 
314
352
  ```typescript
@@ -378,7 +416,7 @@ import { createVercelAIService } from "@witqq/agent-sdk/vercel-ai";
378
416
  | Claude | `claude-sonnet-4-5-20250514` | `sonnet` |
379
417
  | Vercel AI | `anthropic/claude-sonnet-4-5` | (provider-specific) |
380
418
 
381
- Use `service.listModels()` to get available model IDs for each backend.
419
+ Use `service.listModels()` to get available model IDs for each backend. Copilot lists models from GitHub API. Claude queries the Anthropic `/v1/models` endpoint when `oauthToken` is provided (returns empty list without token). Vercel AI queries the provider's `/models` endpoint (returns empty list on failure).
382
420
 
383
421
  ## Build
384
422
 
@@ -388,6 +426,84 @@ npm run test # vitest
388
426
  npm run typecheck # tsc --noEmit
389
427
  ```
390
428
 
429
+ ## Authentication
430
+
431
+ Programmatic OAuth flows for obtaining tokens without manual terminal interaction.
432
+
433
+ ```typescript
434
+ import { CopilotAuth, ClaudeAuth } from "@witqq/agent-sdk/auth";
435
+ ```
436
+
437
+ ### Copilot (GitHub Device Flow)
438
+
439
+ ```typescript
440
+ const auth = new CopilotAuth();
441
+ const { verificationUrl, userCode, waitForToken } = await auth.startDeviceFlow();
442
+
443
+ // Show the user: open verificationUrl and enter userCode
444
+ console.log(`Open ${verificationUrl} and enter code: ${userCode}`);
445
+
446
+ const token = await waitForToken(); // polls until authorized
447
+ // token.accessToken = "gho_..." (long-lived, no expiration)
448
+
449
+ // Use with Copilot backend:
450
+ const service = createCopilotService({ githubToken: token.accessToken });
451
+ ```
452
+
453
+ ### Claude (OAuth + PKCE)
454
+
455
+ ```typescript
456
+ const auth = new ClaudeAuth();
457
+ const { authorizeUrl, completeAuth } = auth.startOAuthFlow();
458
+
459
+ // Open authorizeUrl in browser — user authorizes, gets redirected
460
+ // completeAuth accepts raw code, full redirect URL, or code#state format
461
+ console.log(`Open: ${authorizeUrl}`);
462
+
463
+ const token = await completeAuth(codeOrUrl);
464
+ // token.accessToken = "sk-ant-oat01-..." (expires in 8h, has refreshToken)
465
+
466
+ // Refresh before expiry:
467
+ const refreshed = await auth.refreshToken(token.refreshToken);
468
+
469
+ // Use with Claude backend:
470
+ const service = createClaudeService({ oauthToken: token.accessToken });
471
+ ```
472
+
473
+ ### Token Types
474
+
475
+ ```typescript
476
+ interface AuthToken {
477
+ accessToken: string;
478
+ tokenType: string;
479
+ expiresIn?: number; // seconds until expiry (undefined = long-lived)
480
+ obtainedAt: number; // Date.now() when token was obtained
481
+ }
482
+
483
+ interface ClaudeAuthToken extends AuthToken {
484
+ refreshToken: string; // for refreshing expired tokens
485
+ scopes: string[];
486
+ }
487
+
488
+ interface CopilotAuthToken extends AuthToken {
489
+ login?: string; // GitHub username
490
+ }
491
+ ```
492
+
493
+ ## Interactive Demo
494
+
495
+ A web-based interactive demo showcasing all backends with authentication is in `examples/auth-demo/`.
496
+
497
+ ```bash
498
+ # Run in Docker (web UI at http://localhost:3456)
499
+ docker compose -f examples/auth-demo/docker-compose.yml up
500
+
501
+ # Run locally (CLI mode)
502
+ npx tsx examples/auth-demo/index.ts
503
+ ```
504
+
505
+ Features: provider selection, auth flows (Device Flow, OAuth+PKCE, API key), model selection, streaming chat with conversation history, token persistence across container restarts, and provider switching.
506
+
391
507
  ## License
392
508
 
393
509
  MIT
@@ -0,0 +1,359 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+
5
+ // src/auth/types.ts
6
+ var AuthError = class extends Error {
7
+ constructor(message, options) {
8
+ super(message, options);
9
+ this.name = "AuthError";
10
+ }
11
+ };
12
+ var DeviceCodeExpiredError = class extends AuthError {
13
+ constructor() {
14
+ super("Device code expired. Please restart the auth flow.");
15
+ this.name = "DeviceCodeExpiredError";
16
+ }
17
+ };
18
+ var AccessDeniedError = class extends AuthError {
19
+ constructor() {
20
+ super("Access was denied by the user.");
21
+ this.name = "AccessDeniedError";
22
+ }
23
+ };
24
+ var TokenExchangeError = class extends AuthError {
25
+ constructor(message, options) {
26
+ super(message, options);
27
+ this.name = "TokenExchangeError";
28
+ }
29
+ };
30
+
31
+ // src/auth/copilot-auth.ts
32
+ var CLIENT_ID = "Ov23ctDVkRmgkPke0Mmm";
33
+ var DEVICE_CODE_URL = "https://github.com/login/device/code";
34
+ var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
35
+ var USER_API_URL = "https://api.github.com/user";
36
+ var DEFAULT_SCOPES = "read:user,read:org,repo,gist";
37
+ var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
38
+ var CopilotAuth = class {
39
+ fetchFn;
40
+ /** @param options - Optional configuration with custom fetch for testing */
41
+ constructor(options) {
42
+ this.fetchFn = options?.fetch ?? globalThis.fetch;
43
+ }
44
+ /**
45
+ * Start the GitHub Device Flow.
46
+ * Returns a device code result with user code, verification URL,
47
+ * and a `waitForToken()` function that polls until the user authorizes.
48
+ *
49
+ * @param options - Optional scopes and abort signal
50
+ * @returns Device flow result with user code, verification URL, and waitForToken poller
51
+ * @throws {AuthError} If the device code request fails
52
+ * @throws {DeviceCodeExpiredError} If the device code expires before user authorizes
53
+ * @throws {AccessDeniedError} If the user denies access
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * const auth = new CopilotAuth();
58
+ * const { userCode, verificationUrl, waitForToken } = await auth.startDeviceFlow();
59
+ * console.log(`Open ${verificationUrl} and enter code: ${userCode}`);
60
+ * const token = await waitForToken();
61
+ * ```
62
+ */
63
+ async startDeviceFlow(options) {
64
+ const scopes = options?.scopes ?? DEFAULT_SCOPES;
65
+ const response = await this.fetchFn(DEVICE_CODE_URL, {
66
+ method: "POST",
67
+ headers: {
68
+ "Content-Type": "application/x-www-form-urlencoded",
69
+ Accept: "application/json"
70
+ },
71
+ body: new URLSearchParams({
72
+ client_id: CLIENT_ID,
73
+ scope: scopes
74
+ }),
75
+ signal: options?.signal
76
+ });
77
+ if (!response.ok) {
78
+ throw new AuthError(
79
+ `Failed to request device code: ${response.status} ${response.statusText}`
80
+ );
81
+ }
82
+ const data = await response.json();
83
+ return {
84
+ userCode: data.user_code,
85
+ verificationUrl: data.verification_uri,
86
+ waitForToken: (signal) => this.pollForToken(data.device_code, data.interval, signal)
87
+ };
88
+ }
89
+ async pollForToken(deviceCode, interval, signal) {
90
+ let pollIntervalMs = interval * 1e3;
91
+ while (true) {
92
+ if (signal?.aborted) {
93
+ throw new AuthError("Authentication was aborted.");
94
+ }
95
+ await this.delay(pollIntervalMs, signal);
96
+ const response = await this.fetchFn(ACCESS_TOKEN_URL, {
97
+ method: "POST",
98
+ headers: {
99
+ "Content-Type": "application/x-www-form-urlencoded",
100
+ Accept: "application/json"
101
+ },
102
+ body: new URLSearchParams({
103
+ client_id: CLIENT_ID,
104
+ device_code: deviceCode,
105
+ grant_type: GRANT_TYPE
106
+ }),
107
+ signal
108
+ });
109
+ if (!response.ok) {
110
+ throw new AuthError(
111
+ `Token poll failed: ${response.status} ${response.statusText}`
112
+ );
113
+ }
114
+ const data = await response.json();
115
+ if (data.access_token) {
116
+ const token = {
117
+ accessToken: data.access_token,
118
+ tokenType: data.token_type ?? "bearer",
119
+ obtainedAt: Date.now()
120
+ };
121
+ try {
122
+ const login = await this.fetchUserLogin(data.access_token, signal);
123
+ if (login) token.login = login;
124
+ } catch {
125
+ }
126
+ return token;
127
+ }
128
+ if (data.error === "authorization_pending") {
129
+ continue;
130
+ }
131
+ if (data.error === "slow_down") {
132
+ pollIntervalMs = (data.interval ?? interval + 5) * 1e3;
133
+ continue;
134
+ }
135
+ if (data.error === "expired_token") {
136
+ throw new DeviceCodeExpiredError();
137
+ }
138
+ if (data.error === "access_denied") {
139
+ throw new AccessDeniedError();
140
+ }
141
+ throw new AuthError(
142
+ data.error_description ?? `Unexpected error: ${data.error}`
143
+ );
144
+ }
145
+ }
146
+ async fetchUserLogin(token, signal) {
147
+ const response = await this.fetchFn(USER_API_URL, {
148
+ headers: {
149
+ Authorization: `Bearer ${token}`,
150
+ Accept: "application/json"
151
+ },
152
+ signal
153
+ });
154
+ if (!response.ok) return void 0;
155
+ const user = await response.json();
156
+ return user.login;
157
+ }
158
+ delay(ms, signal) {
159
+ return new Promise((resolve, reject) => {
160
+ const timer = setTimeout(resolve, ms);
161
+ signal?.addEventListener(
162
+ "abort",
163
+ () => {
164
+ clearTimeout(timer);
165
+ reject(new AuthError("Authentication was aborted."));
166
+ },
167
+ { once: true }
168
+ );
169
+ });
170
+ }
171
+ };
172
+ var CLIENT_ID2 = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
173
+ var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
174
+ var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
175
+ var DEFAULT_REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
176
+ var DEFAULT_SCOPES2 = "user:profile user:inference user:sessions:claude_code user:mcp_servers";
177
+ var ClaudeAuth = class _ClaudeAuth {
178
+ fetchFn;
179
+ randomBytes;
180
+ /**
181
+ * @param options - Optional configuration with custom fetch and random bytes for testing
182
+ */
183
+ constructor(options) {
184
+ this.fetchFn = options?.fetch ?? globalThis.fetch;
185
+ this.randomBytes = options?.randomBytes ?? defaultRandomBytes;
186
+ }
187
+ /**
188
+ * Start the Claude OAuth+PKCE flow.
189
+ * Generates PKCE code verifier/challenge and returns an authorize URL
190
+ * plus a `completeAuth(code)` function for token exchange.
191
+ *
192
+ * @param options - Redirect URI and optional scopes
193
+ * @returns OAuth flow result with authorize URL and completeAuth function
194
+ * @throws {AuthError} If PKCE generation fails
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * const auth = new ClaudeAuth();
199
+ * const { authorizeUrl, completeAuth } = auth.startOAuthFlow({
200
+ * redirectUri: "https://platform.claude.com/oauth/code/callback",
201
+ * });
202
+ * console.log(`Open: ${authorizeUrl}`);
203
+ * const token = await completeAuth(authorizationCode);
204
+ * ```
205
+ */
206
+ startOAuthFlow(options) {
207
+ const redirectUri = options?.redirectUri ?? DEFAULT_REDIRECT_URI;
208
+ const scopes = options?.scopes ?? DEFAULT_SCOPES2;
209
+ const codeVerifier = this.generateCodeVerifier();
210
+ const state = this.generateState();
211
+ const authorizeUrl = this.buildAuthorizeUrl(
212
+ redirectUri,
213
+ scopes,
214
+ codeVerifier,
215
+ state
216
+ );
217
+ return {
218
+ authorizeUrl,
219
+ completeAuth: (codeOrUrl) => {
220
+ const code = _ClaudeAuth.extractCode(codeOrUrl);
221
+ return this.exchangeCode(code, codeVerifier, state, redirectUri);
222
+ }
223
+ };
224
+ }
225
+ /**
226
+ * Extract an authorization code from user input.
227
+ * Accepts a raw code string or a full redirect URL containing a `code` query parameter.
228
+ *
229
+ * @param input - Raw authorization code or redirect URL
230
+ * @returns The extracted authorization code
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * ClaudeAuth.extractCode("abc123"); // "abc123"
235
+ * ClaudeAuth.extractCode("https://platform.claude.com/oauth/code/callback?code=abc123&state=xyz"); // "abc123"
236
+ * ```
237
+ */
238
+ static extractCode(input) {
239
+ const trimmed = input.trim();
240
+ try {
241
+ const url = new URL(trimmed);
242
+ const code = url.searchParams.get("code");
243
+ if (code) return code;
244
+ } catch {
245
+ }
246
+ const hashIdx = trimmed.indexOf("#");
247
+ if (hashIdx > 0) {
248
+ return trimmed.substring(0, hashIdx);
249
+ }
250
+ return trimmed;
251
+ }
252
+ /**
253
+ * Refresh an expired Claude token.
254
+ *
255
+ * @param refreshToken - The refresh token from a previous authentication
256
+ * @returns New auth token with refreshed access token
257
+ * @throws {TokenExchangeError} If the refresh request fails
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * const auth = new ClaudeAuth();
262
+ * const newToken = await auth.refreshToken(oldToken.refreshToken);
263
+ * ```
264
+ */
265
+ async refreshToken(refreshToken) {
266
+ const response = await this.fetchFn(TOKEN_URL, {
267
+ method: "POST",
268
+ headers: { "Content-Type": "application/json" },
269
+ body: JSON.stringify({
270
+ grant_type: "refresh_token",
271
+ refresh_token: refreshToken,
272
+ client_id: CLIENT_ID2
273
+ })
274
+ });
275
+ if (!response.ok) {
276
+ const text = await response.text();
277
+ throw new TokenExchangeError(
278
+ `Token refresh failed: ${response.status} ${text}`
279
+ );
280
+ }
281
+ const data = await response.json();
282
+ return this.mapTokenResponse(data);
283
+ }
284
+ generateCodeVerifier() {
285
+ const bytes = this.randomBytes(96);
286
+ return base64Encode(bytes).replace(/\+/g, "~").replace(/=/g, "_").replace(/\//g, "-");
287
+ }
288
+ generateState() {
289
+ const bytes = this.randomBytes(16);
290
+ return hexEncode(bytes);
291
+ }
292
+ buildAuthorizeUrl(redirectUri, scopes, codeVerifier, state) {
293
+ const codeChallenge = this.generateCodeChallengeSync(codeVerifier);
294
+ const url = new URL(AUTHORIZE_URL);
295
+ url.searchParams.set("code", "true");
296
+ url.searchParams.set("client_id", CLIENT_ID2);
297
+ url.searchParams.set("response_type", "code");
298
+ url.searchParams.set("redirect_uri", redirectUri);
299
+ url.searchParams.set("scope", scopes);
300
+ url.searchParams.set("code_challenge", codeChallenge);
301
+ url.searchParams.set("code_challenge_method", "S256");
302
+ url.searchParams.set("state", state);
303
+ return url.toString();
304
+ }
305
+ generateCodeChallengeSync(verifier) {
306
+ const hash = crypto.createHash("sha256").update(verifier).digest("base64");
307
+ return hash.split("=")[0].replace(/\+/g, "-").replace(/\//g, "_");
308
+ }
309
+ async exchangeCode(code, codeVerifier, state, redirectUri) {
310
+ const response = await this.fetchFn(TOKEN_URL, {
311
+ method: "POST",
312
+ headers: { "Content-Type": "application/json" },
313
+ body: JSON.stringify({
314
+ grant_type: "authorization_code",
315
+ code,
316
+ redirect_uri: redirectUri,
317
+ client_id: CLIENT_ID2,
318
+ code_verifier: codeVerifier,
319
+ state
320
+ })
321
+ });
322
+ if (!response.ok) {
323
+ const text = await response.text();
324
+ throw new TokenExchangeError(
325
+ `Token exchange failed: ${response.status} ${text}`
326
+ );
327
+ }
328
+ const data = await response.json();
329
+ return this.mapTokenResponse(data);
330
+ }
331
+ mapTokenResponse(data) {
332
+ return {
333
+ accessToken: data.access_token,
334
+ tokenType: data.token_type ?? "bearer",
335
+ expiresIn: data.expires_in,
336
+ obtainedAt: Date.now(),
337
+ refreshToken: data.refresh_token,
338
+ scopes: data.scope?.split(" ") ?? []
339
+ };
340
+ }
341
+ };
342
+ function defaultRandomBytes(size) {
343
+ return new Uint8Array(crypto.randomBytes(size));
344
+ }
345
+ function base64Encode(bytes) {
346
+ return Buffer.from(bytes).toString("base64");
347
+ }
348
+ function hexEncode(bytes) {
349
+ return Buffer.from(bytes).toString("hex");
350
+ }
351
+
352
+ exports.AccessDeniedError = AccessDeniedError;
353
+ exports.AuthError = AuthError;
354
+ exports.ClaudeAuth = ClaudeAuth;
355
+ exports.CopilotAuth = CopilotAuth;
356
+ exports.DeviceCodeExpiredError = DeviceCodeExpiredError;
357
+ exports.TokenExchangeError = TokenExchangeError;
358
+ //# sourceMappingURL=index.cjs.map
359
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/auth/types.ts","../../src/auth/copilot-auth.ts","../../src/auth/claude-auth.ts"],"names":["CLIENT_ID","DEFAULT_SCOPES","createHash","nodeRandomBytes"],"mappings":"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","file":"index.cjs","sourcesContent":["// ─── Auth Token Types ──────────────────────────────────────────\n\n/**\n * Base auth token returned by all auth providers.\n *\n * @example\n * ```ts\n * import type { AuthToken } from \"@witqq/agent-sdk/auth\";\n *\n * const token: AuthToken = {\n * accessToken: \"gho_abc123...\",\n * tokenType: \"bearer\",\n * obtainedAt: Date.now(),\n * };\n * ```\n */\nexport interface AuthToken {\n /** The access token string */\n accessToken: string;\n /** Token type (e.g. \"bearer\") */\n tokenType: string;\n /** Seconds until token expires (undefined = long-lived) */\n expiresIn?: number;\n /** Timestamp when the token was obtained */\n obtainedAt: number;\n}\n\n/**\n * Copilot-specific token (GitHub OAuth, long-lived).\n *\n * @example\n * ```ts\n * import type { CopilotAuthToken } from \"@witqq/agent-sdk/auth\";\n *\n * const token: CopilotAuthToken = {\n * accessToken: \"gho_abc123...\",\n * tokenType: \"bearer\",\n * obtainedAt: Date.now(),\n * login: \"octocat\",\n * };\n * ```\n */\nexport interface CopilotAuthToken extends AuthToken {\n /** GitHub user login associated with the token */\n login?: string;\n}\n\n/**\n * Claude-specific token (OAuth+PKCE, expires in 8h).\n *\n * @example\n * ```ts\n * import type { ClaudeAuthToken } from \"@witqq/agent-sdk/auth\";\n *\n * const token: ClaudeAuthToken = {\n * accessToken: \"sk-ant-oat01-...\",\n * tokenType: \"bearer\",\n * expiresIn: 28800,\n * obtainedAt: Date.now(),\n * refreshToken: \"sk-ant-rt01-...\",\n * scopes: [\"user:inference\", \"user:profile\"],\n * };\n * ```\n */\nexport interface ClaudeAuthToken extends AuthToken {\n /** Refresh token for obtaining new access tokens */\n refreshToken: string;\n /** OAuth scopes granted */\n scopes: string[];\n}\n\n// ─── Device Flow Types (Copilot) ──────────────────────────────\n\n/**\n * Result of initiating a GitHub Device Flow.\n *\n * @example\n * ```ts\n * import { CopilotAuth } from \"@witqq/agent-sdk/auth\";\n *\n * const auth = new CopilotAuth();\n * const { userCode, verificationUrl, waitForToken } = await auth.startDeviceFlow();\n * console.log(`Open ${verificationUrl} and enter code: ${userCode}`);\n * const token = await waitForToken();\n * ```\n */\nexport interface DeviceFlowResult {\n /** The code the user must enter at the verification URL */\n userCode: string;\n /** URL where the user enters the code */\n verificationUrl: string;\n /** Polls GitHub until user authorizes; resolves with token */\n waitForToken: (signal?: AbortSignal) => Promise<CopilotAuthToken>;\n}\n\n// ─── OAuth Flow Types (Claude) ────────────────────────────────\n\n/** Options for starting a Claude OAuth flow */\nexport interface OAuthFlowOptions {\n /** The redirect URI registered with the OAuth app */\n redirectUri?: string;\n /** OAuth scopes to request (defaults to user:profile user:inference) */\n scopes?: string;\n}\n\n/**\n * Result of initiating a Claude OAuth flow.\n *\n * @example\n * ```ts\n * import type { OAuthFlowResult } from \"@witqq/agent-sdk/auth\";\n *\n * const result: OAuthFlowResult = {\n * authorizeUrl: \"https://claude.ai/oauth/authorize?...\",\n * completeAuth: async (code) => ({ ... }),\n * };\n * // Open result.authorizeUrl in browser, get code from redirect\n * const token = await result.completeAuth(code);\n * ```\n */\nexport interface OAuthFlowResult {\n /** URL to open in browser for user authorization */\n authorizeUrl: string;\n /** Exchange the authorization code (or full redirect URL) for tokens */\n completeAuth: (codeOrUrl: string) => Promise<ClaudeAuthToken>;\n}\n\n// ─── Auth Errors ──────────────────────────────────────────────\n\n/** Base error for auth operations.\n * @param message - Error description\n * @param options - Standard ErrorOptions (e.g. cause)\n */\nexport class AuthError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"AuthError\";\n }\n}\n\n/** Device code expired before user authorized */\nexport class DeviceCodeExpiredError extends AuthError {\n constructor() {\n super(\"Device code expired. Please restart the auth flow.\");\n this.name = \"DeviceCodeExpiredError\";\n }\n}\n\n/** User denied access during OAuth flow */\nexport class AccessDeniedError extends AuthError {\n constructor() {\n super(\"Access was denied by the user.\");\n this.name = \"AccessDeniedError\";\n }\n}\n\n/** Token exchange or refresh failed.\n * @param message - Error description\n * @param options - Standard ErrorOptions (e.g. cause)\n */\nexport class TokenExchangeError extends AuthError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"TokenExchangeError\";\n }\n}\n","import type { CopilotAuthToken, DeviceFlowResult } from \"./types.js\";\nimport {\n AuthError,\n DeviceCodeExpiredError,\n AccessDeniedError,\n} from \"./types.js\";\n\nconst CLIENT_ID = \"Ov23ctDVkRmgkPke0Mmm\";\nconst DEVICE_CODE_URL = \"https://github.com/login/device/code\";\nconst ACCESS_TOKEN_URL = \"https://github.com/login/oauth/access_token\";\nconst USER_API_URL = \"https://api.github.com/user\";\nconst DEFAULT_SCOPES = \"read:user,read:org,repo,gist\";\nconst GRANT_TYPE = \"urn:ietf:params:oauth:grant-type:device_code\";\n\ninterface DeviceCodeResponse {\n device_code: string;\n user_code: string;\n verification_uri: string;\n expires_in: number;\n interval: number;\n}\n\ninterface TokenPollResponse {\n access_token?: string;\n token_type?: string;\n scope?: string;\n error?: string;\n error_description?: string;\n interval?: number;\n}\n\ninterface GitHubUser {\n login: string;\n}\n\n/** Fetch function type for dependency injection in tests */\ntype FetchFn = typeof globalThis.fetch;\n\n/**\n * Programmatic GitHub Device Flow authentication for Copilot SDK.\n *\n * @example\n * ```ts\n * const auth = new CopilotAuth();\n * const flow = await auth.startDeviceFlow();\n * console.log(`Open ${flow.verificationUrl} and enter ${flow.userCode}`);\n * const token = await flow.waitForToken();\n * // Use token.accessToken with CopilotBackendOptions.githubToken\n * ```\n */\nexport class CopilotAuth {\n private readonly fetchFn: FetchFn;\n\n /** @param options - Optional configuration with custom fetch for testing */\n constructor(options?: { fetch?: FetchFn }) {\n this.fetchFn = options?.fetch ?? globalThis.fetch;\n }\n\n /**\n * Start the GitHub Device Flow.\n * Returns a device code result with user code, verification URL,\n * and a `waitForToken()` function that polls until the user authorizes.\n *\n * @param options - Optional scopes and abort signal\n * @returns Device flow result with user code, verification URL, and waitForToken poller\n * @throws {AuthError} If the device code request fails\n * @throws {DeviceCodeExpiredError} If the device code expires before user authorizes\n * @throws {AccessDeniedError} If the user denies access\n *\n * @example\n * ```ts\n * const auth = new CopilotAuth();\n * const { userCode, verificationUrl, waitForToken } = await auth.startDeviceFlow();\n * console.log(`Open ${verificationUrl} and enter code: ${userCode}`);\n * const token = await waitForToken();\n * ```\n */\n async startDeviceFlow(options?: {\n scopes?: string;\n signal?: AbortSignal;\n }): Promise<DeviceFlowResult> {\n const scopes = options?.scopes ?? DEFAULT_SCOPES;\n const response = await this.fetchFn(DEVICE_CODE_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: new URLSearchParams({\n client_id: CLIENT_ID,\n scope: scopes,\n }),\n signal: options?.signal,\n });\n\n if (!response.ok) {\n throw new AuthError(\n `Failed to request device code: ${response.status} ${response.statusText}`,\n );\n }\n\n const data = (await response.json()) as DeviceCodeResponse;\n\n return {\n userCode: data.user_code,\n verificationUrl: data.verification_uri,\n waitForToken: (signal?: AbortSignal) =>\n this.pollForToken(data.device_code, data.interval, signal),\n };\n }\n\n private async pollForToken(\n deviceCode: string,\n interval: number,\n signal?: AbortSignal,\n ): Promise<CopilotAuthToken> {\n let pollIntervalMs = interval * 1000;\n\n while (true) {\n if (signal?.aborted) {\n throw new AuthError(\"Authentication was aborted.\");\n }\n\n await this.delay(pollIntervalMs, signal);\n\n const response = await this.fetchFn(ACCESS_TOKEN_URL, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: new URLSearchParams({\n client_id: CLIENT_ID,\n device_code: deviceCode,\n grant_type: GRANT_TYPE,\n }),\n signal,\n });\n\n if (!response.ok) {\n throw new AuthError(\n `Token poll failed: ${response.status} ${response.statusText}`,\n );\n }\n\n const data = (await response.json()) as TokenPollResponse;\n\n if (data.access_token) {\n const token: CopilotAuthToken = {\n accessToken: data.access_token,\n tokenType: data.token_type ?? \"bearer\",\n obtainedAt: Date.now(),\n };\n\n // Try to fetch user login\n try {\n const login = await this.fetchUserLogin(data.access_token, signal);\n if (login) token.login = login;\n } catch {\n // Non-critical: login is optional\n }\n\n return token;\n }\n\n if (data.error === \"authorization_pending\") {\n continue;\n }\n\n if (data.error === \"slow_down\") {\n pollIntervalMs = (data.interval ?? interval + 5) * 1000;\n continue;\n }\n\n if (data.error === \"expired_token\") {\n throw new DeviceCodeExpiredError();\n }\n\n if (data.error === \"access_denied\") {\n throw new AccessDeniedError();\n }\n\n throw new AuthError(\n data.error_description ?? `Unexpected error: ${data.error}`,\n );\n }\n }\n\n private async fetchUserLogin(\n token: string,\n signal?: AbortSignal,\n ): Promise<string | undefined> {\n const response = await this.fetchFn(USER_API_URL, {\n headers: {\n Authorization: `Bearer ${token}`,\n Accept: \"application/json\",\n },\n signal,\n });\n\n if (!response.ok) return undefined;\n\n const user = (await response.json()) as GitHubUser;\n return user.login;\n }\n\n private delay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n reject(new AuthError(\"Authentication was aborted.\"));\n },\n { once: true },\n );\n });\n }\n}\n","import type { ClaudeAuthToken, OAuthFlowResult, OAuthFlowOptions } from \"./types.js\";\nimport { TokenExchangeError } from \"./types.js\";\nimport { createHash, randomBytes as nodeRandomBytes } from \"node:crypto\";\n\nconst CLIENT_ID = \"9d1c250a-e61b-44d9-88ed-5944d1962f5e\";\nconst AUTHORIZE_URL = \"https://claude.ai/oauth/authorize\";\nconst TOKEN_URL = \"https://platform.claude.com/v1/oauth/token\";\nconst DEFAULT_REDIRECT_URI =\n \"https://platform.claude.com/oauth/code/callback\";\nconst DEFAULT_SCOPES =\n \"user:profile user:inference user:sessions:claude_code user:mcp_servers\";\n\n/** Fetch function type for dependency injection in tests */\ntype FetchFn = typeof globalThis.fetch;\n\n/** Random bytes generator for dependency injection in tests */\ntype RandomBytesFn = (size: number) => Uint8Array;\n\n/**\n * Programmatic OAuth+PKCE authentication for Claude SDK.\n *\n * @example\n * ```ts\n * const auth = new ClaudeAuth();\n * const { authorizeUrl, completeAuth } = auth.startOAuthFlow({\n * redirectUri: \"https://platform.claude.com/oauth/code/callback\",\n * });\n * // Open authorizeUrl in browser, get code from redirect\n * const token = await completeAuth(code);\n * // Use token with ClaudeBackendOptions: env.CLAUDE_CODE_OAUTH_TOKEN\n * ```\n */\nexport class ClaudeAuth {\n private readonly fetchFn: FetchFn;\n private readonly randomBytes: RandomBytesFn;\n\n /**\n * @param options - Optional configuration with custom fetch and random bytes for testing\n */\n constructor(options?: {\n fetch?: FetchFn;\n randomBytes?: RandomBytesFn;\n }) {\n this.fetchFn = options?.fetch ?? globalThis.fetch;\n this.randomBytes = options?.randomBytes ?? defaultRandomBytes;\n }\n\n /**\n * Start the Claude OAuth+PKCE flow.\n * Generates PKCE code verifier/challenge and returns an authorize URL\n * plus a `completeAuth(code)` function for token exchange.\n *\n * @param options - Redirect URI and optional scopes\n * @returns OAuth flow result with authorize URL and completeAuth function\n * @throws {AuthError} If PKCE generation fails\n *\n * @example\n * ```ts\n * const auth = new ClaudeAuth();\n * const { authorizeUrl, completeAuth } = auth.startOAuthFlow({\n * redirectUri: \"https://platform.claude.com/oauth/code/callback\",\n * });\n * console.log(`Open: ${authorizeUrl}`);\n * const token = await completeAuth(authorizationCode);\n * ```\n */\n startOAuthFlow(options?: OAuthFlowOptions): OAuthFlowResult {\n const redirectUri = options?.redirectUri ?? DEFAULT_REDIRECT_URI;\n const scopes = options?.scopes ?? DEFAULT_SCOPES;\n\n const codeVerifier = this.generateCodeVerifier();\n const state = this.generateState();\n\n const authorizeUrl = this.buildAuthorizeUrl(\n redirectUri,\n scopes,\n codeVerifier,\n state,\n );\n\n return {\n authorizeUrl,\n completeAuth: (codeOrUrl: string) => {\n const code = ClaudeAuth.extractCode(codeOrUrl);\n return this.exchangeCode(code, codeVerifier, state, redirectUri);\n },\n };\n }\n\n /**\n * Extract an authorization code from user input.\n * Accepts a raw code string or a full redirect URL containing a `code` query parameter.\n *\n * @param input - Raw authorization code or redirect URL\n * @returns The extracted authorization code\n *\n * @example\n * ```ts\n * ClaudeAuth.extractCode(\"abc123\"); // \"abc123\"\n * ClaudeAuth.extractCode(\"https://platform.claude.com/oauth/code/callback?code=abc123&state=xyz\"); // \"abc123\"\n * ```\n */\n static extractCode(input: string): string {\n const trimmed = input.trim();\n try {\n const url = new URL(trimmed);\n const code = url.searchParams.get(\"code\");\n if (code) return code;\n } catch {\n // Not a URL — treat as raw code\n }\n // Handle code#state format from redirect page\n const hashIdx = trimmed.indexOf(\"#\");\n if (hashIdx > 0) {\n return trimmed.substring(0, hashIdx);\n }\n return trimmed;\n }\n\n /**\n * Refresh an expired Claude token.\n *\n * @param refreshToken - The refresh token from a previous authentication\n * @returns New auth token with refreshed access token\n * @throws {TokenExchangeError} If the refresh request fails\n *\n * @example\n * ```ts\n * const auth = new ClaudeAuth();\n * const newToken = await auth.refreshToken(oldToken.refreshToken);\n * ```\n */\n async refreshToken(refreshToken: string): Promise<ClaudeAuthToken> {\n const response = await this.fetchFn(TOKEN_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n client_id: CLIENT_ID,\n }),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new TokenExchangeError(\n `Token refresh failed: ${response.status} ${text}`,\n );\n }\n\n const data = (await response.json()) as TokenResponse;\n return this.mapTokenResponse(data);\n }\n\n private generateCodeVerifier(): string {\n const bytes = this.randomBytes(96);\n return base64Encode(bytes)\n .replace(/\\+/g, \"~\")\n .replace(/=/g, \"_\")\n .replace(/\\//g, \"-\");\n }\n\n private generateState(): string {\n const bytes = this.randomBytes(16);\n return hexEncode(bytes);\n }\n\n private buildAuthorizeUrl(\n redirectUri: string,\n scopes: string,\n codeVerifier: string,\n state: string,\n ): string {\n const codeChallenge = this.generateCodeChallengeSync(codeVerifier);\n\n const url = new URL(AUTHORIZE_URL);\n url.searchParams.set(\"code\", \"true\");\n url.searchParams.set(\"client_id\", CLIENT_ID);\n url.searchParams.set(\"response_type\", \"code\");\n url.searchParams.set(\"redirect_uri\", redirectUri);\n url.searchParams.set(\"scope\", scopes);\n url.searchParams.set(\"code_challenge\", codeChallenge);\n url.searchParams.set(\"code_challenge_method\", \"S256\");\n url.searchParams.set(\"state\", state);\n\n return url.toString();\n }\n\n private generateCodeChallengeSync(verifier: string): string {\n const hash = createHash(\"sha256\").update(verifier).digest(\"base64\");\n return hash.split(\"=\")[0].replace(/\\+/g, \"-\").replace(/\\//g, \"_\");\n }\n\n private async exchangeCode(\n code: string,\n codeVerifier: string,\n state: string,\n redirectUri: string,\n ): Promise<ClaudeAuthToken> {\n const response = await this.fetchFn(TOKEN_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n client_id: CLIENT_ID,\n code_verifier: codeVerifier,\n state,\n }),\n });\n\n if (!response.ok) {\n const text = await response.text();\n throw new TokenExchangeError(\n `Token exchange failed: ${response.status} ${text}`,\n );\n }\n\n const data = (await response.json()) as TokenResponse;\n return this.mapTokenResponse(data);\n }\n\n private mapTokenResponse(data: TokenResponse): ClaudeAuthToken {\n return {\n accessToken: data.access_token,\n tokenType: data.token_type ?? \"bearer\",\n expiresIn: data.expires_in,\n obtainedAt: Date.now(),\n refreshToken: data.refresh_token,\n scopes: data.scope?.split(\" \") ?? [],\n };\n }\n}\n\n// ─── Internal Types ───────────────────────────────────────────\n\ninterface TokenResponse {\n access_token: string;\n token_type?: string;\n expires_in?: number;\n scope?: string;\n refresh_token: string;\n}\n\n// ─── Utility Functions ────────────────────────────────────────\n\nfunction defaultRandomBytes(size: number): Uint8Array {\n return new Uint8Array(nodeRandomBytes(size));\n}\n\nfunction base64Encode(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString(\"base64\");\n}\n\nfunction hexEncode(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString(\"hex\");\n}\n"]}