@zhafron/opencode-kiro-auth 1.2.8 → 1.3.1

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,94 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ function getBaseDir() {
6
+ const p = process.platform;
7
+ if (p === 'win32')
8
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'opencode');
9
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode');
10
+ }
11
+ export const DB_PATH = join(getBaseDir(), 'kiro.db');
12
+ export class KiroDatabase {
13
+ db;
14
+ constructor(path = DB_PATH) {
15
+ const dir = join(path, '..');
16
+ if (!existsSync(dir))
17
+ mkdirSync(dir, { recursive: true });
18
+ this.db = new Database(path);
19
+ this.db.run('PRAGMA busy_timeout = 5000');
20
+ this.init();
21
+ }
22
+ init() {
23
+ this.db.run('PRAGMA journal_mode = WAL');
24
+ this.db.run(`
25
+ CREATE TABLE IF NOT EXISTS accounts (
26
+ id TEXT PRIMARY KEY, email TEXT NOT NULL, real_email TEXT, auth_method TEXT NOT NULL,
27
+ region TEXT NOT NULL, client_id TEXT, client_secret TEXT, profile_arn TEXT,
28
+ refresh_token TEXT NOT NULL, access_token TEXT NOT NULL, expires_at INTEGER NOT NULL,
29
+ rate_limit_reset INTEGER DEFAULT 0, is_healthy INTEGER DEFAULT 1, unhealthy_reason TEXT,
30
+ recovery_time INTEGER, last_used INTEGER DEFAULT 0
31
+ )
32
+ `);
33
+ this.db.run(`
34
+ CREATE TABLE IF NOT EXISTS usage (
35
+ account_id TEXT PRIMARY KEY, used_count INTEGER DEFAULT 0, limit_count INTEGER DEFAULT 0,
36
+ real_email TEXT, last_sync INTEGER,
37
+ FOREIGN KEY(account_id) REFERENCES accounts(id) ON DELETE CASCADE
38
+ )
39
+ `);
40
+ }
41
+ getAccounts() {
42
+ return this.db.prepare('SELECT * FROM accounts').all();
43
+ }
44
+ upsertAccount(acc) {
45
+ this.db
46
+ .prepare(`
47
+ INSERT INTO accounts (
48
+ id, email, real_email, auth_method, region, client_id, client_secret,
49
+ profile_arn, refresh_token, access_token, expires_at, rate_limit_reset,
50
+ is_healthy, unhealthy_reason, recovery_time, last_used
51
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
52
+ ON CONFLICT(id) DO UPDATE SET
53
+ email=excluded.email, real_email=excluded.real_email, auth_method=excluded.auth_method,
54
+ region=excluded.region, client_id=excluded.client_id, client_secret=excluded.client_secret,
55
+ profile_arn=excluded.profile_arn, refresh_token=excluded.refresh_token,
56
+ access_token=excluded.access_token, expires_at=excluded.expires_at,
57
+ rate_limit_reset=excluded.rate_limit_reset, is_healthy=excluded.is_healthy,
58
+ unhealthy_reason=excluded.unhealthy_reason, recovery_time=excluded.recovery_time,
59
+ last_used=excluded.last_used
60
+ `)
61
+ .run(acc.id, acc.email, acc.realEmail || null, acc.authMethod, acc.region, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.lastUsed || 0);
62
+ }
63
+ deleteAccount(id) {
64
+ this.db.prepare('DELETE FROM accounts WHERE id = ?').run(id);
65
+ }
66
+ getUsage() {
67
+ const rows = this.db.prepare('SELECT * FROM usage').all();
68
+ const usage = {};
69
+ for (const r of rows) {
70
+ usage[r.account_id] = {
71
+ usedCount: r.used_count,
72
+ limitCount: r.limit_count,
73
+ realEmail: r.real_email,
74
+ lastSync: r.last_sync
75
+ };
76
+ }
77
+ return usage;
78
+ }
79
+ upsertUsage(id, meta) {
80
+ this.db
81
+ .prepare(`
82
+ INSERT INTO usage (account_id, used_count, limit_count, real_email, last_sync)
83
+ VALUES (?, ?, ?, ?, ?)
84
+ ON CONFLICT(account_id) DO UPDATE SET
85
+ used_count=excluded.used_count, limit_count=excluded.limit_count,
86
+ real_email=excluded.real_email, last_sync=excluded.last_sync
87
+ `)
88
+ .run(id, meta.usedCount, meta.limitCount, meta.realEmail || null, meta.lastSync);
89
+ }
90
+ close() {
91
+ this.db.close();
92
+ }
93
+ }
94
+ export const kiroDb = new KiroDatabase();
@@ -0,0 +1,2 @@
1
+ export declare function syncFromKiroCli(): Promise<void>;
2
+ export declare function writeToKiroCli(acc: any): Promise<void>;
@@ -0,0 +1,96 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir, platform } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { createDeterministicAccountId } from '../accounts';
6
+ import * as logger from '../logger';
7
+ import { kiroDb } from '../storage/sqlite';
8
+ function getCliDbPath() {
9
+ const p = platform();
10
+ if (p === 'win32')
11
+ return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'kiro-cli', 'data.sqlite3');
12
+ if (p === 'darwin')
13
+ return join(homedir(), 'Library', 'Application Support', 'kiro-cli', 'data.sqlite3');
14
+ return join(homedir(), '.local', 'share', 'kiro-cli', 'data.sqlite3');
15
+ }
16
+ export async function syncFromKiroCli() {
17
+ const dbPath = getCliDbPath();
18
+ if (!existsSync(dbPath))
19
+ return;
20
+ try {
21
+ const cliDb = new Database(dbPath, { readonly: true });
22
+ cliDb.run('PRAGMA busy_timeout = 5000');
23
+ const rows = cliDb.prepare('SELECT key, value FROM auth_kv').all();
24
+ for (const row of rows) {
25
+ if (row.key.includes(':token')) {
26
+ let data;
27
+ try {
28
+ data = JSON.parse(row.value);
29
+ }
30
+ catch {
31
+ continue;
32
+ }
33
+ if (!data.access_token)
34
+ continue;
35
+ const email = data.email || 'cli-account@kiro.dev';
36
+ const authMethod = row.key.includes('odic') ? 'idc' : 'desktop';
37
+ const clientId = data.client_id ||
38
+ (authMethod === 'idc'
39
+ ? JSON.parse(rows.find((r) => r.key.includes('device-registration'))?.value || '{}')
40
+ .client_id
41
+ : undefined);
42
+ const clientSecret = data.client_secret ||
43
+ (authMethod === 'idc'
44
+ ? JSON.parse(rows.find((r) => r.key.includes('device-registration'))?.value || '{}')
45
+ .client_secret
46
+ : undefined);
47
+ const id = createDeterministicAccountId(email, authMethod, clientId, data.profile_arn);
48
+ const existing = kiroDb.getAccounts().find((a) => a.id === id);
49
+ const cliExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() : 0;
50
+ if (existing && existing.is_healthy === 1 && existing.expires_at >= cliExpiresAt)
51
+ continue;
52
+ kiroDb.upsertAccount({
53
+ id,
54
+ email,
55
+ realEmail: data.real_email || email,
56
+ authMethod,
57
+ region: data.region || 'us-east-1',
58
+ clientId,
59
+ clientSecret,
60
+ profileArn: data.profile_arn,
61
+ refreshToken: data.refresh_token,
62
+ accessToken: data.access_token,
63
+ expiresAt: cliExpiresAt || Date.now() + 3600000,
64
+ isHealthy: 1
65
+ });
66
+ }
67
+ }
68
+ cliDb.close();
69
+ }
70
+ catch (e) {
71
+ logger.error('Sync failed', e);
72
+ }
73
+ }
74
+ export async function writeToKiroCli(acc) {
75
+ const dbPath = getCliDbPath();
76
+ if (!existsSync(dbPath))
77
+ return;
78
+ try {
79
+ const cliDb = new Database(dbPath);
80
+ cliDb.run('PRAGMA busy_timeout = 5000');
81
+ const rows = cliDb.prepare('SELECT key, value FROM auth_kv').all();
82
+ const targetKey = acc.authMethod === 'idc' ? 'kirocli:odic:token' : 'kirocli:social:token';
83
+ const row = rows.find((r) => r.key === targetKey || r.key.endsWith(targetKey));
84
+ if (row) {
85
+ const data = JSON.parse(row.value);
86
+ data.access_token = acc.accessToken;
87
+ data.refresh_token = acc.refreshToken;
88
+ data.expires_at = new Date(acc.expiresAt).toISOString();
89
+ cliDb.prepare('UPDATE auth_kv SET value = ? WHERE key = ?').run(JSON.stringify(data), row.key);
90
+ }
91
+ cliDb.close();
92
+ }
93
+ catch (e) {
94
+ logger.warn('Write back failed', e);
95
+ }
96
+ }
@@ -1,17 +1,30 @@
1
- import { KiroTokenRefreshError } from './errors';
1
+ import crypto from 'node:crypto';
2
2
  import { decodeRefreshToken, encodeRefreshToken } from '../kiro/auth';
3
+ import { KiroTokenRefreshError } from './errors';
3
4
  export async function refreshAccessToken(auth) {
4
- const url = `https://oidc.${auth.region}.amazonaws.com/token`;
5
5
  const p = decodeRefreshToken(auth.refresh);
6
- if (!p.clientId || !p.clientSecret) {
6
+ const isIdc = auth.authMethod === 'idc';
7
+ const url = isIdc
8
+ ? `https://oidc.${auth.region}.amazonaws.com/token`
9
+ : `https://prod.${auth.region}.auth.desktop.kiro.dev/refreshToken`;
10
+ if (isIdc && (!p.clientId || !p.clientSecret)) {
7
11
  throw new KiroTokenRefreshError('Missing creds', 'MISSING_CREDENTIALS');
8
12
  }
9
- const requestBody = {
10
- refreshToken: p.refreshToken,
11
- clientId: p.clientId,
12
- clientSecret: p.clientSecret,
13
- grantType: 'refresh_token'
14
- };
13
+ const requestBody = isIdc
14
+ ? {
15
+ refreshToken: p.refreshToken,
16
+ clientId: p.clientId,
17
+ clientSecret: p.clientSecret,
18
+ grantType: 'refresh_token'
19
+ }
20
+ : {
21
+ refreshToken: p.refreshToken
22
+ };
23
+ const machineId = crypto
24
+ .createHash('sha256')
25
+ .update(auth.profileArn || auth.clientId || 'KIRO_DEFAULT_MACHINE')
26
+ .digest('hex');
27
+ const ua = isIdc ? 'aws-sdk-js/1.0.0' : `KiroIDE-0.7.45-${machineId}`;
15
28
  try {
16
29
  const res = await fetch(url, {
17
30
  method: 'POST',
@@ -20,6 +33,7 @@ export async function refreshAccessToken(auth) {
20
33
  Accept: 'application/json',
21
34
  'amz-sdk-request': 'attempt=1; max=1',
22
35
  'x-amzn-kiro-agent-mode': 'vibe',
36
+ 'user-agent': ua,
23
37
  Connection: 'close'
24
38
  },
25
39
  body: JSON.stringify(requestBody)
@@ -37,30 +51,28 @@ export async function refreshAccessToken(auth) {
37
51
  }
38
52
  const d = await res.json();
39
53
  const acc = d.access_token || d.accessToken;
40
- if (!acc) {
54
+ if (!acc)
41
55
  throw new KiroTokenRefreshError('No access token', 'INVALID_RESPONSE');
42
- }
43
56
  const upP = {
44
57
  refreshToken: d.refresh_token || d.refreshToken || p.refreshToken,
45
58
  clientId: p.clientId,
46
59
  clientSecret: p.clientSecret,
47
- authMethod: 'idc'
60
+ authMethod: auth.authMethod
48
61
  };
49
62
  return {
50
63
  refresh: encodeRefreshToken(upP),
51
64
  access: acc,
52
- expires: Date.now() + (d.expires_in || 3600) * 1000,
53
- authMethod: 'idc',
65
+ expires: Date.now() + (d.expires_in || d.expiresIn || 3600) * 1000,
66
+ authMethod: auth.authMethod,
54
67
  region: auth.region,
55
68
  clientId: auth.clientId,
56
69
  clientSecret: auth.clientSecret,
57
- email: auth.email
70
+ email: auth.email || d.userInfo?.email
58
71
  };
59
72
  }
60
73
  catch (error) {
61
- if (error instanceof KiroTokenRefreshError) {
74
+ if (error instanceof KiroTokenRefreshError)
62
75
  throw error;
63
- }
64
76
  throw new KiroTokenRefreshError(`Token refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'NETWORK_ERROR', error instanceof Error ? error : undefined);
65
77
  }
66
78
  }
@@ -1,4 +1,4 @@
1
- export type KiroAuthMethod = 'idc';
1
+ export type KiroAuthMethod = 'idc' | 'desktop';
2
2
  export type KiroRegion = 'us-east-1' | 'us-west-2';
3
3
  export interface KiroAuthDetails {
4
4
  refresh: string;
@@ -38,38 +38,12 @@ export interface ManagedAccount {
38
38
  limitCount?: number;
39
39
  lastUsed?: number;
40
40
  }
41
- export interface AccountMetadata {
42
- id: string;
43
- email: string;
44
- realEmail?: string;
45
- authMethod: KiroAuthMethod;
46
- region: KiroRegion;
47
- clientId?: string;
48
- clientSecret?: string;
49
- profileArn?: string;
50
- refreshToken: string;
51
- accessToken: string;
52
- expiresAt: number;
53
- rateLimitResetTime: number;
54
- isHealthy: boolean;
55
- unhealthyReason?: string;
56
- recoveryTime?: number;
57
- }
58
- export interface AccountStorage {
59
- version: 1;
60
- accounts: AccountMetadata[];
61
- activeIndex: number;
62
- }
63
41
  export interface UsageMetadata {
64
42
  usedCount: number;
65
43
  limitCount: number;
66
44
  realEmail?: string;
67
45
  lastSync: number;
68
46
  }
69
- export interface UsageStorage {
70
- version: 1;
71
- usage: Record<string, UsageMetadata>;
72
- }
73
47
  export interface CodeWhispererMessage {
74
48
  userInputMessage?: {
75
49
  content: string;
@@ -1,3 +1,3 @@
1
- import { ManagedAccount, KiroAuthDetails } from './types';
1
+ import { KiroAuthDetails, ManagedAccount } from './types';
2
2
  export declare function fetchUsageLimits(auth: KiroAuthDetails): Promise<any>;
3
3
  export declare function updateAccountQuota(account: ManagedAccount, usage: any, accountManager?: any): void;