@sunerpy/opencode-kiro-auth 0.6.0 → 0.6.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/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@sunerpy/opencode-kiro-auth)](https://www.npmjs.com/package/@sunerpy/opencode-kiro-auth)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@sunerpy/opencode-kiro-auth)](https://www.npmjs.com/package/@sunerpy/opencode-kiro-auth)
7
+ [![codecov](https://codecov.io/gh/sunerpy/opencode-kiro-auth/branch/main/graph/badge.svg)](https://codecov.io/gh/sunerpy/opencode-kiro-auth)
7
8
  [![license](https://img.shields.io/npm/l/@sunerpy/opencode-kiro-auth)](https://www.npmjs.com/package/@sunerpy/opencode-kiro-auth)
8
9
 
9
10
  > OpenCode plugin that lets OpenCode use AWS Kiro (CodeWhisperer) as a model
@@ -21,6 +21,12 @@ export declare class RequestHandler {
21
21
  private enqueueKiroRequest;
22
22
  private handleKiroRequest;
23
23
  private extractModel;
24
+ /**
25
+ * Seam over the module-level SDK client factory so tests can inject a fake
26
+ * client without a real network call or a leaky module mock. Behavior is
27
+ * identical to calling createSdkClient directly.
28
+ */
29
+ private makeSdkClient;
24
30
  private prepareSdkRequest;
25
31
  private handleSuccessfulRequest;
26
32
  private logSdkRequest;
@@ -133,7 +133,7 @@ export class RequestHandler {
133
133
  this.logSdkRequest(sdkPrep, acc, apiTimestamp);
134
134
  }
135
135
  try {
136
- const client = createSdkClient(auth, sdkPrep.region, sdkPrep.effort);
136
+ const client = this.makeSdkClient(auth, sdkPrep.region, sdkPrep.effort);
137
137
  const command = new GenerateAssistantResponseCommand({
138
138
  conversationState: sdkPrep.conversationState,
139
139
  profileArn: sdkPrep.profileArn
@@ -183,6 +183,14 @@ export class RequestHandler {
183
183
  extractModel(url) {
184
184
  return url.match(/models\/([^/:]+)/)?.[1] || null;
185
185
  }
186
+ /**
187
+ * Seam over the module-level SDK client factory so tests can inject a fake
188
+ * client without a real network call or a leaky module mock. Behavior is
189
+ * identical to calling createSdkClient directly.
190
+ */
191
+ makeSdkClient(auth, region, effort) {
192
+ return createSdkClient(auth, region, effort);
193
+ }
186
194
  prepareSdkRequest(body, model, auth, think, budget, showToast) {
187
195
  return transformToSdkRequest(body, model, auth, think, budget, showToast, {
188
196
  effort: this.config.effort,
@@ -6,3 +6,4 @@ export declare function findClientCredsRecursive(input: unknown): {
6
6
  clientSecret?: string;
7
7
  };
8
8
  export declare function makePlaceholderEmail(authMethod: string, region: string, clientId?: string, profileArn?: string): string;
9
+ export declare function isPlaceholderEmail(email: unknown): boolean;
@@ -70,3 +70,9 @@ export function makePlaceholderEmail(authMethod, region, clientId, profileArn) {
70
70
  const h = createHash('sha256').update(seed).digest('hex').slice(0, 16);
71
71
  return `${authMethod}-placeholder+${h}@awsapps.local`;
72
72
  }
73
+ // Loose pre-filter ONLY. NOT authoritative: before deleting, callers must also
74
+ // require row.email === exact makePlaceholderEmail(...) AND row.id === recomputed
75
+ // placeholderId for the SAME identity, so a real account is never deleted.
76
+ export function isPlaceholderEmail(email) {
77
+ return (typeof email === 'string' && email.includes('-placeholder+') && email.endsWith('@awsapps.local'));
78
+ }
@@ -5,7 +5,7 @@ import { createDeterministicAccountId } from '../accounts.js';
5
5
  import * as logger from '../logger.js';
6
6
  import { kiroDb } from '../storage/sqlite.js';
7
7
  import { fetchUsageLimits } from '../usage.js';
8
- import { findClientCredsRecursive, getCliDbPath, makePlaceholderEmail, normalizeExpiresAt, safeJsonParse } from './kiro-cli-parser.js';
8
+ import { findClientCredsRecursive, getCliDbPath, isPlaceholderEmail, makePlaceholderEmail, normalizeExpiresAt, safeJsonParse } from './kiro-cli-parser.js';
9
9
  import { readActiveProfileArnFromKiroCli } from './kiro-cli-profile.js';
10
10
  import { getStaleKiroCliAccountIds, STALE_CLI_ACCOUNT_REASON } from './stale-accounts.js';
11
11
  export async function syncFromKiroCli() {
@@ -96,61 +96,66 @@ export async function syncFromKiroCli() {
96
96
  }
97
97
  const all = kiroDb.getAccounts();
98
98
  if (!email) {
99
- let existing;
100
- if (profileArn) {
101
- existing = all.find((a) => a.auth_method === authMethod && a.profile_arn === profileArn);
102
- }
103
- if (!existing && authMethod === 'idc' && clientId) {
104
- existing = all.find((a) => a.auth_method === 'idc' && a.client_id === clientId);
105
- }
106
- if (existing && typeof existing.email === 'string' && existing.email) {
107
- email = existing.email;
99
+ const sameIdentity = all.filter((a) => {
100
+ if (profileArn && a.auth_method === authMethod && a.profile_arn === profileArn)
101
+ return true;
102
+ if (authMethod === 'idc' &&
103
+ clientId &&
104
+ a.auth_method === 'idc' &&
105
+ a.client_id === clientId)
106
+ return true;
107
+ return false;
108
+ });
109
+ const realAccount = sameIdentity.find((a) => typeof a.email === 'string' && a.email && !isPlaceholderEmail(a.email));
110
+ if (realAccount) {
111
+ email = realAccount.email;
108
112
  }
109
113
  else {
114
+ const placeholderId = createDeterministicAccountId(makePlaceholderEmail(authMethod, serviceRegion, clientId, profileArn), authMethod, clientId, profileArn);
115
+ if (await kiroDb.isAccountRemoved(placeholderId)) {
116
+ await kiroDb.clearRemovedAccount(placeholderId);
117
+ }
110
118
  email = makePlaceholderEmail(authMethod, serviceRegion, clientId, profileArn);
111
119
  }
112
120
  }
113
121
  const resolvedEmail = email || makePlaceholderEmail(authMethod, serviceRegion, clientId, profileArn);
122
+ const hasRealEmail = !!resolvedEmail && !isPlaceholderEmail(resolvedEmail);
114
123
  const id = createDeterministicAccountId(resolvedEmail, authMethod, clientId, profileArn);
124
+ // Cleanup runs BEFORE the fresh-enough early-continue: a lingering
125
+ // same-identity placeholder must be removed even when the real account
126
+ // is already up to date and would otherwise skip the rest of the round.
127
+ if (hasRealEmail) {
128
+ for (const row of all) {
129
+ if (row.id === id)
130
+ continue;
131
+ const sameIdentity = (!!profileArn && row.auth_method === authMethod && row.profile_arn === profileArn) ||
132
+ (authMethod === 'idc' &&
133
+ !!clientId &&
134
+ row.auth_method === 'idc' &&
135
+ row.client_id === clientId);
136
+ if (!sameIdentity)
137
+ continue;
138
+ if (!isPlaceholderEmail(row.email))
139
+ continue;
140
+ // Prove the row is a genuine placeholder by self-consistency against
141
+ // its OWN fields (clientId rotates across device re-registration, so
142
+ // recomputing with the current token's clientId would miss it). A
143
+ // real email can never equal its own makePlaceholderEmail recompute,
144
+ // so this never deletes a real account.
145
+ const rowPlaceholderEmail = makePlaceholderEmail(row.auth_method, row.region, row.client_id, row.profile_arn);
146
+ const rowPlaceholderId = createDeterministicAccountId(rowPlaceholderEmail, row.auth_method, row.client_id, row.profile_arn);
147
+ if (row.email === rowPlaceholderEmail && row.id === rowPlaceholderId) {
148
+ await kiroDb.deleteAccount(row.id);
149
+ await kiroDb.addRemovedAccount(row.id);
150
+ }
151
+ }
152
+ }
115
153
  const existingById = all.find((a) => a.id === id);
116
154
  if (existingById &&
117
155
  existingById.is_healthy === 1 &&
118
156
  existingById.expires_at >= cliExpiresAt &&
119
157
  existingById.expires_at > Date.now())
120
158
  continue;
121
- if (usageOk) {
122
- const placeholderEmail = makePlaceholderEmail(authMethod, serviceRegion, clientId, profileArn);
123
- const placeholderId = createDeterministicAccountId(placeholderEmail, authMethod, clientId, profileArn);
124
- if (placeholderId !== id) {
125
- const placeholderRow = all.find((a) => a.id === placeholderId);
126
- if (placeholderRow && (await kiroDb.isAccountRemoved(placeholderId))) {
127
- skippedRemoved++;
128
- }
129
- else if (placeholderRow) {
130
- await kiroDb.upsertAccount({
131
- id: placeholderId,
132
- email: placeholderRow.email,
133
- authMethod,
134
- region: placeholderRow.region || serviceRegion,
135
- oidcRegion: placeholderRow.oidc_region || oidcRegion,
136
- clientId,
137
- clientSecret,
138
- profileArn,
139
- refreshToken: placeholderRow.refresh_token || refreshToken,
140
- accessToken: placeholderRow.access_token || accessToken,
141
- expiresAt: placeholderRow.expires_at || cliExpiresAt,
142
- rateLimitResetTime: 0,
143
- isHealthy: false,
144
- failCount: 10,
145
- unhealthyReason: 'Replaced by real email',
146
- recoveryTime: Date.now() + 31536000000,
147
- usedCount: placeholderRow.used_count || 0,
148
- limitCount: placeholderRow.limit_count || 0,
149
- lastSync: Date.now()
150
- });
151
- }
152
- }
153
- }
154
159
  if (await kiroDb.isAccountRemoved(id)) {
155
160
  skippedRemoved++;
156
161
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunerpy/opencode-kiro-auth",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",