@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.
@@ -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
- }