@zhafron/opencode-kiro-auth 1.6.5 → 1.7.0
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 +53 -7
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +13 -0
- package/dist/core/account/account-selector.js +0 -9
- package/dist/core/auth/auth-handler.js +25 -1
- package/dist/core/auth/idc-auth-method.d.ts +2 -3
- package/dist/core/auth/idc-auth-method.js +106 -167
- package/dist/core/request/error-handler.js +0 -2
- package/dist/infrastructure/database/account-repository.js +1 -0
- package/dist/kiro/oauth-idc.js +20 -6
- package/dist/plugin/accounts.js +2 -0
- package/dist/plugin/config/schema.d.ts +6 -0
- package/dist/plugin/config/schema.js +2 -0
- package/dist/plugin/logger.js +16 -1
- package/dist/plugin/request.js +2 -0
- package/dist/plugin/storage/migrations.js +14 -3
- package/dist/plugin/storage/sqlite.js +6 -5
- package/dist/plugin/sync/kiro-cli-profile.d.ts +1 -0
- package/dist/plugin/sync/kiro-cli-profile.js +30 -0
- package/dist/plugin/sync/kiro-cli.js +36 -9
- package/dist/plugin/token.js +4 -1
- package/dist/plugin/types.d.ts +2 -0
- package/dist/plugin/usage.js +19 -4
- package/package.json +1 -1
- package/dist/plugin/cli.d.ts +0 -8
- package/dist/plugin/cli.js +0 -103
- package/dist/plugin/server.d.ts +0 -34
- package/dist/plugin/server.js +0 -362
package/dist/plugin/server.js
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { authorizeKiroIDC } from '../kiro/oauth-idc';
|
|
3
|
-
import { getErrorHtml, getIDCAuthHtml, getStartUrlInputHtml, getSuccessHtml } from './auth-page';
|
|
4
|
-
import * as logger from './logger';
|
|
5
|
-
async function tryPort(port) {
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
const testServer = createServer();
|
|
8
|
-
testServer.once('error', () => resolve(false));
|
|
9
|
-
testServer.once('listening', () => {
|
|
10
|
-
testServer.close();
|
|
11
|
-
resolve(true);
|
|
12
|
-
});
|
|
13
|
-
testServer.listen(port, '127.0.0.1');
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
async function findAvailablePort(startPort, range) {
|
|
17
|
-
for (let i = 0; i < range; i++) {
|
|
18
|
-
const port = startPort + i;
|
|
19
|
-
const available = await tryPort(port);
|
|
20
|
-
if (available)
|
|
21
|
-
return port;
|
|
22
|
-
}
|
|
23
|
-
throw new Error(`No available ports in range ${startPort}-${startPort + range - 1}. Please close other applications using these ports.`);
|
|
24
|
-
}
|
|
25
|
-
export async function startIDCAuthServer(authData, startPort = 19847, portRange = 10) {
|
|
26
|
-
return new Promise(async (resolve, reject) => {
|
|
27
|
-
let port;
|
|
28
|
-
try {
|
|
29
|
-
port = await findAvailablePort(startPort, portRange);
|
|
30
|
-
logger.log(`Auth server will use port ${port}`);
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
logger.error('Failed to find available port', error);
|
|
34
|
-
reject(error);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
let server = null;
|
|
38
|
-
let timeoutId = null;
|
|
39
|
-
let resolver = null;
|
|
40
|
-
let rejector = null;
|
|
41
|
-
const status = { status: 'pending' };
|
|
42
|
-
const cleanup = () => {
|
|
43
|
-
if (timeoutId)
|
|
44
|
-
clearTimeout(timeoutId);
|
|
45
|
-
if (server)
|
|
46
|
-
server.close();
|
|
47
|
-
};
|
|
48
|
-
const sendHtml = (res, html) => {
|
|
49
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
50
|
-
res.end(html);
|
|
51
|
-
};
|
|
52
|
-
const poll = async () => {
|
|
53
|
-
try {
|
|
54
|
-
const body = {
|
|
55
|
-
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
56
|
-
deviceCode: authData.deviceCode,
|
|
57
|
-
clientId: authData.clientId,
|
|
58
|
-
clientSecret: authData.clientSecret
|
|
59
|
-
};
|
|
60
|
-
const res = await fetch(`https://oidc.${authData.region}.amazonaws.com/token`, {
|
|
61
|
-
method: 'POST',
|
|
62
|
-
headers: { 'Content-Type': 'application/json' },
|
|
63
|
-
body: JSON.stringify(body)
|
|
64
|
-
});
|
|
65
|
-
const responseText = await res.text();
|
|
66
|
-
let d = {};
|
|
67
|
-
if (responseText) {
|
|
68
|
-
try {
|
|
69
|
-
d = JSON.parse(responseText);
|
|
70
|
-
}
|
|
71
|
-
catch (parseError) {
|
|
72
|
-
logger.error(`Auth polling error: Failed to parse JSON (status ${res.status})`, parseError);
|
|
73
|
-
throw parseError;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (res.ok) {
|
|
77
|
-
const acc = d.access_token || d.accessToken, ref = d.refresh_token || d.refreshToken, exp = Date.now() + (d.expires_in || d.expiresIn || 0) * 1000;
|
|
78
|
-
let email = 'builder-id@aws.amazon.com';
|
|
79
|
-
try {
|
|
80
|
-
// Derive user info URL from startUrl: replace /start with /api/user/info
|
|
81
|
-
const userInfoUrl = authData.startUrl.replace(/\/start\/?$/, '/api/user/info');
|
|
82
|
-
const infoRes = await fetch(userInfoUrl, {
|
|
83
|
-
headers: { Authorization: `Bearer ${acc}` }
|
|
84
|
-
});
|
|
85
|
-
if (infoRes.ok) {
|
|
86
|
-
const info = await infoRes.json();
|
|
87
|
-
email = info.email || info.userName || email;
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
logger.warn(`User info request failed with status ${infoRes.status}; using fallback email`);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
catch (infoError) {
|
|
94
|
-
logger.warn(`Failed to fetch user info; using fallback email: ${infoError?.message || infoError}`);
|
|
95
|
-
}
|
|
96
|
-
status.status = 'success';
|
|
97
|
-
if (resolver)
|
|
98
|
-
resolver({
|
|
99
|
-
email,
|
|
100
|
-
accessToken: acc,
|
|
101
|
-
refreshToken: ref,
|
|
102
|
-
expiresAt: exp,
|
|
103
|
-
clientId: authData.clientId,
|
|
104
|
-
clientSecret: authData.clientSecret
|
|
105
|
-
});
|
|
106
|
-
setTimeout(cleanup, 2000);
|
|
107
|
-
}
|
|
108
|
-
else if (d.error === 'authorization_pending') {
|
|
109
|
-
setTimeout(poll, authData.interval * 1000);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
status.status = 'failed';
|
|
113
|
-
status.error = d.error_description || d.error;
|
|
114
|
-
logger.error(`Auth polling failed a: ${status.error}`);
|
|
115
|
-
if (rejector)
|
|
116
|
-
rejector(new Error(status.error));
|
|
117
|
-
setTimeout(cleanup, 2000);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
catch (e) {
|
|
121
|
-
status.status = 'failed';
|
|
122
|
-
status.error = e.message;
|
|
123
|
-
logger.error(`Auth polling error b: ${e.message}`, e);
|
|
124
|
-
if (rejector)
|
|
125
|
-
rejector(e);
|
|
126
|
-
setTimeout(cleanup, 2000);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
server = createServer((req, res) => {
|
|
130
|
-
const u = req.url || '';
|
|
131
|
-
if (u === '/' || u.startsWith('/?'))
|
|
132
|
-
sendHtml(res, getIDCAuthHtml(authData.verificationUriComplete, authData.userCode, `http://127.0.0.1:${port}/status`));
|
|
133
|
-
else if (u === '/status') {
|
|
134
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
135
|
-
res.end(JSON.stringify(status));
|
|
136
|
-
}
|
|
137
|
-
else if (u === '/success')
|
|
138
|
-
sendHtml(res, getSuccessHtml());
|
|
139
|
-
else if (u === '/error')
|
|
140
|
-
sendHtml(res, getErrorHtml(status.error || 'Failed'));
|
|
141
|
-
else {
|
|
142
|
-
res.writeHead(404);
|
|
143
|
-
res.end();
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
server.on('error', (e) => {
|
|
147
|
-
logger.error(`Auth server error on port ${port}`, e);
|
|
148
|
-
cleanup();
|
|
149
|
-
reject(e);
|
|
150
|
-
});
|
|
151
|
-
server.listen(port, '127.0.0.1', () => {
|
|
152
|
-
timeoutId = setTimeout(() => {
|
|
153
|
-
status.status = 'timeout';
|
|
154
|
-
logger.warn('Auth timeout waiting for authorization');
|
|
155
|
-
if (rejector)
|
|
156
|
-
rejector(new Error('Timeout'));
|
|
157
|
-
cleanup();
|
|
158
|
-
}, 900000);
|
|
159
|
-
poll();
|
|
160
|
-
resolve({
|
|
161
|
-
url: `http://127.0.0.1:${port}`,
|
|
162
|
-
waitForAuth: () => new Promise((rv, rj) => {
|
|
163
|
-
resolver = rv;
|
|
164
|
-
rejector = rj;
|
|
165
|
-
})
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Starts a local auth server that first shows a Start URL input page.
|
|
172
|
-
* After the user submits, it calls authorizeKiroIDC internally and transitions
|
|
173
|
-
* to the verification code page — no need to call authorizeKiroIDC beforehand.
|
|
174
|
-
*/
|
|
175
|
-
export async function startIDCAuthServerWithInput(region, defaultStartUrl, startPort = 19847, portRange = 10) {
|
|
176
|
-
return new Promise(async (resolve, reject) => {
|
|
177
|
-
let port;
|
|
178
|
-
try {
|
|
179
|
-
port = await findAvailablePort(startPort, portRange);
|
|
180
|
-
logger.log(`Auth server (with input) will use port ${port}`);
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
logger.error('Failed to find available port', error);
|
|
184
|
-
reject(error);
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
let server = null;
|
|
188
|
-
let timeoutId = null;
|
|
189
|
-
let resolver = null;
|
|
190
|
-
let rejector = null;
|
|
191
|
-
const status = { status: 'pending' };
|
|
192
|
-
// authData is populated after the user submits the start URL form
|
|
193
|
-
let authData = null;
|
|
194
|
-
const cleanup = () => {
|
|
195
|
-
if (timeoutId)
|
|
196
|
-
clearTimeout(timeoutId);
|
|
197
|
-
if (server)
|
|
198
|
-
server.close();
|
|
199
|
-
};
|
|
200
|
-
const sendHtml = (res, html) => {
|
|
201
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
202
|
-
res.end(html);
|
|
203
|
-
};
|
|
204
|
-
const sendJson = (res, code, data) => {
|
|
205
|
-
res.writeHead(code, { 'Content-Type': 'application/json' });
|
|
206
|
-
res.end(JSON.stringify(data));
|
|
207
|
-
};
|
|
208
|
-
const poll = async (data) => {
|
|
209
|
-
try {
|
|
210
|
-
const body = {
|
|
211
|
-
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
212
|
-
deviceCode: data.deviceCode,
|
|
213
|
-
clientId: data.clientId,
|
|
214
|
-
clientSecret: data.clientSecret
|
|
215
|
-
};
|
|
216
|
-
const res = await fetch(`https://oidc.${data.region}.amazonaws.com/token`, {
|
|
217
|
-
method: 'POST',
|
|
218
|
-
headers: { 'Content-Type': 'application/json' },
|
|
219
|
-
body: JSON.stringify(body)
|
|
220
|
-
});
|
|
221
|
-
const responseText = await res.text();
|
|
222
|
-
let d = {};
|
|
223
|
-
if (responseText) {
|
|
224
|
-
try {
|
|
225
|
-
d = JSON.parse(responseText);
|
|
226
|
-
}
|
|
227
|
-
catch (parseError) {
|
|
228
|
-
logger.error(`Auth polling error: Failed to parse JSON (status ${res.status})`, parseError);
|
|
229
|
-
throw parseError;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (res.ok) {
|
|
233
|
-
const acc = d.access_token || d.accessToken, ref = d.refresh_token || d.refreshToken, exp = Date.now() + (d.expires_in || d.expiresIn || 0) * 1000;
|
|
234
|
-
let email = 'builder-id@aws.amazon.com';
|
|
235
|
-
try {
|
|
236
|
-
const userInfoUrl = data.startUrl.replace(/\/start\/?$/, '/api/user/info');
|
|
237
|
-
const infoRes = await fetch(userInfoUrl, {
|
|
238
|
-
headers: { Authorization: `Bearer ${acc}` }
|
|
239
|
-
});
|
|
240
|
-
if (infoRes.ok) {
|
|
241
|
-
const info = await infoRes.json();
|
|
242
|
-
email = info.email || info.userName || email;
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
logger.warn(`User info request failed with status ${infoRes.status}; using fallback email`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
catch (infoError) {
|
|
249
|
-
logger.warn(`Failed to fetch user info; using fallback email: ${infoError?.message || infoError}`);
|
|
250
|
-
}
|
|
251
|
-
status.status = 'success';
|
|
252
|
-
if (resolver)
|
|
253
|
-
resolver({
|
|
254
|
-
email,
|
|
255
|
-
accessToken: acc,
|
|
256
|
-
refreshToken: ref,
|
|
257
|
-
expiresAt: exp,
|
|
258
|
-
clientId: data.clientId,
|
|
259
|
-
clientSecret: data.clientSecret
|
|
260
|
-
});
|
|
261
|
-
setTimeout(cleanup, 2000);
|
|
262
|
-
}
|
|
263
|
-
else if (d.error === 'authorization_pending') {
|
|
264
|
-
setTimeout(() => poll(data), data.interval * 1000);
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
status.status = 'failed';
|
|
268
|
-
status.error = d.error_description || d.error;
|
|
269
|
-
logger.error(`Auth polling failed: ${status.error}`);
|
|
270
|
-
if (rejector)
|
|
271
|
-
rejector(new Error(status.error));
|
|
272
|
-
setTimeout(cleanup, 2000);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
catch (e) {
|
|
276
|
-
status.status = 'failed';
|
|
277
|
-
status.error = e.message;
|
|
278
|
-
logger.error(`Auth polling error: ${e.message}`, e);
|
|
279
|
-
if (rejector)
|
|
280
|
-
rejector(e);
|
|
281
|
-
setTimeout(cleanup, 2000);
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
server = createServer(async (req, res) => {
|
|
285
|
-
const u = req.url || '';
|
|
286
|
-
// Step 1: Show start URL input page
|
|
287
|
-
if (u === '/' || u === '') {
|
|
288
|
-
sendHtml(res, getStartUrlInputHtml(defaultStartUrl || '', `http://127.0.0.1:${port}/setup`));
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
// Step 2: Receive start URL, call authorizeKiroIDC, return auth info to browser
|
|
292
|
-
if (u === '/setup' && req.method === 'POST') {
|
|
293
|
-
let body = '';
|
|
294
|
-
req.on('data', (chunk) => {
|
|
295
|
-
body += chunk;
|
|
296
|
-
});
|
|
297
|
-
req.on('end', async () => {
|
|
298
|
-
try {
|
|
299
|
-
const { startUrl } = JSON.parse(body);
|
|
300
|
-
const effectiveStartUrl = startUrl || undefined;
|
|
301
|
-
const data = await authorizeKiroIDC(region, effectiveStartUrl);
|
|
302
|
-
authData = data;
|
|
303
|
-
// Start polling now that we have device code
|
|
304
|
-
poll(authData);
|
|
305
|
-
sendJson(res, 200, {
|
|
306
|
-
userCode: data.userCode,
|
|
307
|
-
verificationUriComplete: data.verificationUriComplete
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
catch (e) {
|
|
311
|
-
logger.error('authorizeKiroIDC failed', e);
|
|
312
|
-
sendJson(res, 500, { error: e.message || 'Failed to initialize authentication' });
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
// Step 3: Show verification code page (browser redirects here after /setup)
|
|
318
|
-
if (u.startsWith('/auth')) {
|
|
319
|
-
const params = new URL(u, `http://127.0.0.1:${port}`).searchParams;
|
|
320
|
-
const code = params.get('code') || '';
|
|
321
|
-
const verUrl = params.get('url') || '';
|
|
322
|
-
sendHtml(res, getIDCAuthHtml(verUrl, code, `http://127.0.0.1:${port}/status`));
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
if (u === '/status') {
|
|
326
|
-
sendJson(res, 200, status);
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
if (u === '/success') {
|
|
330
|
-
sendHtml(res, getSuccessHtml());
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (u.startsWith('/error')) {
|
|
334
|
-
sendHtml(res, getErrorHtml(status.error || 'Failed'));
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
res.writeHead(404);
|
|
338
|
-
res.end();
|
|
339
|
-
});
|
|
340
|
-
server.on('error', (e) => {
|
|
341
|
-
logger.error(`Auth server error on port ${port}`, e);
|
|
342
|
-
cleanup();
|
|
343
|
-
reject(e);
|
|
344
|
-
});
|
|
345
|
-
server.listen(port, '127.0.0.1', () => {
|
|
346
|
-
timeoutId = setTimeout(() => {
|
|
347
|
-
status.status = 'timeout';
|
|
348
|
-
logger.warn('Auth timeout waiting for authorization');
|
|
349
|
-
if (rejector)
|
|
350
|
-
rejector(new Error('Timeout'));
|
|
351
|
-
cleanup();
|
|
352
|
-
}, 900000);
|
|
353
|
-
resolve({
|
|
354
|
-
url: `http://127.0.0.1:${port}`,
|
|
355
|
-
waitForAuth: () => new Promise((rv, rj) => {
|
|
356
|
-
resolver = rv;
|
|
357
|
-
rejector = rj;
|
|
358
|
-
})
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
}
|