@oh-my-pi/pi-ai 6.8.1 → 6.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "6.8.1",
3
+ "version": "6.8.2",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -17,7 +17,7 @@
17
17
  "test": "bun test"
18
18
  },
19
19
  "dependencies": {
20
- "@oh-my-pi/pi-utils": "6.8.1",
20
+ "@oh-my-pi/pi-utils": "6.8.2",
21
21
  "@anthropic-ai/sdk": "0.71.2",
22
22
  "@aws-sdk/client-bedrock-runtime": "^3.968.0",
23
23
  "@bufbuild/protobuf": "^2.10.2",
@@ -753,7 +753,12 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli"> = (
753
753
 
754
754
  if (emptyAttempt > 0) {
755
755
  const backoffMs = EMPTY_STREAM_BASE_DELAY_MS * 2 ** (emptyAttempt - 1);
756
- await abortableSleep(backoffMs, options?.signal);
756
+ try {
757
+ await abortableSleep(backoffMs, options?.signal);
758
+ } catch {
759
+ // Normalize AbortError to expected message for consistent error handling
760
+ throw new Error("Request was aborted");
761
+ }
757
762
 
758
763
  if (!requestUrl) {
759
764
  throw new Error("Missing request URL");
package/src/storage.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Database } from "bun:sqlite";
7
- import { existsSync, mkdirSync } from "node:fs";
7
+ import { chmodSync, existsSync, mkdirSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import { dirname, join } from "node:path";
10
10
  import type { OAuthCredentials } from "./utils/oauth/types";
@@ -88,13 +88,19 @@ export class CliAuthStorage {
88
88
  private deleteByProviderStmt: ReturnType<Database["prepare"]>;
89
89
 
90
90
  constructor(dbPath: string = getAgentDbPath()) {
91
- // Ensure directory exists
91
+ // Ensure directory exists with secure permissions
92
92
  const dir = dirname(dbPath);
93
93
  if (!existsSync(dir)) {
94
- mkdirSync(dir, { recursive: true });
94
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
95
95
  }
96
96
 
97
97
  this.db = new Database(dbPath);
98
+ // Harden database file permissions to prevent credential leakage
99
+ try {
100
+ chmodSync(dbPath, 0o600);
101
+ } catch {
102
+ // Ignore chmod failures (e.g., Windows)
103
+ }
98
104
  this.initializeSchema();
99
105
 
100
106
  this.insertStmt = this.db.prepare(
@@ -197,17 +197,24 @@ export abstract class OAuthCallbackFlow {
197
197
  });
198
198
 
199
199
  // Manual input race (if supported)
200
+ // Errors from manual input should not abort the flow - only successful input wins the race
200
201
  if (this.ctrl.onManualCodeInput) {
201
- const manualPromise = this.ctrl.onManualCodeInput().then((input): CallbackResult => {
202
- const parsed = parseCallbackInput(input);
203
- if (!parsed.code) {
204
- throw new Error("No authorization code found in input");
205
- }
206
- if (expectedState && parsed.state && parsed.state !== expectedState) {
207
- throw new Error("State mismatch - possible CSRF attack");
208
- }
209
- return { code: parsed.code, state: parsed.state ?? "" };
210
- });
202
+ const manualPromise = this.ctrl
203
+ .onManualCodeInput()
204
+ .then((input): CallbackResult => {
205
+ const parsed = parseCallbackInput(input);
206
+ if (!parsed.code) {
207
+ throw new Error("No authorization code found in input");
208
+ }
209
+ if (expectedState && parsed.state && parsed.state !== expectedState) {
210
+ throw new Error("State mismatch - possible CSRF attack");
211
+ }
212
+ return { code: parsed.code, state: parsed.state ?? "" };
213
+ })
214
+ .catch((): Promise<CallbackResult> => {
215
+ // On manual input error, wait forever - let callback or abort signal win
216
+ return new Promise(() => {});
217
+ });
211
218
 
212
219
  return Promise.race([callbackPromise, manualPromise]);
213
220
  }
@@ -174,20 +174,32 @@ async function pollForGitHubAccessToken(
174
174
  if (raw && typeof raw === "object" && typeof (raw as DeviceTokenErrorResponse).error === "string") {
175
175
  const err = (raw as DeviceTokenErrorResponse).error;
176
176
  if (err === "authorization_pending") {
177
- await abortableSleep(intervalMs, signal);
177
+ try {
178
+ await abortableSleep(intervalMs, signal);
179
+ } catch {
180
+ throw new Error("Login cancelled");
181
+ }
178
182
  continue;
179
183
  }
180
184
 
181
185
  if (err === "slow_down") {
182
186
  intervalMs += 5000;
183
- await abortableSleep(intervalMs, signal);
187
+ try {
188
+ await abortableSleep(intervalMs, signal);
189
+ } catch {
190
+ throw new Error("Login cancelled");
191
+ }
184
192
  continue;
185
193
  }
186
194
 
187
195
  throw new Error(`Device flow failed: ${err}`);
188
196
  }
189
197
 
190
- await abortableSleep(intervalMs, signal);
198
+ try {
199
+ await abortableSleep(intervalMs, signal);
200
+ } catch {
201
+ throw new Error("Login cancelled");
202
+ }
191
203
  }
192
204
 
193
205
  throw new Error("Device flow timed out");
@@ -2,7 +2,7 @@
2
2
  * OpenAI Codex (ChatGPT OAuth) flow
3
3
  */
4
4
 
5
- import { OAuthCallbackFlow, parseCallbackInput } from "./callback-server";
5
+ import { OAuthCallbackFlow } from "./callback-server";
6
6
  import { generatePKCE } from "./pkce";
7
7
  import type { OAuthController, OAuthCredentials } from "./types";
8
8
 
@@ -125,28 +125,8 @@ async function exchangeCodeForToken(code: string, verifier: string, redirectUri:
125
125
  export async function loginOpenAICodex(ctrl: OAuthController): Promise<OAuthCredentials> {
126
126
  const pkce = await generatePKCE();
127
127
  const flow = new OpenAICodexOAuthFlow(ctrl, pkce);
128
- const redirectUri = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
129
128
 
130
- try {
131
- return await flow.login();
132
- } catch (error) {
133
- if (!ctrl.onPrompt) {
134
- throw error;
135
- }
136
-
137
- ctrl.onProgress?.("Callback server failed, falling back to manual input");
138
-
139
- const input = await ctrl.onPrompt({
140
- message: "Paste the authorization code (or full redirect URL):",
141
- });
142
-
143
- const parsed = parseCallbackInput(input);
144
- if (!parsed.code) {
145
- throw new Error("No authorization code found in input");
146
- }
147
-
148
- return exchangeCodeForToken(parsed.code, pkce.verifier, redirectUri);
149
- }
129
+ return flow.login();
150
130
  }
151
131
 
152
132
  /**