@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.
- package/LICENSE +679 -0
- package/README.md +238 -0
- package/dist-web/assets/index-CM4-0adf.css +1 -0
- package/dist-web/assets/index-DCslvfUR.js +139 -0
- package/dist-web/favicon.svg +9 -0
- package/dist-web/icon.svg +9 -0
- package/dist-web/index.html +19 -0
- package/out-server/main/kiroAuthSync.js +249 -0
- package/out-server/main/kproxy/certManager.js +262 -0
- package/out-server/main/kproxy/index.js +254 -0
- package/out-server/main/kproxy/mitmProxy.js +475 -0
- package/out-server/main/kproxy/types.js +23 -0
- package/out-server/main/proxy/accountPool.js +543 -0
- package/out-server/main/proxy/clientConfig.js +596 -0
- package/out-server/main/proxy/index.js +25 -0
- package/out-server/main/proxy/kiroApi.js +1996 -0
- package/out-server/main/proxy/logger.js +407 -0
- package/out-server/main/proxy/modelCatalog.js +75 -0
- package/out-server/main/proxy/promptCacheTracker.js +301 -0
- package/out-server/main/proxy/proxyServer.js +3543 -0
- package/out-server/main/proxy/selfSignedCert.js +179 -0
- package/out-server/main/proxy/systemProxy.js +250 -0
- package/out-server/main/proxy/tokenCounter.js +164 -0
- package/out-server/main/proxy/toolNameRegistry.js +57 -0
- package/out-server/main/proxy/translator.js +1084 -0
- package/out-server/main/proxy/types.js +3 -0
- package/out-server/main/registration/browser-identity.js +184 -0
- package/out-server/main/registration/chainProxy.js +349 -0
- package/out-server/main/registration/config.js +58 -0
- package/out-server/main/registration/email-service.js +801 -0
- package/out-server/main/registration/fingerprint.js +352 -0
- package/out-server/main/registration/http-utils.js +148 -0
- package/out-server/main/registration/jwe.js +74 -0
- package/out-server/main/registration/names.js +142 -0
- package/out-server/main/registration/proton-mail-window.js +339 -0
- package/out-server/main/registration/registrar.js +1715 -0
- package/out-server/main/registration/tlsClientPool.js +70 -0
- package/out-server/main/registration/xxtea.js +161 -0
- package/out-server/main/runtimePaths.js +19 -0
- package/out-server/main/utils/redact.js +95 -0
- package/out-server/server/index.js +1272 -0
- package/out-server/server/services/accountExtras.js +105 -0
- package/out-server/server/services/accountProfileHydration.js +95 -0
- package/out-server/server/services/authFlows.js +509 -0
- package/out-server/server/services/dashboardTunnel.js +315 -0
- package/out-server/server/services/diagnostics.js +326 -0
- package/out-server/server/services/kiroAccounts.js +431 -0
- package/out-server/server/services/kiroSettings.js +260 -0
- package/out-server/server/services/kproxyRuntime.js +264 -0
- package/out-server/server/services/localKiroCredentials.js +320 -0
- package/out-server/server/services/machineIdRuntime.js +327 -0
- package/out-server/server/services/protonBrowserRuntime.js +724 -0
- package/out-server/server/services/proxyRuntime.js +523 -0
- package/out-server/server/services/registrationRuntime.js +106 -0
- package/out-server/server/store.js +266 -0
- package/package.json +113 -0
- package/resources/tls-client-xgo-1.14.0-windows-amd64.dll +0 -0
- package/scripts/kiro-manager-cli.cjs +3 -0
- package/scripts/krouter-cli.cjs +509 -0
- package/src/renderer/src/assets/krouter-logo.svg +11 -0
- 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
|
+
'&': '&',
|
|
504
|
+
'<': '<',
|
|
505
|
+
'>': '>',
|
|
506
|
+
'"': '"',
|
|
507
|
+
"'": '''
|
|
508
|
+
}[char] || char));
|
|
509
|
+
}
|