@oh-my-pi/pi-ai 13.16.2 → 13.16.3

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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.16.3] - 2026-03-28
6
+ ### Changed
7
+
8
+ - Modified OAuth credential saving to preserve unrelated identities instead of replacing all credentials for a provider
9
+ - Updated credential identity resolution to use provider context for more accurate email deduplication
10
+
11
+ ### Fixed
12
+
13
+ - Fixed OAuth credential updates to replace matching credentials in-place rather than creating disabled rows, preventing unbounded accumulation of soft-deleted credentials
14
+
5
15
  ## [13.15.0] - 2026-03-23
6
16
 
7
17
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "13.16.2",
4
+ "version": "13.16.3",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,7 +41,7 @@
41
41
  "@aws-sdk/client-bedrock-runtime": "^3",
42
42
  "@bufbuild/protobuf": "^2.11",
43
43
  "@google/genai": "^1.43",
44
- "@oh-my-pi/pi-utils": "13.16.2",
44
+ "@oh-my-pi/pi-utils": "13.16.3",
45
45
  "@sinclair/typebox": "^0.34",
46
46
  "@smithy/node-http-handler": "^4.4",
47
47
  "ajv": "^8.18",
@@ -656,6 +656,15 @@ export class AuthStorage {
656
656
  this.#resetProviderAssignments(provider);
657
657
  }
658
658
 
659
+ async #upsertOAuthCredential(provider: string, credential: OAuthCredential): Promise<void> {
660
+ const stored = this.#store.upsertAuthCredentialForProvider(provider, credential);
661
+ this.#setStoredCredentials(
662
+ provider,
663
+ stored.map(record => ({ id: record.id, credential: record.credential })),
664
+ );
665
+ this.#resetProviderAssignments(provider);
666
+ }
667
+
659
668
  /**
660
669
  * Remove credential for a provider.
661
670
  */
@@ -945,12 +954,7 @@ export class AuthStorage {
945
954
  }
946
955
  }
947
956
  const newCredential: OAuthCredential = { type: "oauth", ...credentials };
948
- const existing = this.#getCredentialsForProvider(provider);
949
- if (existing.length === 0) {
950
- await this.set(provider, newCredential);
951
- return;
952
- }
953
- await this.set(provider, [...existing, newCredential]);
957
+ await this.#upsertOAuthCredential(provider, newCredential);
954
958
  }
955
959
 
956
960
  /**
@@ -1975,7 +1979,7 @@ function normalizeStoredIdentityKey(identityKey: string | null | undefined): str
1975
1979
  return normalized && normalized.length > 0 ? normalized : null;
1976
1980
  }
1977
1981
 
1978
- function serializeCredential(credential: AuthCredential): SerializedCredentialRecord | null {
1982
+ function serializeCredential(provider: string, credential: AuthCredential): SerializedCredentialRecord | null {
1979
1983
  if (credential.type === "api_key") {
1980
1984
  return {
1981
1985
  credentialType: "api_key",
@@ -1988,7 +1992,7 @@ function serializeCredential(credential: AuthCredential): SerializedCredentialRe
1988
1992
  return {
1989
1993
  credentialType: "oauth",
1990
1994
  data: JSON.stringify(rest),
1991
- identityKey: resolveCredentialIdentityKey("", credential),
1995
+ identityKey: resolveCredentialIdentityKey(provider, credential),
1992
1996
  };
1993
1997
  }
1994
1998
  return null;
@@ -2423,7 +2427,7 @@ export class AuthCredentialStore {
2423
2427
  const matchedExistingIds = new Set<number>();
2424
2428
 
2425
2429
  for (const credential of items) {
2426
- const serialized = serializeCredential(credential);
2430
+ const serialized = serializeCredential(providerName, credential);
2427
2431
  if (!serialized) continue;
2428
2432
  const match = existing.find(
2429
2433
  entry =>
@@ -2461,6 +2465,53 @@ export class AuthCredentialStore {
2461
2465
  return result;
2462
2466
  }
2463
2467
 
2468
+ upsertAuthCredentialForProvider(provider: string, credential: AuthCredential): StoredAuthCredential[] {
2469
+ const upsert = this.#db.transaction((providerName: string, item: AuthCredential) => {
2470
+ const serialized = serializeCredential(providerName, item);
2471
+ if (!serialized) return this.listAuthCredentials(providerName);
2472
+ const existingRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
2473
+ const existing = existingRows.map(row => ({
2474
+ id: row.id,
2475
+ credential: deserializeCredential(row),
2476
+ identityKey: resolveRowCredentialIdentityKey(providerName, row),
2477
+ }));
2478
+
2479
+ let targetId: number | null = null;
2480
+ for (const row of existing) {
2481
+ if (!matchesReplacementCredential(providerName, row.credential, row.identityKey, item)) continue;
2482
+ if (targetId === null) {
2483
+ targetId = row.id;
2484
+ this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, row.id);
2485
+ continue;
2486
+ }
2487
+ this.#deleteStmt.run("replaced by newer credential", row.id);
2488
+ }
2489
+
2490
+ if (targetId === null) {
2491
+ const row = this.#insertStmt.get(
2492
+ providerName,
2493
+ serialized.credentialType,
2494
+ serialized.data,
2495
+ serialized.identityKey,
2496
+ ) as { id?: number } | undefined;
2497
+ targetId = row?.id ?? null;
2498
+ }
2499
+
2500
+ const activeRows = this.#listActiveByProviderStmt.all(providerName) as AuthRow[];
2501
+ const result: StoredAuthCredential[] = [];
2502
+ for (const row of activeRows) {
2503
+ const activeCredential = deserializeCredential(row);
2504
+ if (!activeCredential) continue;
2505
+ result.push(toStoredAuthCredential(row, activeCredential));
2506
+ }
2507
+ return result;
2508
+ });
2509
+
2510
+ const result = upsert(provider, credential);
2511
+ this.#purgeSupersededDisabledRows(provider, result);
2512
+ return result;
2513
+ }
2514
+
2464
2515
  /**
2465
2516
  * Hard-deletes disabled rows for a provider when an active row with the same identity exists.
2466
2517
  * This prevents unbounded accumulation of soft-deleted credentials while preserving
@@ -2488,15 +2539,16 @@ export class AuthCredentialStore {
2488
2539
  }
2489
2540
 
2490
2541
  updateAuthCredential(id: number, credential: AuthCredential): void {
2491
- const serialized = serializeCredential(credential);
2492
- if (!serialized) return;
2493
2542
  try {
2494
- this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, id);
2495
2543
  const providerRow = this.#db.prepare("SELECT provider FROM auth_credentials WHERE id = ?").get(id) as
2496
2544
  | { provider?: string }
2497
2545
  | undefined;
2498
- if (providerRow?.provider) {
2499
- this.#purgeSupersededDisabledRows(providerRow.provider, this.listAuthCredentials(providerRow.provider));
2546
+ const provider = providerRow?.provider ?? "";
2547
+ const serialized = serializeCredential(provider, credential);
2548
+ if (!serialized) return;
2549
+ this.#updateStmt.run(serialized.credentialType, serialized.data, serialized.identityKey, id);
2550
+ if (provider) {
2551
+ this.#purgeSupersededDisabledRows(provider, this.listAuthCredentials(provider));
2500
2552
  }
2501
2553
  } catch {
2502
2554
  // Ignore update failures
@@ -2547,11 +2599,12 @@ export class AuthCredentialStore {
2547
2599
  // ─── Convenience methods for CLI ────────────────────────────────────────
2548
2600
 
2549
2601
  /**
2550
- * Save OAuth credentials for a provider (replaces existing).
2602
+ * Save OAuth credentials for a provider.
2603
+ * Preserves unrelated identities and replaces only the matching credential.
2551
2604
  */
2552
2605
  saveOAuth(provider: string, credentials: OAuthCredentials): void {
2553
2606
  const credential: AuthCredential = { type: "oauth", ...credentials };
2554
- this.replaceAuthCredentialsForProvider(provider, [credential]);
2607
+ this.upsertAuthCredentialForProvider(provider, credential);
2555
2608
  }
2556
2609
 
2557
2610
  /**