@lightharu/krouter 1.8.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.
Files changed (61) hide show
  1. package/LICENSE +679 -0
  2. package/README.md +238 -0
  3. package/dist-web/assets/index-CM4-0adf.css +1 -0
  4. package/dist-web/assets/index-DCslvfUR.js +139 -0
  5. package/dist-web/favicon.svg +9 -0
  6. package/dist-web/icon.svg +9 -0
  7. package/dist-web/index.html +19 -0
  8. package/out-server/main/kiroAuthSync.js +249 -0
  9. package/out-server/main/kproxy/certManager.js +262 -0
  10. package/out-server/main/kproxy/index.js +254 -0
  11. package/out-server/main/kproxy/mitmProxy.js +475 -0
  12. package/out-server/main/kproxy/types.js +23 -0
  13. package/out-server/main/proxy/accountPool.js +543 -0
  14. package/out-server/main/proxy/clientConfig.js +596 -0
  15. package/out-server/main/proxy/index.js +25 -0
  16. package/out-server/main/proxy/kiroApi.js +1996 -0
  17. package/out-server/main/proxy/logger.js +407 -0
  18. package/out-server/main/proxy/modelCatalog.js +75 -0
  19. package/out-server/main/proxy/promptCacheTracker.js +301 -0
  20. package/out-server/main/proxy/proxyServer.js +3543 -0
  21. package/out-server/main/proxy/selfSignedCert.js +179 -0
  22. package/out-server/main/proxy/systemProxy.js +250 -0
  23. package/out-server/main/proxy/tokenCounter.js +164 -0
  24. package/out-server/main/proxy/toolNameRegistry.js +57 -0
  25. package/out-server/main/proxy/translator.js +1084 -0
  26. package/out-server/main/proxy/types.js +3 -0
  27. package/out-server/main/registration/browser-identity.js +184 -0
  28. package/out-server/main/registration/chainProxy.js +349 -0
  29. package/out-server/main/registration/config.js +58 -0
  30. package/out-server/main/registration/email-service.js +801 -0
  31. package/out-server/main/registration/fingerprint.js +352 -0
  32. package/out-server/main/registration/http-utils.js +148 -0
  33. package/out-server/main/registration/jwe.js +74 -0
  34. package/out-server/main/registration/names.js +142 -0
  35. package/out-server/main/registration/proton-mail-window.js +339 -0
  36. package/out-server/main/registration/registrar.js +1715 -0
  37. package/out-server/main/registration/tlsClientPool.js +70 -0
  38. package/out-server/main/registration/xxtea.js +161 -0
  39. package/out-server/main/runtimePaths.js +19 -0
  40. package/out-server/main/utils/redact.js +95 -0
  41. package/out-server/server/index.js +1272 -0
  42. package/out-server/server/services/accountExtras.js +105 -0
  43. package/out-server/server/services/accountProfileHydration.js +95 -0
  44. package/out-server/server/services/authFlows.js +509 -0
  45. package/out-server/server/services/dashboardTunnel.js +315 -0
  46. package/out-server/server/services/diagnostics.js +326 -0
  47. package/out-server/server/services/kiroAccounts.js +431 -0
  48. package/out-server/server/services/kiroSettings.js +260 -0
  49. package/out-server/server/services/kproxyRuntime.js +264 -0
  50. package/out-server/server/services/localKiroCredentials.js +320 -0
  51. package/out-server/server/services/machineIdRuntime.js +327 -0
  52. package/out-server/server/services/protonBrowserRuntime.js +724 -0
  53. package/out-server/server/services/proxyRuntime.js +523 -0
  54. package/out-server/server/services/registrationRuntime.js +106 -0
  55. package/out-server/server/store.js +266 -0
  56. package/package.json +113 -0
  57. package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
  58. package/scripts/kiro-manager-cli.cjs +3 -0
  59. package/scripts/krouter-cli.cjs +509 -0
  60. package/src/renderer/src/assets/krouter-logo.svg +11 -0
  61. package/src/renderer/src/assets/krouter-mark.svg +9 -0
@@ -0,0 +1,509 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startBuilderIdLogin = startBuilderIdLogin;
7
+ exports.pollBuilderIdAuth = pollBuilderIdAuth;
8
+ exports.cancelBuilderIdLogin = cancelBuilderIdLogin;
9
+ exports.startIamSsoLogin = startIamSsoLogin;
10
+ exports.handleIamSsoCallback = handleIamSsoCallback;
11
+ exports.pollIamSsoAuth = pollIamSsoAuth;
12
+ exports.cancelIamSsoLogin = cancelIamSsoLogin;
13
+ exports.completeIamSsoLogin = completeIamSsoLogin;
14
+ exports.startSocialLogin = startSocialLogin;
15
+ exports.handleSocialCallback = handleSocialCallback;
16
+ exports.exchangeSocialToken = exchangeSocialToken;
17
+ exports.cancelSocialLogin = cancelSocialLogin;
18
+ exports.importFromSsoToken = importFromSsoToken;
19
+ exports.sendAuthHtml = sendAuthHtml;
20
+ const crypto_1 = __importDefault(require("crypto"));
21
+ const kiroAccounts_1 = require("./kiroAccounts");
22
+ const KIRO_AUTH_ENDPOINT = 'https://prod.us-east-1.auth.desktop.kiro.dev';
23
+ const KIRO_SOCIAL_REDIRECT_URI = 'kiro://kiro.kiroAgent/authenticate-success';
24
+ const DEFAULT_START_URL = 'https://view.awsapps.com/start';
25
+ const PORTAL_BASE = 'https://portal.sso.us-east-1.amazonaws.com';
26
+ const SSO_SCOPES = [
27
+ 'codewhisperer:completions',
28
+ 'codewhisperer:analysis',
29
+ 'codewhisperer:conversations',
30
+ 'codewhisperer:transformations',
31
+ 'codewhisperer:taskassist'
32
+ ];
33
+ let currentLoginState = null;
34
+ let iamSsoResult = null;
35
+ function publicBaseUrl() {
36
+ if (process.env.PUBLIC_BASE_URL)
37
+ return process.env.PUBLIC_BASE_URL.replace(/\/$/, '');
38
+ const host = process.env.HOST && process.env.HOST !== '0.0.0.0' ? process.env.HOST : '127.0.0.1';
39
+ return `http://${host}:${process.env.PORT || 4010}`;
40
+ }
41
+ function oidcBase(region = 'us-east-1') {
42
+ return `https://oidc.${region}.amazonaws.com`;
43
+ }
44
+ async function postJson(url, body, headers = {}) {
45
+ const response = await fetch(url, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json', ...headers },
48
+ body: JSON.stringify(body)
49
+ });
50
+ if (!response.ok)
51
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
52
+ return response.json();
53
+ }
54
+ async function getJson(url, headers = {}) {
55
+ const response = await fetch(url, { method: 'GET', headers });
56
+ if (!response.ok)
57
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
58
+ return response.json();
59
+ }
60
+ async function registerOidcClient(region, input) {
61
+ return postJson(`${oidcBase(region)}/client/register`, {
62
+ clientName: 'Krouter',
63
+ clientType: 'public',
64
+ scopes: SSO_SCOPES,
65
+ grantTypes: input.grantTypes,
66
+ issuerUrl: input.issuerUrl,
67
+ redirectUris: input.redirectUris
68
+ });
69
+ }
70
+ async function enrichTokenData(bundle) {
71
+ const data = {
72
+ accessToken: bundle.accessToken,
73
+ refreshToken: bundle.refreshToken,
74
+ clientId: bundle.clientId,
75
+ clientSecret: bundle.clientSecret,
76
+ region: bundle.region || 'us-east-1',
77
+ expiresIn: bundle.expiresIn,
78
+ authMethod: bundle.authMethod,
79
+ provider: bundle.provider,
80
+ idp: bundle.provider
81
+ };
82
+ try {
83
+ const status = await (0, kiroAccounts_1.checkAccountStatus)({
84
+ id: 'auth-import',
85
+ idp: bundle.provider,
86
+ credentials: {
87
+ accessToken: bundle.accessToken,
88
+ refreshToken: bundle.refreshToken,
89
+ clientId: bundle.clientId,
90
+ clientSecret: bundle.clientSecret,
91
+ region: bundle.region || 'us-east-1',
92
+ authMethod: bundle.authMethod,
93
+ provider: bundle.provider,
94
+ expiresAt: Date.now() + (bundle.expiresIn || 3600) * 1000
95
+ }
96
+ });
97
+ if (status?.success && status.data)
98
+ Object.assign(data, status.data);
99
+ }
100
+ catch {
101
+ // Credential verification can still be run by the caller later.
102
+ }
103
+ return data;
104
+ }
105
+ async function startBuilderIdLogin(region = 'us-east-1') {
106
+ try {
107
+ const { clientId, clientSecret } = await registerOidcClient(region, {
108
+ grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'],
109
+ issuerUrl: DEFAULT_START_URL
110
+ });
111
+ const auth = await postJson(`${oidcBase(region)}/device_authorization`, { clientId, clientSecret, startUrl: DEFAULT_START_URL });
112
+ currentLoginState = {
113
+ type: 'builderid',
114
+ clientId,
115
+ clientSecret,
116
+ deviceCode: auth.deviceCode,
117
+ userCode: auth.userCode,
118
+ verificationUri: auth.verificationUri,
119
+ interval: auth.interval || 5,
120
+ expiresAt: Date.now() + (auth.expiresIn || 600) * 1000,
121
+ region
122
+ };
123
+ return {
124
+ success: true,
125
+ userCode: auth.userCode,
126
+ verificationUri: auth.verificationUriComplete || auth.verificationUri,
127
+ expiresIn: auth.expiresIn || 600,
128
+ interval: auth.interval || 5
129
+ };
130
+ }
131
+ catch (error) {
132
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to start Builder ID login' };
133
+ }
134
+ }
135
+ async function pollBuilderIdAuth(region = 'us-east-1') {
136
+ if (!currentLoginState || currentLoginState.type !== 'builderid')
137
+ return { success: false, error: 'No Builder ID login is in progress' };
138
+ if (Date.now() > (currentLoginState.expiresAt || 0)) {
139
+ currentLoginState = null;
140
+ return { success: false, error: 'Authorization expired, please start again' };
141
+ }
142
+ try {
143
+ const response = await fetch(`${oidcBase(region)}/token`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify({
147
+ clientId: currentLoginState.clientId,
148
+ clientSecret: currentLoginState.clientSecret,
149
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code',
150
+ deviceCode: currentLoginState.deviceCode
151
+ })
152
+ });
153
+ if (response.status === 200) {
154
+ const token = await response.json();
155
+ const result = {
156
+ success: true,
157
+ completed: true,
158
+ accessToken: token.accessToken,
159
+ refreshToken: token.refreshToken,
160
+ clientId: currentLoginState.clientId,
161
+ clientSecret: currentLoginState.clientSecret,
162
+ region,
163
+ expiresIn: token.expiresIn
164
+ };
165
+ currentLoginState = null;
166
+ return result;
167
+ }
168
+ if (response.status === 400) {
169
+ const error = (await response.json()).error;
170
+ if (error === 'authorization_pending')
171
+ return { success: true, completed: false, status: 'pending' };
172
+ if (error === 'slow_down') {
173
+ currentLoginState.interval = (currentLoginState.interval || 5) + 5;
174
+ return { success: true, completed: false, status: 'slow_down' };
175
+ }
176
+ currentLoginState = null;
177
+ return { success: false, error: error || 'Authorization failed' };
178
+ }
179
+ return { success: false, error: `Unexpected response: ${response.status}` };
180
+ }
181
+ catch (error) {
182
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to poll Builder ID auth' };
183
+ }
184
+ }
185
+ function cancelBuilderIdLogin() {
186
+ currentLoginState = null;
187
+ return { success: true };
188
+ }
189
+ async function startIamSsoLogin(startUrl, region = 'us-east-1') {
190
+ if (!startUrl || !startUrl.startsWith('https://'))
191
+ return { success: false, error: 'SSO Start URL must start with https://' };
192
+ try {
193
+ const { clientId, clientSecret } = await registerOidcClient(region, {
194
+ grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'],
195
+ issuerUrl: startUrl
196
+ });
197
+ const auth = await postJson(`${oidcBase(region)}/device_authorization`, { clientId, clientSecret, startUrl });
198
+ iamSsoResult = null;
199
+ currentLoginState = {
200
+ type: 'iamsso',
201
+ clientId,
202
+ clientSecret,
203
+ deviceCode: auth.deviceCode,
204
+ userCode: auth.userCode,
205
+ verificationUri: auth.verificationUri,
206
+ interval: auth.interval || 5,
207
+ region,
208
+ startUrl,
209
+ expiresAt: Date.now() + (auth.expiresIn || 600) * 1000
210
+ };
211
+ const verificationUri = auth.verificationUriComplete || auth.verificationUri;
212
+ return {
213
+ success: true,
214
+ authorizeUrl: verificationUri,
215
+ userCode: auth.userCode,
216
+ verificationUri,
217
+ expiresIn: auth.expiresIn || 600,
218
+ interval: auth.interval || 5
219
+ };
220
+ }
221
+ catch (error) {
222
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to start IAM SSO login' };
223
+ }
224
+ }
225
+ async function handleIamSsoCallback(url) {
226
+ const code = url.searchParams.get('code');
227
+ const state = url.searchParams.get('state');
228
+ const error = url.searchParams.get('error');
229
+ if (!currentLoginState || currentLoginState.type !== 'iamsso') {
230
+ iamSsoResult = { completed: true, success: false, error: 'No IAM SSO login is in progress' };
231
+ return { title: 'Xác thực thất bại', body: 'Không có phiên đăng nhập IAM SSO nào đang chạy.' };
232
+ }
233
+ if (error) {
234
+ iamSsoResult = { completed: true, success: false, error };
235
+ return { title: 'Xác thực thất bại', body: error };
236
+ }
237
+ if (!code || state !== currentLoginState.oauthState) {
238
+ iamSsoResult = { completed: true, success: false, error: 'Invalid authorization callback' };
239
+ return { title: 'Xác thực thất bại', body: 'Callback xác thực không hợp lệ.' };
240
+ }
241
+ try {
242
+ const token = await postJson(`${oidcBase(currentLoginState.region)}/token`, {
243
+ clientId: currentLoginState.clientId,
244
+ clientSecret: currentLoginState.clientSecret,
245
+ grantType: 'authorization_code',
246
+ redirectUri: currentLoginState.redirectUri,
247
+ code,
248
+ codeVerifier: currentLoginState.codeVerifier
249
+ });
250
+ iamSsoResult = {
251
+ completed: true,
252
+ success: true,
253
+ accessToken: token.accessToken,
254
+ refreshToken: token.refreshToken,
255
+ clientId: currentLoginState.clientId,
256
+ clientSecret: currentLoginState.clientSecret,
257
+ region: currentLoginState.region,
258
+ expiresIn: token.expiresIn
259
+ };
260
+ return { title: 'Xác thực hoàn tất', body: 'Anh có thể đóng tab trình duyệt này và quay lại Krouter.' };
261
+ }
262
+ catch (exchangeError) {
263
+ iamSsoResult = { completed: true, success: false, error: exchangeError instanceof Error ? exchangeError.message : 'Token exchange failed' };
264
+ return { title: 'Xác thực thất bại', body: iamSsoResult.error || 'Đổi token thất bại.' };
265
+ }
266
+ }
267
+ async function pollIamSsoAuth() {
268
+ if (!currentLoginState || currentLoginState.type !== 'iamsso')
269
+ return { success: false, error: 'No IAM SSO login is in progress' };
270
+ if (Date.now() > (currentLoginState.expiresAt || 0)) {
271
+ currentLoginState = null;
272
+ iamSsoResult = null;
273
+ return { success: false, error: 'Authorization expired, please start again' };
274
+ }
275
+ if (iamSsoResult) {
276
+ const result = { ...iamSsoResult };
277
+ if (result.completed) {
278
+ currentLoginState = null;
279
+ iamSsoResult = null;
280
+ }
281
+ return result;
282
+ }
283
+ if (currentLoginState.deviceCode) {
284
+ try {
285
+ const state = currentLoginState;
286
+ const response = await fetch(`${oidcBase(state.region)}/token`, {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify({
290
+ clientId: state.clientId,
291
+ clientSecret: state.clientSecret,
292
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code',
293
+ deviceCode: state.deviceCode
294
+ })
295
+ });
296
+ if (response.status === 200) {
297
+ const token = await response.json();
298
+ currentLoginState = null;
299
+ return {
300
+ success: true,
301
+ completed: true,
302
+ accessToken: token.accessToken,
303
+ refreshToken: token.refreshToken,
304
+ clientId: state.clientId,
305
+ clientSecret: state.clientSecret,
306
+ region: state.region,
307
+ expiresIn: token.expiresIn
308
+ };
309
+ }
310
+ if (response.status === 400) {
311
+ const error = (await response.json()).error;
312
+ if (error === 'authorization_pending')
313
+ return { success: true, completed: false, status: 'pending' };
314
+ if (error === 'slow_down') {
315
+ state.interval = (state.interval || 5) + 5;
316
+ return { success: true, completed: false, status: 'slow_down' };
317
+ }
318
+ currentLoginState = null;
319
+ return { success: false, error: error || 'Authorization failed' };
320
+ }
321
+ return { success: false, error: `Unexpected response: ${response.status}` };
322
+ }
323
+ catch (error) {
324
+ return { success: false, error: error instanceof Error ? error.message : 'Failed to poll IAM SSO auth' };
325
+ }
326
+ }
327
+ return { success: true, completed: false, status: 'pending' };
328
+ }
329
+ function cancelIamSsoLogin() {
330
+ currentLoginState = null;
331
+ iamSsoResult = null;
332
+ return { success: true };
333
+ }
334
+ async function completeIamSsoLogin(code) {
335
+ const state = currentLoginState?.oauthState || '';
336
+ await handleIamSsoCallback(new URL(`${publicBaseUrl()}/api/auth/iam-sso/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`));
337
+ return pollIamSsoAuth();
338
+ }
339
+ function startSocialLogin(provider) {
340
+ if (provider !== 'Google' && provider !== 'Github')
341
+ return { success: false, error: 'Unsupported social provider' };
342
+ const codeVerifier = crypto_1.default.randomBytes(64).toString('base64url').substring(0, 128);
343
+ const codeChallenge = crypto_1.default.createHash('sha256').update(codeVerifier).digest('base64url');
344
+ const oauthState = crypto_1.default.randomBytes(32).toString('base64url');
345
+ // Kiro's hosted Cognito client only allowlists the desktop custom-scheme
346
+ // callback. HTTP callbacks such as the web server URL produce
347
+ // `redirect_mismatch` before the user reaches GitHub/Google.
348
+ const redirectUri = KIRO_SOCIAL_REDIRECT_URI;
349
+ const loginUrl = new URL(`${KIRO_AUTH_ENDPOINT}/login`);
350
+ loginUrl.searchParams.set('idp', provider);
351
+ loginUrl.searchParams.set('redirect_uri', redirectUri);
352
+ loginUrl.searchParams.set('code_challenge', codeChallenge);
353
+ loginUrl.searchParams.set('code_challenge_method', 'S256');
354
+ loginUrl.searchParams.set('state', oauthState);
355
+ currentLoginState = { type: 'social', codeVerifier, codeChallenge, oauthState, provider, redirectUri, expiresAt: Date.now() + 600000 };
356
+ return { success: true, loginUrl: loginUrl.toString(), state: oauthState };
357
+ }
358
+ function handleSocialCallback(url, emit) {
359
+ const error = url.searchParams.get('error');
360
+ const code = url.searchParams.get('code');
361
+ const state = url.searchParams.get('state');
362
+ if (error) {
363
+ emit('social-auth-callback', { error });
364
+ return { title: 'Xác thực thất bại', body: error };
365
+ }
366
+ if (!code || !state) {
367
+ emit('social-auth-callback', { error: 'Missing code or state' });
368
+ return { title: 'Xác thực thất bại', body: 'Thiếu code hoặc state.' };
369
+ }
370
+ emit('social-auth-callback', { code, state });
371
+ return { title: 'Xác thực hoàn tất', body: 'Anh có thể đóng tab trình duyệt này và quay lại Krouter.' };
372
+ }
373
+ async function exchangeSocialToken(code, state) {
374
+ if (!currentLoginState || currentLoginState.type !== 'social')
375
+ return { success: false, error: 'No social login is in progress' };
376
+ if (Date.now() > (currentLoginState.expiresAt || 0)) {
377
+ currentLoginState = null;
378
+ return { success: false, error: 'Authorization expired, please start again' };
379
+ }
380
+ if (state !== currentLoginState.oauthState) {
381
+ currentLoginState = null;
382
+ return { success: false, error: 'State parameter does not match' };
383
+ }
384
+ try {
385
+ const token = await postJson(`${KIRO_AUTH_ENDPOINT}/oauth/token`, {
386
+ code,
387
+ code_verifier: currentLoginState.codeVerifier,
388
+ redirect_uri: currentLoginState.redirectUri
389
+ });
390
+ const result = {
391
+ success: true,
392
+ accessToken: token.accessToken,
393
+ refreshToken: token.refreshToken,
394
+ profileArn: token.profileArn,
395
+ expiresIn: token.expiresIn,
396
+ authMethod: 'social',
397
+ provider: currentLoginState.provider
398
+ };
399
+ currentLoginState = null;
400
+ return result;
401
+ }
402
+ catch (error) {
403
+ currentLoginState = null;
404
+ return { success: false, error: error instanceof Error ? error.message : 'Token exchange failed' };
405
+ }
406
+ }
407
+ function cancelSocialLogin() {
408
+ currentLoginState = null;
409
+ return { success: true };
410
+ }
411
+ async function importFromSsoToken(bearerToken, region = 'us-east-1') {
412
+ if (!bearerToken)
413
+ return { success: false, error: { message: 'Missing SSO bearer token' } };
414
+ try {
415
+ const ssoResult = await ssoDeviceAuth(bearerToken, region);
416
+ if (!ssoResult.success || !ssoResult.accessToken || !ssoResult.refreshToken) {
417
+ return { success: false, error: { message: ssoResult.error || 'SSO authorization failed' } };
418
+ }
419
+ return {
420
+ success: true,
421
+ data: await enrichTokenData({
422
+ accessToken: ssoResult.accessToken,
423
+ refreshToken: ssoResult.refreshToken,
424
+ clientId: ssoResult.clientId,
425
+ clientSecret: ssoResult.clientSecret,
426
+ region: ssoResult.region || region,
427
+ expiresIn: ssoResult.expiresIn,
428
+ authMethod: 'IdC',
429
+ provider: 'BuilderId'
430
+ })
431
+ };
432
+ }
433
+ catch (error) {
434
+ return { success: false, error: { message: error instanceof Error ? error.message : 'Unknown error' } };
435
+ }
436
+ }
437
+ async function ssoDeviceAuth(bearerToken, region = 'us-east-1') {
438
+ try {
439
+ const { clientId, clientSecret } = await registerOidcClient(region, {
440
+ grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token'],
441
+ issuerUrl: DEFAULT_START_URL
442
+ });
443
+ const device = await postJson(`${oidcBase(region)}/device_authorization`, {
444
+ clientId,
445
+ clientSecret,
446
+ startUrl: DEFAULT_START_URL
447
+ });
448
+ await getJson(`${PORTAL_BASE}/token/whoAmI`, { Authorization: `Bearer ${bearerToken}`, Accept: 'application/json' });
449
+ const session = await postJson(`${PORTAL_BASE}/session/device`, {}, { Authorization: `Bearer ${bearerToken}` });
450
+ const accepted = await postJson(`${oidcBase(region)}/device_authorization/accept_user_code`, { userCode: device.userCode, userSessionId: session.token }, { Referer: DEFAULT_START_URL });
451
+ const deviceContext = accepted.deviceContext;
452
+ if (deviceContext?.deviceContextId) {
453
+ await postJson(`${oidcBase(region)}/device_authorization/associate_token`, {
454
+ deviceContext: {
455
+ deviceContextId: deviceContext.deviceContextId,
456
+ clientId: deviceContext.clientId || clientId,
457
+ clientType: deviceContext.clientType || 'public'
458
+ },
459
+ userSessionId: session.token
460
+ }, { Referer: DEFAULT_START_URL });
461
+ }
462
+ let interval = device.interval || 1;
463
+ const started = Date.now();
464
+ while (Date.now() - started < 120000) {
465
+ await new Promise((resolve) => setTimeout(resolve, interval * 1000));
466
+ const response = await fetch(`${oidcBase(region)}/token`, {
467
+ method: 'POST',
468
+ headers: { 'Content-Type': 'application/json' },
469
+ body: JSON.stringify({
470
+ clientId,
471
+ clientSecret,
472
+ grantType: 'urn:ietf:params:oauth:grant-type:device_code',
473
+ deviceCode: device.deviceCode
474
+ })
475
+ });
476
+ if (response.ok) {
477
+ const token = await response.json();
478
+ return { success: true, accessToken: token.accessToken, refreshToken: token.refreshToken, clientId, clientSecret, region, expiresIn: token.expiresIn };
479
+ }
480
+ if (response.status === 400) {
481
+ const error = (await response.json()).error;
482
+ if (error === 'authorization_pending')
483
+ continue;
484
+ if (error === 'slow_down') {
485
+ interval += 5;
486
+ continue;
487
+ }
488
+ return { success: false, error: error || 'Token polling failed' };
489
+ }
490
+ }
491
+ return { success: false, error: 'Authorization timed out' };
492
+ }
493
+ catch (error) {
494
+ return { success: false, error: error instanceof Error ? error.message : 'SSO device auth failed' };
495
+ }
496
+ }
497
+ function sendAuthHtml(response, title, body) {
498
+ response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
499
+ response.end(`<!doctype html><meta charset="utf-8"><title>${escapeHtml(title)}</title><body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(body)}</p></body>`);
500
+ }
501
+ function escapeHtml(value) {
502
+ return value.replace(/[&<>"']/g, (char) => ({
503
+ '&': '&amp;',
504
+ '<': '&lt;',
505
+ '>': '&gt;',
506
+ '"': '&quot;',
507
+ "'": '&#39;'
508
+ }[char] || char));
509
+ }