@zhafron/opencode-kiro-auth 1.4.8 → 1.4.9
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 +16 -0
- package/dist/plugin/sync/kiro-cli.js +173 -48
- package/dist/plugin.js +10 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# OpenCode Kiro Auth Plugin
|
|
2
|
+
|
|
2
3
|
[](https://www.npmjs.com/package/@zhafron/opencode-kiro-auth)
|
|
3
4
|
[](https://www.npmjs.com/package/@zhafron/opencode-kiro-auth)
|
|
4
5
|
[](https://www.npmjs.com/package/@zhafron/opencode-kiro-auth)
|
|
@@ -71,12 +72,25 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
|
|
|
71
72
|
1. **Authentication via Kiro CLI (Recommended)**:
|
|
72
73
|
- Perform login directly in your terminal using `kiro-cli login`.
|
|
73
74
|
- The plugin will automatically detect and import your session on startup.
|
|
75
|
+
- For AWS IAM Identity Center (SSO/IDC), the plugin imports both the token and device registration (OIDC client credentials) from the `kiro-cli` database.
|
|
74
76
|
2. **Direct Authentication**:
|
|
75
77
|
- Run `opencode auth login`.
|
|
76
78
|
- Select `Other`, type `kiro`, and press enter.
|
|
77
79
|
- Follow the instructions for **AWS Builder ID (IDC)**.
|
|
78
80
|
3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`.
|
|
79
81
|
|
|
82
|
+
## Troubleshooting
|
|
83
|
+
|
|
84
|
+
### Error: No accounts
|
|
85
|
+
|
|
86
|
+
This happens when the plugin has no records in `~/.config/opencode/kiro.db`.
|
|
87
|
+
|
|
88
|
+
1. Ensure `kiro-cli login` succeeds.
|
|
89
|
+
2. Ensure `auto_sync_kiro_cli` is `true` in `~/.config/opencode/kiro.json`.
|
|
90
|
+
3. Retry the request; the plugin will attempt a Kiro CLI sync when it detects zero accounts.
|
|
91
|
+
|
|
92
|
+
Note for IDC/SSO (ODIC): the plugin may temporarily create an account with a placeholder email if it cannot fetch the real email during sync (e.g. offline). It will replace it with the real email once usage/email lookup succeeds.
|
|
93
|
+
|
|
80
94
|
## Configuration
|
|
81
95
|
|
|
82
96
|
The plugin supports extensive configuration options. Edit `~/.config/opencode/kiro.json`:
|
|
@@ -118,10 +132,12 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
|
|
|
118
132
|
## Storage
|
|
119
133
|
|
|
120
134
|
**Linux/macOS:**
|
|
135
|
+
|
|
121
136
|
- SQLite Database: `~/.config/opencode/kiro.db`
|
|
122
137
|
- Plugin Config: `~/.config/opencode/kiro.json`
|
|
123
138
|
|
|
124
139
|
**Windows:**
|
|
140
|
+
|
|
125
141
|
- SQLite Database: `%APPDATA%\opencode\kiro.db`
|
|
126
142
|
- Plugin Config: `%APPDATA%\opencode\kiro.json`
|
|
127
143
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { existsSync } from 'node:fs';
|
|
3
4
|
import { homedir, platform } from 'node:os';
|
|
4
5
|
import { join } from 'node:path';
|
|
@@ -7,6 +8,9 @@ import * as logger from '../logger';
|
|
|
7
8
|
import { kiroDb } from '../storage/sqlite';
|
|
8
9
|
import { fetchUsageLimits } from '../usage';
|
|
9
10
|
function getCliDbPath() {
|
|
11
|
+
const override = process.env.KIROCLI_DB_PATH;
|
|
12
|
+
if (override)
|
|
13
|
+
return override;
|
|
10
14
|
const p = platform();
|
|
11
15
|
if (p === 'win32')
|
|
12
16
|
return join(process.env.APPDATA || join(homedir(), 'AppData', 'Roaming'), 'kiro-cli', 'data.sqlite3');
|
|
@@ -14,6 +18,65 @@ function getCliDbPath() {
|
|
|
14
18
|
return join(homedir(), 'Library', 'Application Support', 'kiro-cli', 'data.sqlite3');
|
|
15
19
|
return join(homedir(), '.local', 'share', 'kiro-cli', 'data.sqlite3');
|
|
16
20
|
}
|
|
21
|
+
function safeJsonParse(value) {
|
|
22
|
+
if (typeof value !== 'string')
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(value);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function normalizeExpiresAt(input) {
|
|
32
|
+
if (typeof input === 'number') {
|
|
33
|
+
// Heuristic: < 10^10 is likely seconds.
|
|
34
|
+
return input < 10_000_000_000 ? input * 1000 : input;
|
|
35
|
+
}
|
|
36
|
+
if (typeof input === 'string' && input.trim()) {
|
|
37
|
+
const t = new Date(input).getTime();
|
|
38
|
+
if (!Number.isNaN(t) && t > 0)
|
|
39
|
+
return t;
|
|
40
|
+
const n = Number(input);
|
|
41
|
+
if (Number.isFinite(n) && n > 0)
|
|
42
|
+
return normalizeExpiresAt(n);
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
function findClientCredsRecursive(input) {
|
|
47
|
+
const root = input;
|
|
48
|
+
if (!root || typeof root !== 'object')
|
|
49
|
+
return {};
|
|
50
|
+
const stack = [root];
|
|
51
|
+
const visited = new Set();
|
|
52
|
+
while (stack.length) {
|
|
53
|
+
const cur = stack.pop();
|
|
54
|
+
if (!cur || typeof cur !== 'object')
|
|
55
|
+
continue;
|
|
56
|
+
if (visited.has(cur))
|
|
57
|
+
continue;
|
|
58
|
+
visited.add(cur);
|
|
59
|
+
const clientId = cur.client_id || cur.clientId;
|
|
60
|
+
const clientSecret = cur.client_secret || cur.clientSecret;
|
|
61
|
+
if (typeof clientId === 'string' && typeof clientSecret === 'string') {
|
|
62
|
+
if (clientId && clientSecret)
|
|
63
|
+
return { clientId, clientSecret };
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(cur)) {
|
|
66
|
+
for (const v of cur)
|
|
67
|
+
stack.push(v);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
for (const v of Object.values(cur))
|
|
71
|
+
stack.push(v);
|
|
72
|
+
}
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
function makePlaceholderEmail(authMethod, region, clientId, profileArn) {
|
|
76
|
+
const seed = `${authMethod}:${region}:${clientId || ''}:${profileArn || ''}`;
|
|
77
|
+
const h = createHash('sha256').update(seed).digest('hex').slice(0, 16);
|
|
78
|
+
return `${authMethod}-placeholder+${h}@awsapps.local`;
|
|
79
|
+
}
|
|
17
80
|
export async function syncFromKiroCli() {
|
|
18
81
|
const dbPath = getCliDbPath();
|
|
19
82
|
if (!existsSync(dbPath))
|
|
@@ -22,66 +85,128 @@ export async function syncFromKiroCli() {
|
|
|
22
85
|
const cliDb = new Database(dbPath, { readonly: true });
|
|
23
86
|
cliDb.run('PRAGMA busy_timeout = 5000');
|
|
24
87
|
const rows = cliDb.prepare('SELECT key, value FROM auth_kv').all();
|
|
88
|
+
const deviceRegRow = rows.find((r) => typeof r?.key === 'string' && r.key.includes('device-registration'));
|
|
89
|
+
const deviceReg = safeJsonParse(deviceRegRow?.value);
|
|
90
|
+
const regCreds = deviceReg ? findClientCredsRecursive(deviceReg) : {};
|
|
25
91
|
for (const row of rows) {
|
|
26
92
|
if (row.key.includes(':token')) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
data = JSON.parse(row.value);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
93
|
+
const data = safeJsonParse(row.value);
|
|
94
|
+
if (!data)
|
|
32
95
|
continue;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
continue;
|
|
36
|
-
const authMethod = row.key.includes('odic') ? 'idc' : 'desktop';
|
|
96
|
+
const isIdc = row.key.includes('odic');
|
|
97
|
+
const authMethod = isIdc ? 'idc' : 'desktop';
|
|
37
98
|
const region = data.region || 'us-east-1';
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
99
|
+
const profileArn = data.profile_arn || data.profileArn;
|
|
100
|
+
const accessToken = data.access_token || data.accessToken || '';
|
|
101
|
+
const refreshToken = data.refresh_token || data.refreshToken;
|
|
102
|
+
if (!refreshToken)
|
|
103
|
+
continue;
|
|
104
|
+
const clientId = data.client_id || data.clientId || (isIdc ? regCreds.clientId : undefined);
|
|
105
|
+
const clientSecret = data.client_secret || data.clientSecret || (isIdc ? regCreds.clientSecret : undefined);
|
|
106
|
+
if (authMethod === 'idc' && (!clientId || !clientSecret)) {
|
|
107
|
+
logger.warn('Kiro CLI sync: missing IDC device credentials; skipping token import');
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const cliExpiresAt = normalizeExpiresAt(data.expires_at ?? data.expiresAt) || Date.now() + 3600000;
|
|
111
|
+
let usedCount = 0;
|
|
112
|
+
let limitCount = 0;
|
|
113
|
+
let email;
|
|
114
|
+
let usageOk = false;
|
|
48
115
|
try {
|
|
49
|
-
const
|
|
116
|
+
const authForUsage = {
|
|
50
117
|
refresh: '',
|
|
51
|
-
access:
|
|
52
|
-
expires:
|
|
53
|
-
authMethod,
|
|
54
|
-
region,
|
|
55
|
-
clientId,
|
|
56
|
-
clientSecret
|
|
57
|
-
});
|
|
58
|
-
const email = u.email;
|
|
59
|
-
if (!email)
|
|
60
|
-
continue;
|
|
61
|
-
const id = createDeterministicAccountId(email, authMethod, clientId, data.profile_arn);
|
|
62
|
-
const existing = kiroDb.getAccounts().find((a) => a.id === id);
|
|
63
|
-
const cliExpiresAt = data.expires_at ? new Date(data.expires_at).getTime() : 0;
|
|
64
|
-
if (existing && existing.is_healthy === 1 && existing.expires_at >= cliExpiresAt)
|
|
65
|
-
continue;
|
|
66
|
-
kiroDb.upsertAccount({
|
|
67
|
-
id,
|
|
68
|
-
email,
|
|
118
|
+
access: accessToken,
|
|
119
|
+
expires: cliExpiresAt,
|
|
69
120
|
authMethod,
|
|
70
121
|
region,
|
|
122
|
+
profileArn,
|
|
71
123
|
clientId,
|
|
72
124
|
clientSecret,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
125
|
+
email: ''
|
|
126
|
+
};
|
|
127
|
+
const u = await fetchUsageLimits(authForUsage);
|
|
128
|
+
usedCount = u.usedCount || 0;
|
|
129
|
+
limitCount = u.limitCount || 0;
|
|
130
|
+
if (typeof u.email === 'string' && u.email) {
|
|
131
|
+
email = u.email;
|
|
132
|
+
usageOk = true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
logger.warn('Kiro CLI sync: failed to fetch usage/email; falling back', {
|
|
137
|
+
authMethod,
|
|
138
|
+
region
|
|
82
139
|
});
|
|
140
|
+
logger.debug('Kiro CLI sync: usage fetch error', e);
|
|
141
|
+
}
|
|
142
|
+
const all = kiroDb.getAccounts();
|
|
143
|
+
if (!email) {
|
|
144
|
+
let existing;
|
|
145
|
+
if (profileArn) {
|
|
146
|
+
existing = all.find((a) => a.auth_method === authMethod && a.profile_arn === profileArn);
|
|
147
|
+
}
|
|
148
|
+
if (!existing && authMethod === 'idc' && clientId) {
|
|
149
|
+
existing = all.find((a) => a.auth_method === 'idc' && a.client_id === clientId);
|
|
150
|
+
}
|
|
151
|
+
if (existing && typeof existing.email === 'string' && existing.email) {
|
|
152
|
+
email = existing.email;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
email = makePlaceholderEmail(authMethod, region, clientId, profileArn);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const resolvedEmail = email || makePlaceholderEmail(authMethod, region, clientId, profileArn);
|
|
159
|
+
const id = createDeterministicAccountId(resolvedEmail, authMethod, clientId, profileArn);
|
|
160
|
+
const existingById = all.find((a) => a.id === id);
|
|
161
|
+
if (existingById &&
|
|
162
|
+
existingById.is_healthy === 1 &&
|
|
163
|
+
existingById.expires_at >= cliExpiresAt)
|
|
164
|
+
continue;
|
|
165
|
+
if (usageOk) {
|
|
166
|
+
const placeholderEmail = makePlaceholderEmail(authMethod, region, clientId, profileArn);
|
|
167
|
+
const placeholderId = createDeterministicAccountId(placeholderEmail, authMethod, clientId, profileArn);
|
|
168
|
+
if (placeholderId !== id) {
|
|
169
|
+
const placeholderRow = all.find((a) => a.id === placeholderId);
|
|
170
|
+
if (placeholderRow) {
|
|
171
|
+
kiroDb.upsertAccount({
|
|
172
|
+
id: placeholderId,
|
|
173
|
+
email: placeholderRow.email,
|
|
174
|
+
authMethod,
|
|
175
|
+
region: placeholderRow.region || region,
|
|
176
|
+
clientId,
|
|
177
|
+
clientSecret,
|
|
178
|
+
profileArn,
|
|
179
|
+
refreshToken: placeholderRow.refresh_token || refreshToken,
|
|
180
|
+
accessToken: placeholderRow.access_token || accessToken,
|
|
181
|
+
expiresAt: placeholderRow.expires_at || cliExpiresAt,
|
|
182
|
+
isHealthy: 0,
|
|
183
|
+
failCount: 10,
|
|
184
|
+
unhealthyReason: 'Replaced by real email',
|
|
185
|
+
recoveryTime: Date.now() + 31536000000,
|
|
186
|
+
usedCount: placeholderRow.used_count || 0,
|
|
187
|
+
limitCount: placeholderRow.limit_count || 0,
|
|
188
|
+
lastSync: Date.now()
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
83
192
|
}
|
|
84
|
-
|
|
193
|
+
kiroDb.upsertAccount({
|
|
194
|
+
id,
|
|
195
|
+
email: resolvedEmail,
|
|
196
|
+
authMethod,
|
|
197
|
+
region,
|
|
198
|
+
clientId,
|
|
199
|
+
clientSecret,
|
|
200
|
+
profileArn,
|
|
201
|
+
refreshToken,
|
|
202
|
+
accessToken,
|
|
203
|
+
expiresAt: cliExpiresAt,
|
|
204
|
+
isHealthy: 1,
|
|
205
|
+
failCount: 0,
|
|
206
|
+
usedCount,
|
|
207
|
+
limitCount,
|
|
208
|
+
lastSync: Date.now()
|
|
209
|
+
});
|
|
85
210
|
}
|
|
86
211
|
}
|
|
87
212
|
cliDb.close();
|
package/dist/plugin.js
CHANGED
|
@@ -83,6 +83,7 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
83
83
|
const think = model.endsWith('-thinking') || !!body.providerOptions?.thinkingConfig;
|
|
84
84
|
const budget = body.providerOptions?.thinkingConfig?.thinkingBudget || 20000;
|
|
85
85
|
let retry = 0, iterations = 0, reductionFactor = 1.0;
|
|
86
|
+
let triedEmptySync = false;
|
|
86
87
|
const startTime = Date.now(), maxIterations = config.max_request_iterations, timeoutMs = config.request_timeout_ms;
|
|
87
88
|
while (true) {
|
|
88
89
|
iterations++;
|
|
@@ -90,7 +91,15 @@ export const createKiroPlugin = (id) => async ({ client, directory }) => {
|
|
|
90
91
|
throw new Error(`Exceeded max iterations (${maxIterations})`);
|
|
91
92
|
if (Date.now() - startTime > timeoutMs)
|
|
92
93
|
throw new Error('Request timeout');
|
|
93
|
-
|
|
94
|
+
let count = am.getAccountCount();
|
|
95
|
+
if (count === 0 && config.auto_sync_kiro_cli && !triedEmptySync) {
|
|
96
|
+
triedEmptySync = true;
|
|
97
|
+
await syncFromKiroCli();
|
|
98
|
+
const refreshedAm = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
99
|
+
for (const a of refreshedAm.getAccounts())
|
|
100
|
+
am.addAccount(a);
|
|
101
|
+
count = am.getAccountCount();
|
|
102
|
+
}
|
|
94
103
|
if (count === 0)
|
|
95
104
|
throw new Error('No accounts');
|
|
96
105
|
let acc = am.getCurrentOrNext();
|