@pentoshi/clai 4.11.24 → 4.11.25
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/dist/agent/stream-recovery.d.ts +1 -0
- package/dist/agent/stream-recovery.js +9 -0
- package/dist/agent/stream-recovery.js.map +1 -1
- package/dist/agent/turn/compaction-coordinator.js +2 -2
- package/dist/agent/turn/compaction-coordinator.js.map +1 -1
- package/dist/agent/turn/loop/stream-failure.js +4 -1
- package/dist/agent/turn/loop/stream-failure.js.map +1 -1
- package/dist/app/commands/catalog.js +31 -0
- package/dist/app/commands/catalog.js.map +1 -1
- package/dist/app/controllers/session-compact-helper.js +6 -8
- package/dist/app/controllers/session-compact-helper.js.map +1 -1
- package/dist/app/controllers/session-controller.js +0 -9
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/commands/providers.d.ts +14 -0
- package/dist/commands/providers.js +223 -0
- package/dist/commands/providers.js.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/llm/capabilities.js +10 -3
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/capability/state.d.ts +1 -1
- package/dist/llm/capability/tool-dialect.js +2 -0
- package/dist/llm/capability/tool-dialect.js.map +1 -1
- package/dist/llm/capability/vision-patterns.js +5 -0
- package/dist/llm/capability/vision-patterns.js.map +1 -1
- package/dist/llm/cline-auth.d.ts +12 -1
- package/dist/llm/cline-auth.js +229 -88
- package/dist/llm/cline-auth.js.map +1 -1
- package/dist/llm/cline.js +146 -48
- package/dist/llm/cline.js.map +1 -1
- package/dist/llm/codex-auth.d.ts +3 -1
- package/dist/llm/codex-auth.js +42 -8
- package/dist/llm/codex-auth.js.map +1 -1
- package/dist/llm/codex.js +17 -14
- package/dist/llm/codex.js.map +1 -1
- package/dist/llm/context-windows.js +6 -0
- package/dist/llm/context-windows.js.map +1 -1
- package/dist/llm/free.js +30 -9
- package/dist/llm/free.js.map +1 -1
- package/dist/llm/kiro-auth.d.ts +113 -0
- package/dist/llm/kiro-auth.js +1017 -0
- package/dist/llm/kiro-auth.js.map +1 -0
- package/dist/llm/kiro.d.ts +10 -0
- package/dist/llm/kiro.js +846 -0
- package/dist/llm/kiro.js.map +1 -0
- package/dist/llm/mimo.d.ts +2 -0
- package/dist/llm/mimo.js +89 -0
- package/dist/llm/mimo.js.map +1 -0
- package/dist/llm/model-families.js +9 -0
- package/dist/llm/model-families.js.map +1 -1
- package/dist/llm/model-layers/model-rules.js +33 -0
- package/dist/llm/model-layers/model-rules.js.map +1 -1
- package/dist/llm/provider-identity.d.ts +1 -0
- package/dist/llm/provider-identity.js +15 -0
- package/dist/llm/provider-identity.js.map +1 -1
- package/dist/llm/provider-info-text.js +87 -1
- package/dist/llm/provider-info-text.js.map +1 -1
- package/dist/llm/provider-profile-layers.d.ts +1 -0
- package/dist/llm/provider-profile-layers.js +67 -1
- package/dist/llm/provider-profile-layers.js.map +1 -1
- package/dist/llm/provider-profiles.js +5 -1
- package/dist/llm/provider-profiles.js.map +1 -1
- package/dist/llm/reasoning-capability.js +2 -0
- package/dist/llm/reasoning-capability.js.map +1 -1
- package/dist/llm/responses-complete.js +7 -1
- package/dist/llm/responses-complete.js.map +1 -1
- package/dist/llm/responses-config.d.ts +1 -1
- package/dist/llm/responses-config.js.map +1 -1
- package/dist/llm/responses-http.d.ts +2 -2
- package/dist/llm/responses-http.js +2 -2
- package/dist/llm/responses-http.js.map +1 -1
- package/dist/llm/responses-stream.js +9 -3
- package/dist/llm/responses-stream.js.map +1 -1
- package/dist/llm/router.js +14 -12
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/routing/provider-selection.js +6 -0
- package/dist/llm/routing/provider-selection.js.map +1 -1
- package/dist/llm/sampling.js +6 -0
- package/dist/llm/sampling.js.map +1 -1
- package/dist/llm/wire/chat-body.js +47 -12
- package/dist/llm/wire/chat-body.js.map +1 -1
- package/dist/noninteractive/stream-blocks.js +3 -0
- package/dist/noninteractive/stream-blocks.js.map +1 -1
- package/dist/store/config.js +2 -0
- package/dist/store/config.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-core/bootstrap/session-resume.d.ts +3 -0
- package/dist/ui-core/bootstrap/session-resume.js +52 -0
- package/dist/ui-core/bootstrap/session-resume.js.map +1 -1
- package/dist/ui-core/commands/key-commands.d.ts +9 -0
- package/dist/ui-core/commands/key-commands.js +438 -16
- package/dist/ui-core/commands/key-commands.js.map +1 -1
- package/dist/ui-core/commands/picker-commands.js +18 -0
- package/dist/ui-core/commands/picker-commands.js.map +1 -1
- package/dist/ui-core/state/hydrate/classic-transcript.js +4 -0
- package/dist/ui-core/state/hydrate/classic-transcript.js.map +1 -1
- package/dist/ui-core/state/transcript/apply-app-event.js +17 -0
- package/dist/ui-core/state/transcript/apply-app-event.js.map +1 -1
- package/dist/ui-core/state/transcript-hydrate.js +165 -80
- package/dist/ui-core/state/transcript-hydrate.js.map +1 -1
- package/dist/ui-core/state/transcript-store.js +7 -1
- package/dist/ui-core/state/transcript-store.js.map +1 -1
- package/dist/ui-core/state/transcript-types.d.ts +2 -0
- package/dist/ui-core/state/transcript-types.js +5 -2
- package/dist/ui-core/state/transcript-types.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
import { homedir, platform } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
export const KIRO_DEFAULT_REGION = "us-east-1";
|
|
7
|
+
export const KIRO_AUTH_SERVICE = "https://prod.us-east-1.auth.desktop.kiro.dev";
|
|
8
|
+
export const KIRO_START_URL = "https://view.awsapps.com/start";
|
|
9
|
+
export const KIRO_REDIRECT_URI = "kiro://kiro.kiroAgent/authenticate-success";
|
|
10
|
+
export const KIRO_KEY_PREFIX = "kiro:";
|
|
11
|
+
export const KIRO_CLI_PORTAL_URL = "https://app.kiro.dev/signin";
|
|
12
|
+
export const KIRO_CLI_REDIRECT_URI = "http://localhost:3128";
|
|
13
|
+
export const KIRO_CLI_REDIRECT_FROM = "kirocli";
|
|
14
|
+
export const KIRO_CLI_CALLBACK_PORT = 3128;
|
|
15
|
+
export const KIRO_CLI_CALLBACK_PATH = "/oauth/callback";
|
|
16
|
+
export const KIRO_CLI_SIGNIN_CALLBACK_PATH = "/signin/callback";
|
|
17
|
+
export const KIRO_DESKTOP_UA_VERSION = "0.7.45";
|
|
18
|
+
export const AWS_REGION_PATTERN = /^[a-z]{2}-[a-z]+-\d{1,2}$/;
|
|
19
|
+
export function assertValidAwsRegion(region) {
|
|
20
|
+
const value = (region ?? KIRO_DEFAULT_REGION).trim().toLowerCase();
|
|
21
|
+
if (!AWS_REGION_PATTERN.test(value)) {
|
|
22
|
+
throw new Error(`Invalid AWS region: "${value}"`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function asRecord(value) {
|
|
27
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
28
|
+
? value
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
|
31
|
+
function sleep(ms) {
|
|
32
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
|
+
}
|
|
34
|
+
function base64UrlEncode(buffer) {
|
|
35
|
+
return buffer
|
|
36
|
+
.toString("base64")
|
|
37
|
+
.replace(/\+/g, "-")
|
|
38
|
+
.replace(/\//g, "_")
|
|
39
|
+
.replace(/=+$/, "");
|
|
40
|
+
}
|
|
41
|
+
function sha256Base64Url(str) {
|
|
42
|
+
return base64UrlEncode(createHash("sha256").update(str, "utf8").digest());
|
|
43
|
+
}
|
|
44
|
+
export function isKiroOAuthToken(value) {
|
|
45
|
+
return value.startsWith(KIRO_KEY_PREFIX);
|
|
46
|
+
}
|
|
47
|
+
export function isKiroRefreshToken(value) {
|
|
48
|
+
return value.trim().startsWith("aorAAAAAG");
|
|
49
|
+
}
|
|
50
|
+
export function encodeKiroKey(credential) {
|
|
51
|
+
const payload = {
|
|
52
|
+
a: credential.accessToken,
|
|
53
|
+
r: credential.refreshToken ?? "",
|
|
54
|
+
p: credential.profileArn ?? "",
|
|
55
|
+
e: credential.expiresAt ?? 0,
|
|
56
|
+
m: credential.authMethod ?? "builder-id",
|
|
57
|
+
g: credential.region ?? KIRO_DEFAULT_REGION,
|
|
58
|
+
c: credential.clientId ?? "",
|
|
59
|
+
s: credential.clientSecret ?? "",
|
|
60
|
+
u: credential.startUrl ?? "",
|
|
61
|
+
k: credential.apiKey ?? "",
|
|
62
|
+
};
|
|
63
|
+
return (KIRO_KEY_PREFIX +
|
|
64
|
+
Buffer.from(JSON.stringify(payload), "utf8").toString("base64url"));
|
|
65
|
+
}
|
|
66
|
+
export function decodeKiroKey(value) {
|
|
67
|
+
if (!isKiroOAuthToken(value))
|
|
68
|
+
return undefined;
|
|
69
|
+
try {
|
|
70
|
+
const raw = Buffer.from(value.slice(KIRO_KEY_PREFIX.length), "base64url").toString("utf8");
|
|
71
|
+
const json = JSON.parse(raw);
|
|
72
|
+
if (typeof json?.a !== "string" && typeof json?.k !== "string")
|
|
73
|
+
return undefined;
|
|
74
|
+
return {
|
|
75
|
+
accessToken: typeof json?.a === "string" ? json.a : "",
|
|
76
|
+
refreshToken: typeof json?.r === "string" && json.r ? json.r : undefined,
|
|
77
|
+
profileArn: typeof json?.p === "string" && json.p ? json.p : undefined,
|
|
78
|
+
expiresAt: typeof json?.e === "number" && json.e > 0 ? json.e : undefined,
|
|
79
|
+
authMethod: typeof json?.m === "string" ? json.m : undefined,
|
|
80
|
+
region: typeof json?.g === "string" && json.g ? json.g : KIRO_DEFAULT_REGION,
|
|
81
|
+
clientId: typeof json?.c === "string" && json.c ? json.c : undefined,
|
|
82
|
+
clientSecret: typeof json?.s === "string" && json.s ? json.s : undefined,
|
|
83
|
+
startUrl: typeof json?.u === "string" && json.u ? json.u : undefined,
|
|
84
|
+
apiKey: typeof json?.k === "string" && json.k ? json.k : undefined,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function registerOidcClient(region = KIRO_DEFAULT_REGION, issuerUrl = KIRO_START_URL) {
|
|
92
|
+
const safeRegion = assertValidAwsRegion(region);
|
|
93
|
+
const endpoint = `https://oidc.${safeRegion}.amazonaws.com/client/register`;
|
|
94
|
+
const response = await fetch(endpoint, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
Accept: "application/json",
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
clientName: "kiro-cli",
|
|
102
|
+
clientType: "public",
|
|
103
|
+
scopes: [
|
|
104
|
+
"codewhisperer:conversations",
|
|
105
|
+
"codewhisperer:analysis",
|
|
106
|
+
"codewhisperer:completions",
|
|
107
|
+
],
|
|
108
|
+
grantTypes: [
|
|
109
|
+
"urn:ietf:params:oauth:grant-type:device_code",
|
|
110
|
+
"refresh_token",
|
|
111
|
+
],
|
|
112
|
+
issuerUrl,
|
|
113
|
+
}),
|
|
114
|
+
signal: AbortSignal.timeout(30_000),
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const errorText = await response.text();
|
|
118
|
+
throw new Error(`Failed to register OIDC client (${response.status}): ${errorText}`);
|
|
119
|
+
}
|
|
120
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
121
|
+
const clientId = typeof json?.clientId === "string" ? json.clientId : "";
|
|
122
|
+
const clientSecret = typeof json?.clientSecret === "string" ? json.clientSecret : "";
|
|
123
|
+
if (!clientId || !clientSecret) {
|
|
124
|
+
throw new Error("Invalid OIDC client registration response");
|
|
125
|
+
}
|
|
126
|
+
const clientSecretExpiresAt = typeof json?.clientSecretExpiresAt === "number"
|
|
127
|
+
? json.clientSecretExpiresAt
|
|
128
|
+
: undefined;
|
|
129
|
+
return { clientId, clientSecret, clientSecretExpiresAt };
|
|
130
|
+
}
|
|
131
|
+
export async function startKiroDeviceAuth(options = {}) {
|
|
132
|
+
const region = assertValidAwsRegion(options.region);
|
|
133
|
+
const authMethod = options.authMethod ?? "builder-id";
|
|
134
|
+
const startUrl = options.startUrl?.trim() || KIRO_START_URL;
|
|
135
|
+
const client = await registerOidcClient(region, startUrl);
|
|
136
|
+
const endpoint = `https://oidc.${region}.amazonaws.com/device_authorization`;
|
|
137
|
+
const response = await fetch(endpoint, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
Accept: "application/json",
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
clientId: client.clientId,
|
|
145
|
+
clientSecret: client.clientSecret,
|
|
146
|
+
startUrl,
|
|
147
|
+
}),
|
|
148
|
+
signal: AbortSignal.timeout(30_000),
|
|
149
|
+
});
|
|
150
|
+
if (!response.ok) {
|
|
151
|
+
const errorText = await response.text();
|
|
152
|
+
throw new Error(`Failed to start Kiro device authorization (${response.status}): ${errorText}`);
|
|
153
|
+
}
|
|
154
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
155
|
+
const deviceCode = typeof json?.deviceCode === "string" ? json.deviceCode : "";
|
|
156
|
+
const userCode = typeof json?.userCode === "string" ? json.userCode : "";
|
|
157
|
+
const verificationUri = typeof json?.verificationUri === "string" ? json.verificationUri : "";
|
|
158
|
+
const verificationUriComplete = typeof json?.verificationUriComplete === "string"
|
|
159
|
+
? json.verificationUriComplete
|
|
160
|
+
: undefined;
|
|
161
|
+
if (!deviceCode || !userCode || !verificationUri) {
|
|
162
|
+
throw new Error("Invalid Kiro device authorization response");
|
|
163
|
+
}
|
|
164
|
+
const expiresInSeconds = typeof json?.expiresIn === "number" ? json.expiresIn : 600;
|
|
165
|
+
const pollIntervalSeconds = typeof json?.interval === "number" ? Math.max(1, json.interval) : 5;
|
|
166
|
+
return {
|
|
167
|
+
deviceCode,
|
|
168
|
+
userCode,
|
|
169
|
+
verificationUri,
|
|
170
|
+
verificationUriComplete,
|
|
171
|
+
expiresInSeconds,
|
|
172
|
+
pollIntervalSeconds,
|
|
173
|
+
clientId: client.clientId,
|
|
174
|
+
clientSecret: client.clientSecret,
|
|
175
|
+
region,
|
|
176
|
+
startUrl,
|
|
177
|
+
authMethod,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export async function pollKiroDeviceAuth(start, options = {}) {
|
|
181
|
+
const deadline = Date.now() + start.expiresInSeconds * 1000;
|
|
182
|
+
let interval = start.pollIntervalSeconds * 1000;
|
|
183
|
+
const endpoint = `https://oidc.${start.region}.amazonaws.com/token`;
|
|
184
|
+
for (;;) {
|
|
185
|
+
if (options.signal?.aborted) {
|
|
186
|
+
throw new Error("Kiro authentication cancelled");
|
|
187
|
+
}
|
|
188
|
+
const remaining = Math.max(0, Math.ceil((deadline - Date.now()) / 1000));
|
|
189
|
+
if (remaining <= 0) {
|
|
190
|
+
throw new Error("Kiro authentication code expired — start again");
|
|
191
|
+
}
|
|
192
|
+
options.onPending?.(remaining);
|
|
193
|
+
const response = await fetch(endpoint, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
Accept: "application/json",
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
clientId: start.clientId,
|
|
201
|
+
clientSecret: start.clientSecret,
|
|
202
|
+
deviceCode: start.deviceCode,
|
|
203
|
+
grantType: "urn:ietf:params:oauth:grant-type:device_code",
|
|
204
|
+
}),
|
|
205
|
+
signal: AbortSignal.timeout(30_000),
|
|
206
|
+
});
|
|
207
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
208
|
+
if (response.ok && typeof json?.accessToken === "string") {
|
|
209
|
+
const accessToken = json.accessToken;
|
|
210
|
+
const refreshToken = typeof json?.refreshToken === "string" ? json.refreshToken : undefined;
|
|
211
|
+
const expiresIn = typeof json?.expiresIn === "number" ? json.expiresIn : 3600;
|
|
212
|
+
const expiresAt = Date.now() + expiresIn * 1000;
|
|
213
|
+
let profileArn;
|
|
214
|
+
try {
|
|
215
|
+
profileArn = await listAvailableProfiles(accessToken, start.region);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
profileArn = undefined;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
accessToken,
|
|
222
|
+
refreshToken,
|
|
223
|
+
profileArn,
|
|
224
|
+
expiresAt,
|
|
225
|
+
authMethod: start.authMethod,
|
|
226
|
+
region: start.region,
|
|
227
|
+
clientId: start.clientId,
|
|
228
|
+
clientSecret: start.clientSecret,
|
|
229
|
+
startUrl: start.startUrl,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const error = typeof json?.error === "string" ? json.error : String(response.status);
|
|
233
|
+
if (error === "authorization_pending") {
|
|
234
|
+
await sleep(interval);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (error === "slow_down") {
|
|
238
|
+
interval += 5_000;
|
|
239
|
+
await sleep(interval);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (error === "expired_token" || error === "access_denied") {
|
|
243
|
+
throw new Error(error === "access_denied"
|
|
244
|
+
? "Kiro authentication was denied"
|
|
245
|
+
: "Kiro authentication code expired — start again");
|
|
246
|
+
}
|
|
247
|
+
const description = typeof json?.error_description === "string" ? json.error_description : "";
|
|
248
|
+
throw new Error(`Kiro authentication failed (${response.status})${description ? `: ${description}` : ""}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export async function startKiroSocialAuth(provider) {
|
|
252
|
+
const idp = provider === "google" ? "Google" : "Github";
|
|
253
|
+
const codeVerifier = base64UrlEncode(randomBytes(32));
|
|
254
|
+
const codeChallenge = sha256Base64Url(codeVerifier);
|
|
255
|
+
const state = base64UrlEncode(randomBytes(16));
|
|
256
|
+
const params = new URLSearchParams({
|
|
257
|
+
idp,
|
|
258
|
+
redirect_uri: KIRO_REDIRECT_URI,
|
|
259
|
+
code_challenge: codeChallenge,
|
|
260
|
+
code_challenge_method: "S256",
|
|
261
|
+
state,
|
|
262
|
+
prompt: "select_account",
|
|
263
|
+
});
|
|
264
|
+
const url = `${KIRO_AUTH_SERVICE}/login?${params.toString()}`;
|
|
265
|
+
return { url, codeVerifier, state };
|
|
266
|
+
}
|
|
267
|
+
export async function exchangeKiroSocialCode(codeOrUrl, codeVerifier, provider = "google") {
|
|
268
|
+
let code = codeOrUrl.trim().replace(/^['"]|['"]$/g, "");
|
|
269
|
+
if (code.includes("error=")) {
|
|
270
|
+
try {
|
|
271
|
+
const parsedUrl = new URL(code);
|
|
272
|
+
const desc = parsedUrl.searchParams.get("error_description") || parsedUrl.searchParams.get("error");
|
|
273
|
+
if (desc)
|
|
274
|
+
throw new Error(desc);
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
if (e instanceof Error && !e.message.includes("Invalid URL"))
|
|
278
|
+
throw e;
|
|
279
|
+
}
|
|
280
|
+
const errorMatch = /[?&]error=([^&#\s]+)/.exec(code);
|
|
281
|
+
const descMatch = /[?&]error_description=([^&#\s]+)/.exec(code);
|
|
282
|
+
const err = errorMatch?.[1] ? decodeURIComponent(errorMatch[1].replace(/\+/g, " ")) : "authentication_failed";
|
|
283
|
+
const desc = descMatch?.[1] ? decodeURIComponent(descMatch[1].replace(/\+/g, " ")) : err;
|
|
284
|
+
throw new Error(desc);
|
|
285
|
+
}
|
|
286
|
+
if (code.includes("code=")) {
|
|
287
|
+
try {
|
|
288
|
+
const parsedUrl = new URL(code);
|
|
289
|
+
const c = parsedUrl.searchParams.get("code");
|
|
290
|
+
if (c)
|
|
291
|
+
code = c.trim();
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
const match = /[?&]code=([^&#\s]+)/.exec(code);
|
|
295
|
+
if (match?.[1]) {
|
|
296
|
+
code = decodeURIComponent(match[1]).trim();
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const direct = /^code=([^&#\s]+)/.exec(code);
|
|
300
|
+
if (direct?.[1])
|
|
301
|
+
code = decodeURIComponent(direct[1]).trim();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const endpoint = `${KIRO_AUTH_SERVICE}/oauth/token`;
|
|
306
|
+
const response = await fetch(endpoint, {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: {
|
|
309
|
+
"Content-Type": "application/json",
|
|
310
|
+
Accept: "application/json",
|
|
311
|
+
"User-Agent": kiroDesktopUserAgent(),
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify({
|
|
314
|
+
code,
|
|
315
|
+
code_verifier: codeVerifier,
|
|
316
|
+
redirect_uri: KIRO_REDIRECT_URI,
|
|
317
|
+
}),
|
|
318
|
+
signal: AbortSignal.timeout(30_000),
|
|
319
|
+
});
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
const errorText = await response.text();
|
|
322
|
+
throw new Error(`Kiro social token exchange failed (${response.status}): ${errorText}`);
|
|
323
|
+
}
|
|
324
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
325
|
+
const accessToken = typeof json?.accessToken === "string" ? json.accessToken : "";
|
|
326
|
+
if (!accessToken) {
|
|
327
|
+
throw new Error("Invalid token response from Kiro auth service");
|
|
328
|
+
}
|
|
329
|
+
const refreshToken = typeof json?.refreshToken === "string" ? json.refreshToken : undefined;
|
|
330
|
+
const profileArn = typeof json?.profileArn === "string" ? json.profileArn : undefined;
|
|
331
|
+
const expiresIn = typeof json?.expiresIn === "number" ? json.expiresIn : 3600;
|
|
332
|
+
return {
|
|
333
|
+
accessToken,
|
|
334
|
+
refreshToken,
|
|
335
|
+
profileArn,
|
|
336
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
337
|
+
authMethod: provider,
|
|
338
|
+
region: KIRO_DEFAULT_REGION,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
export async function listenForKiroSocialCallback(options) {
|
|
342
|
+
const { createServer } = await import("node:http");
|
|
343
|
+
const { writeFile, unlink, mkdir } = await import("node:fs/promises");
|
|
344
|
+
const home = homedir();
|
|
345
|
+
const claiDir = join(home, ".clai");
|
|
346
|
+
await mkdir(claiDir, { recursive: true }).catch(() => { });
|
|
347
|
+
const portFile = join(claiDir, "kiro-pending-callback.json");
|
|
348
|
+
let closed = false;
|
|
349
|
+
const server = createServer((req, res) => {
|
|
350
|
+
const host = req.headers.host || "127.0.0.1";
|
|
351
|
+
const parsed = new URL(req.url || "/", `http://${host}`);
|
|
352
|
+
if (parsed.pathname === "/callback" || parsed.pathname === "/kiro-callback") {
|
|
353
|
+
const urlParam = parsed.searchParams.get("url") || parsed.searchParams.get("code");
|
|
354
|
+
if (urlParam) {
|
|
355
|
+
options.onCode(urlParam);
|
|
356
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
357
|
+
res.end(`<!doctype html><html><body style="font-family:system-ui,-apple-system,sans-serif;background:#18181b;color:#f4f4f5;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;"><div style="text-align:center;padding:32px;background:#27272a;border-radius:12px;border:1px solid #3f3f46;box-shadow:0 10px 25px rgba(0,0,0,0.5);max-width:400px;"><h2 style="color:#22c55e;margin-top:0;">Kiro AI Authenticated</h2><p style="color:#a1a1aa;line-height:1.5;">You can close this tab and return to clai.</p></div></body></html>`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
362
|
+
res.end("Not found");
|
|
363
|
+
});
|
|
364
|
+
await new Promise((resolve, reject) => {
|
|
365
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
366
|
+
server.once("error", reject);
|
|
367
|
+
});
|
|
368
|
+
const address = server.address();
|
|
369
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
370
|
+
await writeFile(portFile, JSON.stringify({ port, pid: process.pid, createdAt: Date.now() }), "utf8").catch(() => { });
|
|
371
|
+
if (platform() === "linux") {
|
|
372
|
+
try {
|
|
373
|
+
await ensureLinuxSchemeHandler();
|
|
374
|
+
}
|
|
375
|
+
catch { }
|
|
376
|
+
}
|
|
377
|
+
const close = () => {
|
|
378
|
+
if (closed)
|
|
379
|
+
return;
|
|
380
|
+
closed = true;
|
|
381
|
+
server.close();
|
|
382
|
+
unlink(portFile).catch(() => { });
|
|
383
|
+
};
|
|
384
|
+
if (options.signal) {
|
|
385
|
+
options.signal.addEventListener("abort", close, { once: true });
|
|
386
|
+
}
|
|
387
|
+
return { port, close };
|
|
388
|
+
}
|
|
389
|
+
async function ensureLinuxSchemeHandler() {
|
|
390
|
+
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
391
|
+
const { execFile } = await import("node:child_process");
|
|
392
|
+
const home = homedir();
|
|
393
|
+
const binDir = join(home, ".local", "bin");
|
|
394
|
+
const appDir = join(home, ".local", "share", "applications");
|
|
395
|
+
await mkdir(binDir, { recursive: true }).catch(() => { });
|
|
396
|
+
await mkdir(appDir, { recursive: true }).catch(() => { });
|
|
397
|
+
const scriptPath = join(binDir, "clai-kiro-auth");
|
|
398
|
+
const desktopPath = join(appDir, "kiro-scheme-handler.desktop");
|
|
399
|
+
const scriptContent = [
|
|
400
|
+
"#!/bin/sh",
|
|
401
|
+
'FILE="$HOME/.clai/kiro-pending-callback.json"',
|
|
402
|
+
'if [ -f "$FILE" ]; then',
|
|
403
|
+
' PORT=$(grep -o \'"port":[0-9]*\' "$FILE" | cut -d: -f2)',
|
|
404
|
+
' if [ -n "$PORT" ]; then',
|
|
405
|
+
' ARG="$1"',
|
|
406
|
+
' node -e \'const http = require("node:http"); const u = new URL("http://127.0.0.1:" + process.argv[1] + "/callback?url=" + encodeURIComponent(process.argv[2])); http.get(u, () => process.exit(0)).on("error", () => process.exit(0));\' "$PORT" "$ARG" 2>/dev/null || curl -s -m 3 "http://127.0.0.1:${PORT}/callback?url=$(node -e \'console.log(encodeURIComponent(process.argv[1]))\' "$ARG" 2>/dev/null || echo "$ARG")" >/dev/null 2>&1',
|
|
407
|
+
" fi",
|
|
408
|
+
"fi",
|
|
409
|
+
].join("\n") + "\n";
|
|
410
|
+
await writeFile(scriptPath, scriptContent, { mode: 0o755 });
|
|
411
|
+
const desktopContent = [
|
|
412
|
+
"[Desktop Entry]",
|
|
413
|
+
"Type=Application",
|
|
414
|
+
"Name=Kiro Scheme Handler",
|
|
415
|
+
`Exec=${scriptPath} %u`,
|
|
416
|
+
"StartupNotify=false",
|
|
417
|
+
"NoDisplay=true",
|
|
418
|
+
"MimeType=x-scheme-handler/kiro;",
|
|
419
|
+
].join("\n") + "\n";
|
|
420
|
+
await writeFile(desktopPath, desktopContent, "utf8");
|
|
421
|
+
await new Promise((resolve) => {
|
|
422
|
+
execFile("gio", ["mime", "x-scheme-handler/kiro", "kiro-scheme-handler.desktop"], () => resolve());
|
|
423
|
+
}).catch(() => { });
|
|
424
|
+
}
|
|
425
|
+
function kiroMachineFingerprint() {
|
|
426
|
+
const seed = process.env.USER || process.env.USERNAME || process.env.HOME || "clai";
|
|
427
|
+
return createHash("sha256").update(seed, "utf8").digest("hex").slice(0, 16);
|
|
428
|
+
}
|
|
429
|
+
export function kiroDesktopUserAgent() {
|
|
430
|
+
return `KiroIDE-${KIRO_DESKTOP_UA_VERSION}-${kiroMachineFingerprint()}`;
|
|
431
|
+
}
|
|
432
|
+
export function isHeadlessEnvironment() {
|
|
433
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) {
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
if (platform() === "linux") {
|
|
437
|
+
return !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
export function createKiroCliAuthorizationFlow() {
|
|
442
|
+
const codeVerifier = base64UrlEncode(randomBytes(32));
|
|
443
|
+
const codeChallenge = sha256Base64Url(codeVerifier);
|
|
444
|
+
const state = randomBytes(16).toString("hex");
|
|
445
|
+
const params = new URLSearchParams({
|
|
446
|
+
state,
|
|
447
|
+
code_challenge: codeChallenge,
|
|
448
|
+
code_challenge_method: "S256",
|
|
449
|
+
redirect_uri: KIRO_CLI_REDIRECT_URI,
|
|
450
|
+
redirect_from: KIRO_CLI_REDIRECT_FROM,
|
|
451
|
+
});
|
|
452
|
+
return {
|
|
453
|
+
authorizeUrl: `${KIRO_CLI_PORTAL_URL}?${params.toString()}`,
|
|
454
|
+
codeVerifier,
|
|
455
|
+
state,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
export function parseKiroCliCallback(input) {
|
|
459
|
+
const value = input.trim();
|
|
460
|
+
if (!value) {
|
|
461
|
+
throw new Error("missing Kiro callback URL");
|
|
462
|
+
}
|
|
463
|
+
let path = KIRO_CLI_CALLBACK_PATH;
|
|
464
|
+
let query = value;
|
|
465
|
+
const urlMatch = /^https?:\/\/[^/]+(\/[^?]*)?(\?.*)?$/i.exec(value);
|
|
466
|
+
const bareHostMatch = /^(?:localhost|127\.0\.0\.1)(?::\d+)?(\/[^?]*)?(\?.*)?$/i.exec(value);
|
|
467
|
+
if (urlMatch) {
|
|
468
|
+
path = urlMatch[1] || KIRO_CLI_CALLBACK_PATH;
|
|
469
|
+
query = (urlMatch[2] || "").replace(/^\?/, "");
|
|
470
|
+
}
|
|
471
|
+
else if (bareHostMatch) {
|
|
472
|
+
path = bareHostMatch[1] || KIRO_CLI_CALLBACK_PATH;
|
|
473
|
+
query = (bareHostMatch[2] || "").replace(/^\?/, "");
|
|
474
|
+
}
|
|
475
|
+
else if (value.includes("?")) {
|
|
476
|
+
const idx = value.indexOf("?");
|
|
477
|
+
path = value.slice(0, idx) || KIRO_CLI_CALLBACK_PATH;
|
|
478
|
+
query = value.slice(idx + 1);
|
|
479
|
+
}
|
|
480
|
+
const queryNoHash = query.split("#")[0] ?? "";
|
|
481
|
+
const pairs = new URLSearchParams(queryNoHash);
|
|
482
|
+
const error = pairs.get("error");
|
|
483
|
+
if (error) {
|
|
484
|
+
const desc = pairs.get("error_description") || error;
|
|
485
|
+
throw new Error(`Kiro login failed: ${desc}`);
|
|
486
|
+
}
|
|
487
|
+
const code = pairs.get("code");
|
|
488
|
+
if (!code) {
|
|
489
|
+
throw new Error("missing Kiro authorization code");
|
|
490
|
+
}
|
|
491
|
+
const loginOption = pairs.get("login_option");
|
|
492
|
+
if (!loginOption) {
|
|
493
|
+
throw new Error("missing Kiro login_option");
|
|
494
|
+
}
|
|
495
|
+
const state = pairs.get("state") || undefined;
|
|
496
|
+
return { code, loginOption, state, path };
|
|
497
|
+
}
|
|
498
|
+
export async function exchangeKiroPortalCode(callback, codeVerifier) {
|
|
499
|
+
const redirectUri = `${KIRO_CLI_REDIRECT_URI}${callback.path}?login_option=${callback.loginOption}`;
|
|
500
|
+
const endpoint = `${KIRO_AUTH_SERVICE}/oauth/token`;
|
|
501
|
+
const response = await fetch(endpoint, {
|
|
502
|
+
method: "POST",
|
|
503
|
+
headers: {
|
|
504
|
+
"Content-Type": "application/json",
|
|
505
|
+
Accept: "application/json",
|
|
506
|
+
"User-Agent": kiroDesktopUserAgent(),
|
|
507
|
+
},
|
|
508
|
+
body: JSON.stringify({
|
|
509
|
+
code: callback.code,
|
|
510
|
+
code_verifier: codeVerifier,
|
|
511
|
+
redirect_uri: redirectUri,
|
|
512
|
+
invitation_code: null,
|
|
513
|
+
}),
|
|
514
|
+
signal: AbortSignal.timeout(30_000),
|
|
515
|
+
});
|
|
516
|
+
if (!response.ok) {
|
|
517
|
+
const errorText = await response.text();
|
|
518
|
+
throw new Error(`Kiro portal token exchange failed (${response.status}): ${errorText}`);
|
|
519
|
+
}
|
|
520
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
521
|
+
const accessToken = typeof json?.accessToken === "string" ? json.accessToken : "";
|
|
522
|
+
if (!accessToken) {
|
|
523
|
+
throw new Error("Invalid token response from Kiro auth service");
|
|
524
|
+
}
|
|
525
|
+
const refreshToken = typeof json?.refreshToken === "string" ? json.refreshToken : undefined;
|
|
526
|
+
const profileArn = typeof json?.profileArn === "string" ? json.profileArn : undefined;
|
|
527
|
+
const expiresIn = typeof json?.expiresIn === "number" ? json.expiresIn : 3600;
|
|
528
|
+
return {
|
|
529
|
+
accessToken,
|
|
530
|
+
refreshToken,
|
|
531
|
+
profileArn,
|
|
532
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
533
|
+
authMethod: callback.loginOption === "google" || callback.loginOption === "github"
|
|
534
|
+
? callback.loginOption
|
|
535
|
+
: "imported",
|
|
536
|
+
region: KIRO_DEFAULT_REGION,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
export function listenForKiroCliCallback(options) {
|
|
540
|
+
const timeoutMs = options.timeoutMs ?? 300_000;
|
|
541
|
+
let resolvePromise;
|
|
542
|
+
let rejectPromise;
|
|
543
|
+
const promise = new Promise((res, rej) => {
|
|
544
|
+
resolvePromise = res;
|
|
545
|
+
rejectPromise = rej;
|
|
546
|
+
});
|
|
547
|
+
let settled = false;
|
|
548
|
+
let server;
|
|
549
|
+
const finish = (err, cb) => {
|
|
550
|
+
if (settled)
|
|
551
|
+
return;
|
|
552
|
+
settled = true;
|
|
553
|
+
clearTimeout(timer);
|
|
554
|
+
server?.close();
|
|
555
|
+
if (err)
|
|
556
|
+
rejectPromise(err);
|
|
557
|
+
else if (cb)
|
|
558
|
+
resolvePromise(cb);
|
|
559
|
+
};
|
|
560
|
+
const timer = setTimeout(() => {
|
|
561
|
+
finish(new Error("Kiro login timed out waiting for browser callback"));
|
|
562
|
+
}, timeoutMs);
|
|
563
|
+
if (typeof timer.unref === "function")
|
|
564
|
+
timer.unref();
|
|
565
|
+
server = createServer((req, res) => {
|
|
566
|
+
const host = req.headers.host || `localhost:${KIRO_CLI_CALLBACK_PORT}`;
|
|
567
|
+
let parsed;
|
|
568
|
+
try {
|
|
569
|
+
parsed = new URL(req.url || "/", `http://${host}`);
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
573
|
+
res.end("Bad request");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const pathname = parsed.pathname;
|
|
577
|
+
if (pathname !== KIRO_CLI_CALLBACK_PATH &&
|
|
578
|
+
pathname !== KIRO_CLI_SIGNIN_CALLBACK_PATH &&
|
|
579
|
+
pathname !== "/") {
|
|
580
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
581
|
+
res.end("Not found");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const fullInput = `${pathname}${parsed.search}`;
|
|
585
|
+
let callback;
|
|
586
|
+
try {
|
|
587
|
+
callback = parseKiroCliCallback(fullInput);
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
591
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
592
|
+
res.end(`<!doctype html><html><body style="font-family:system-ui,-apple-system,sans-serif;background:#18181b;color:#f4f4f5;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;"><div style="text-align:center;padding:32px;background:#27272a;border-radius:12px;border:1px solid #3f3f46;max-width:420px;"><h2 style="color:#ef4444;margin-top:0;">Kiro login failed</h2><p style="color:#a1a1aa;line-height:1.5;">${msg.replace(/</g, "<")}</p><p style="color:#71717a;font-size:13px;">Return to clai and try again.</p></div></body></html>`);
|
|
593
|
+
finish(err instanceof Error ? err : new Error(msg));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (options.expectedState && callback.state && callback.state !== options.expectedState) {
|
|
597
|
+
const err = new Error("Kiro login state mismatch (possible CSRF). Please try again.");
|
|
598
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
599
|
+
res.end(`<!doctype html><html><body style="font-family:system-ui,-apple-system,sans-serif;background:#18181b;color:#f4f4f5;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;"><div style="text-align:center;padding:32px;background:#27272a;border-radius:12px;border:1px solid #3f3f46;max-width:420px;"><h2 style="color:#ef4444;margin-top:0;">Kiro login failed</h2><p style="color:#a1a1aa;line-height:1.5;">State mismatch. Return to clai and try again.</p></div></body></html>`);
|
|
600
|
+
finish(err);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
604
|
+
res.end(`<!doctype html><html><body style="font-family:system-ui,-apple-system,sans-serif;background:#18181b;color:#f4f4f5;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;"><div style="text-align:center;padding:32px;background:#27272a;border-radius:12px;border:1px solid #3f3f46;max-width:420px;"><h2 style="color:#22c55e;margin-top:0;">Kiro AI Authenticated</h2><p style="color:#a1a1aa;line-height:1.5;">You can close this tab and return to clai.</p></div></body></html>`);
|
|
605
|
+
finish(undefined, callback);
|
|
606
|
+
});
|
|
607
|
+
server.once("error", (err) => {
|
|
608
|
+
if (err.code === "EADDRINUSE") {
|
|
609
|
+
finish(new Error(`Port ${KIRO_CLI_CALLBACK_PORT} is in use — close the other app or use device-code sign-in instead.`));
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
finish(err);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
server.listen(KIRO_CLI_CALLBACK_PORT, "127.0.0.1");
|
|
616
|
+
if (options.signal) {
|
|
617
|
+
options.signal.addEventListener("abort", () => finish(new Error("Kiro authentication cancelled")), { once: true });
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
port: KIRO_CLI_CALLBACK_PORT,
|
|
621
|
+
promise,
|
|
622
|
+
close() {
|
|
623
|
+
finish(new Error("Kiro authentication cancelled"));
|
|
624
|
+
},
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
export async function refreshKiroToken(refreshToken, options = {}) {
|
|
628
|
+
const { clientId, clientSecret, region } = options;
|
|
629
|
+
if (clientId && clientSecret) {
|
|
630
|
+
const safeRegion = assertValidAwsRegion(region);
|
|
631
|
+
const endpoint = `https://oidc.${safeRegion}.amazonaws.com/token`;
|
|
632
|
+
const response = await fetch(endpoint, {
|
|
633
|
+
method: "POST",
|
|
634
|
+
headers: {
|
|
635
|
+
"Content-Type": "application/json",
|
|
636
|
+
Accept: "application/json",
|
|
637
|
+
},
|
|
638
|
+
body: JSON.stringify({
|
|
639
|
+
clientId,
|
|
640
|
+
clientSecret,
|
|
641
|
+
refreshToken,
|
|
642
|
+
grantType: "refresh_token",
|
|
643
|
+
}),
|
|
644
|
+
signal: AbortSignal.timeout(30_000),
|
|
645
|
+
});
|
|
646
|
+
if (!response.ok) {
|
|
647
|
+
const errorText = await response.text();
|
|
648
|
+
throw new Error(`Kiro token refresh failed (${response.status}): ${errorText}`);
|
|
649
|
+
}
|
|
650
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
651
|
+
const accessToken = typeof json?.accessToken === "string" ? json.accessToken : "";
|
|
652
|
+
if (!accessToken) {
|
|
653
|
+
throw new Error("Invalid token refresh response from AWS OIDC");
|
|
654
|
+
}
|
|
655
|
+
const newRefreshToken = typeof json?.refreshToken === "string"
|
|
656
|
+
? json.refreshToken
|
|
657
|
+
: refreshToken;
|
|
658
|
+
const expiresIn = typeof json?.expiresIn === "number" ? json.expiresIn : 3600;
|
|
659
|
+
let profileArn = options.profileArn;
|
|
660
|
+
if (!profileArn) {
|
|
661
|
+
try {
|
|
662
|
+
profileArn = await listAvailableProfiles(accessToken, safeRegion);
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
profileArn = undefined;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return {
|
|
669
|
+
accessToken,
|
|
670
|
+
refreshToken: newRefreshToken,
|
|
671
|
+
profileArn,
|
|
672
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
673
|
+
authMethod: options.authMethod ?? "builder-id",
|
|
674
|
+
region: safeRegion,
|
|
675
|
+
clientId,
|
|
676
|
+
clientSecret,
|
|
677
|
+
startUrl: options.startUrl,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const endpoint = `${KIRO_AUTH_SERVICE}/refreshToken`;
|
|
681
|
+
const response = await fetch(endpoint, {
|
|
682
|
+
method: "POST",
|
|
683
|
+
headers: {
|
|
684
|
+
"Content-Type": "application/json",
|
|
685
|
+
Accept: "application/json",
|
|
686
|
+
"User-Agent": kiroDesktopUserAgent(),
|
|
687
|
+
},
|
|
688
|
+
body: JSON.stringify({ refreshToken }),
|
|
689
|
+
signal: AbortSignal.timeout(30_000),
|
|
690
|
+
});
|
|
691
|
+
if (!response.ok) {
|
|
692
|
+
const errorText = await response.text();
|
|
693
|
+
throw new Error(`Kiro token refresh failed (${response.status}): ${errorText}`);
|
|
694
|
+
}
|
|
695
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
696
|
+
const accessToken = typeof json?.accessToken === "string" ? json.accessToken : "";
|
|
697
|
+
if (!accessToken) {
|
|
698
|
+
throw new Error("Invalid token refresh response from Kiro service");
|
|
699
|
+
}
|
|
700
|
+
const newRefreshToken = typeof json?.refreshToken === "string" ? json.refreshToken : refreshToken;
|
|
701
|
+
const profileArn = typeof json?.profileArn === "string"
|
|
702
|
+
? json.profileArn
|
|
703
|
+
: options.profileArn;
|
|
704
|
+
const expiresIn = typeof json?.expiresIn === "number" ? json.expiresIn : 3600;
|
|
705
|
+
return {
|
|
706
|
+
accessToken,
|
|
707
|
+
refreshToken: newRefreshToken,
|
|
708
|
+
profileArn,
|
|
709
|
+
expiresAt: Date.now() + expiresIn * 1000,
|
|
710
|
+
authMethod: options.authMethod ?? "imported",
|
|
711
|
+
region: options.region ?? KIRO_DEFAULT_REGION,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
export async function listAvailableProfiles(accessToken, region = KIRO_DEFAULT_REGION) {
|
|
715
|
+
const safeRegion = assertValidAwsRegion(region);
|
|
716
|
+
const endpoint = `https://codewhisperer.${safeRegion}.amazonaws.com`;
|
|
717
|
+
const response = await fetch(endpoint, {
|
|
718
|
+
method: "POST",
|
|
719
|
+
headers: {
|
|
720
|
+
"Content-Type": "application/x-amz-json-1.0",
|
|
721
|
+
"x-amz-target": "AmazonCodeWhispererService.ListAvailableProfiles",
|
|
722
|
+
Authorization: `Bearer ${accessToken}`,
|
|
723
|
+
Accept: "application/json",
|
|
724
|
+
},
|
|
725
|
+
body: JSON.stringify({ maxResults: 10 }),
|
|
726
|
+
signal: AbortSignal.timeout(15_000),
|
|
727
|
+
});
|
|
728
|
+
if (!response.ok)
|
|
729
|
+
return undefined;
|
|
730
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
731
|
+
const profiles = Array.isArray(json?.profiles) ? json.profiles : [];
|
|
732
|
+
const arnOf = (p) => {
|
|
733
|
+
const rec = asRecord(p);
|
|
734
|
+
return typeof rec?.arn === "string"
|
|
735
|
+
? rec.arn
|
|
736
|
+
: typeof rec?.profileArn === "string"
|
|
737
|
+
? rec.profileArn
|
|
738
|
+
: null;
|
|
739
|
+
};
|
|
740
|
+
const match = profiles.find((p) => arnOf(p)?.split(":")[3] === safeRegion) ||
|
|
741
|
+
profiles[0];
|
|
742
|
+
return arnOf(match) ?? undefined;
|
|
743
|
+
}
|
|
744
|
+
export async function listAvailableApiKeyModels(apiKey, region = KIRO_DEFAULT_REGION) {
|
|
745
|
+
const safeRegion = assertValidAwsRegion(region);
|
|
746
|
+
const params = new URLSearchParams({ origin: "AI_EDITOR" });
|
|
747
|
+
const endpoint = `https://q.${safeRegion}.amazonaws.com/ListAvailableModels?${params.toString()}`;
|
|
748
|
+
const response = await fetch(endpoint, {
|
|
749
|
+
method: "GET",
|
|
750
|
+
headers: {
|
|
751
|
+
Authorization: `Bearer ${apiKey}`,
|
|
752
|
+
TokenType: "API_KEY",
|
|
753
|
+
Accept: "application/json",
|
|
754
|
+
"User-Agent": "AWS-SDK-JS/3.0.0 kiro-ide/1.0.0",
|
|
755
|
+
"X-Amz-User-Agent": "aws-sdk-js/3.0.0 kiro-ide/1.0.0",
|
|
756
|
+
},
|
|
757
|
+
signal: AbortSignal.timeout(20_000),
|
|
758
|
+
});
|
|
759
|
+
if (!response.ok) {
|
|
760
|
+
const errorText = await response.text();
|
|
761
|
+
throw new Error(`Failed to list API key models (${response.status}): ${errorText}`);
|
|
762
|
+
}
|
|
763
|
+
const json = asRecord(await response.json().catch(() => ({})));
|
|
764
|
+
const models = Array.isArray(json?.models) ? json.models : [];
|
|
765
|
+
return models
|
|
766
|
+
.map((m) => {
|
|
767
|
+
const rec = asRecord(m);
|
|
768
|
+
return typeof rec?.modelId === "string"
|
|
769
|
+
? rec.modelId
|
|
770
|
+
: typeof rec?.id === "string"
|
|
771
|
+
? rec.id
|
|
772
|
+
: "";
|
|
773
|
+
})
|
|
774
|
+
.filter(Boolean);
|
|
775
|
+
}
|
|
776
|
+
export async function validateKiroApiKey(apiKey, region = KIRO_DEFAULT_REGION) {
|
|
777
|
+
try {
|
|
778
|
+
const models = await listAvailableApiKeyModels(apiKey, region);
|
|
779
|
+
return models.length > 0;
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
export async function candidateKiroCredentialPaths() {
|
|
786
|
+
const home = homedir();
|
|
787
|
+
const os = platform();
|
|
788
|
+
const paths = [
|
|
789
|
+
join(home, ".kiro", "auth.json"),
|
|
790
|
+
join(home, ".kiro", "credentials.json"),
|
|
791
|
+
];
|
|
792
|
+
if (os === "darwin") {
|
|
793
|
+
paths.push(join(home, "Library", "Application Support", "Kiro", "auth.json"), join(home, "Library", "Application Support", "kiro-desktop", "auth.json"));
|
|
794
|
+
}
|
|
795
|
+
else if (os === "linux") {
|
|
796
|
+
paths.push(join(home, ".config", "kiro", "auth.json"), join(home, ".config", "kiro-desktop", "auth.json"));
|
|
797
|
+
}
|
|
798
|
+
else if (os === "win32") {
|
|
799
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
800
|
+
paths.push(join(appData, "Kiro", "auth.json"), join(appData, "kiro-desktop", "auth.json"));
|
|
801
|
+
}
|
|
802
|
+
const ssoCacheDir = join(home, ".aws", "sso", "cache");
|
|
803
|
+
try {
|
|
804
|
+
const files = await readdir(ssoCacheDir);
|
|
805
|
+
for (const file of files) {
|
|
806
|
+
if (file.endsWith(".json")) {
|
|
807
|
+
paths.push(join(ssoCacheDir, file));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
catch { }
|
|
812
|
+
return paths;
|
|
813
|
+
}
|
|
814
|
+
export async function readKiroStoredAuth() {
|
|
815
|
+
const paths = await candidateKiroCredentialPaths();
|
|
816
|
+
for (const path of paths) {
|
|
817
|
+
let parsed;
|
|
818
|
+
try {
|
|
819
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const rec = asRecord(parsed);
|
|
825
|
+
if (!rec)
|
|
826
|
+
continue;
|
|
827
|
+
const accessToken = typeof rec.accessToken === "string"
|
|
828
|
+
? rec.accessToken
|
|
829
|
+
: typeof rec.access_token === "string"
|
|
830
|
+
? rec.access_token
|
|
831
|
+
: "";
|
|
832
|
+
const refreshToken = typeof rec.refreshToken === "string"
|
|
833
|
+
? rec.refreshToken
|
|
834
|
+
: typeof rec.refresh_token === "string"
|
|
835
|
+
? rec.refresh_token
|
|
836
|
+
: undefined;
|
|
837
|
+
if (!accessToken && !refreshToken)
|
|
838
|
+
continue;
|
|
839
|
+
const clientId = typeof rec.clientId === "string"
|
|
840
|
+
? rec.clientId
|
|
841
|
+
: typeof rec.client_id === "string"
|
|
842
|
+
? rec.client_id
|
|
843
|
+
: undefined;
|
|
844
|
+
const clientSecret = typeof rec.clientSecret === "string"
|
|
845
|
+
? rec.clientSecret
|
|
846
|
+
: typeof rec.client_secret === "string"
|
|
847
|
+
? rec.client_secret
|
|
848
|
+
: undefined;
|
|
849
|
+
const region = typeof rec.region === "string" && rec.region
|
|
850
|
+
? rec.region
|
|
851
|
+
: KIRO_DEFAULT_REGION;
|
|
852
|
+
const profileArn = typeof rec.profileArn === "string"
|
|
853
|
+
? rec.profileArn
|
|
854
|
+
: typeof rec.profile_arn === "string"
|
|
855
|
+
? rec.profile_arn
|
|
856
|
+
: undefined;
|
|
857
|
+
const startUrl = typeof rec.startUrl === "string"
|
|
858
|
+
? rec.startUrl
|
|
859
|
+
: typeof rec.start_url === "string"
|
|
860
|
+
? rec.start_url
|
|
861
|
+
: undefined;
|
|
862
|
+
let expiresAt;
|
|
863
|
+
if (typeof rec.expiresAt === "number") {
|
|
864
|
+
expiresAt = rec.expiresAt;
|
|
865
|
+
}
|
|
866
|
+
else if (typeof rec.expires_at === "string") {
|
|
867
|
+
expiresAt = Date.parse(rec.expires_at);
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
accessToken,
|
|
871
|
+
refreshToken,
|
|
872
|
+
profileArn,
|
|
873
|
+
expiresAt,
|
|
874
|
+
region,
|
|
875
|
+
clientId,
|
|
876
|
+
clientSecret,
|
|
877
|
+
startUrl,
|
|
878
|
+
authMethod: clientId && clientSecret ? "builder-id" : "imported",
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
const home = homedir();
|
|
882
|
+
const os = platform();
|
|
883
|
+
const sqlitePaths = [
|
|
884
|
+
join(home, ".local", "share", "kiro-cli", "data.sqlite3"),
|
|
885
|
+
join(home, ".config", "kiro-cli", "data.sqlite3"),
|
|
886
|
+
join(home, ".config", "kiro", "data.sqlite3"),
|
|
887
|
+
join(home, ".kiro", "data.sqlite3"),
|
|
888
|
+
];
|
|
889
|
+
if (os === "darwin") {
|
|
890
|
+
sqlitePaths.push(join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3"), join(home, "Library", "Application Support", "Kiro", "data.sqlite3"));
|
|
891
|
+
}
|
|
892
|
+
else if (os === "win32") {
|
|
893
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
894
|
+
sqlitePaths.push(join(appData, "kiro-cli", "data.sqlite3"), join(appData, "Kiro", "data.sqlite3"));
|
|
895
|
+
}
|
|
896
|
+
for (const dbPath of sqlitePaths) {
|
|
897
|
+
try {
|
|
898
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
899
|
+
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
900
|
+
try {
|
|
901
|
+
const rows = db.prepare("SELECT key, value FROM auth_kv").all();
|
|
902
|
+
const byKey = new Map(rows.map((r) => [r.key, r.value]));
|
|
903
|
+
const tokenKeys = [
|
|
904
|
+
"kirocli:social:token",
|
|
905
|
+
"kirocli:oidc:token",
|
|
906
|
+
"kirocli:odic:token",
|
|
907
|
+
"codewhisperer:oidc:token",
|
|
908
|
+
"codewhisperer:odic:token",
|
|
909
|
+
];
|
|
910
|
+
const deviceKeys = [
|
|
911
|
+
"kirocli:oidc:device-registration",
|
|
912
|
+
"kirocli:odic:device-registration",
|
|
913
|
+
"codewhisperer:oidc:device-registration",
|
|
914
|
+
"codewhisperer:odic:device-registration",
|
|
915
|
+
];
|
|
916
|
+
const tokenRaw = tokenKeys.map((k) => byKey.get(k)).find((v) => v);
|
|
917
|
+
if (!tokenRaw)
|
|
918
|
+
continue;
|
|
919
|
+
const deviceRaw = deviceKeys.map((k) => byKey.get(k)).find((v) => v);
|
|
920
|
+
try {
|
|
921
|
+
const parsed = JSON.parse(tokenRaw);
|
|
922
|
+
const device = deviceRaw
|
|
923
|
+
? JSON.parse(deviceRaw)
|
|
924
|
+
: undefined;
|
|
925
|
+
const accessToken = typeof parsed.access_token === "string"
|
|
926
|
+
? parsed.access_token
|
|
927
|
+
: typeof parsed.accessToken === "string"
|
|
928
|
+
? parsed.accessToken
|
|
929
|
+
: "";
|
|
930
|
+
const refreshToken = typeof parsed.refresh_token === "string"
|
|
931
|
+
? parsed.refresh_token
|
|
932
|
+
: typeof parsed.refreshToken === "string"
|
|
933
|
+
? parsed.refreshToken
|
|
934
|
+
: undefined;
|
|
935
|
+
if (!accessToken && !refreshToken)
|
|
936
|
+
continue;
|
|
937
|
+
let expiresAt;
|
|
938
|
+
if (typeof parsed.expires_at === "number")
|
|
939
|
+
expiresAt = parsed.expires_at;
|
|
940
|
+
else if (typeof parsed.expiresAt === "number")
|
|
941
|
+
expiresAt = parsed.expiresAt;
|
|
942
|
+
else if (typeof parsed.expires_at === "string")
|
|
943
|
+
expiresAt = Date.parse(parsed.expires_at);
|
|
944
|
+
const profileArn = typeof parsed.profile_arn === "string"
|
|
945
|
+
? parsed.profile_arn
|
|
946
|
+
: typeof parsed.profileArn === "string"
|
|
947
|
+
? parsed.profileArn
|
|
948
|
+
: undefined;
|
|
949
|
+
const clientId = (typeof parsed.clientId === "string" && parsed.clientId) ||
|
|
950
|
+
(typeof parsed.client_id === "string" && parsed.client_id) ||
|
|
951
|
+
(typeof device?.clientId === "string" && device.clientId) ||
|
|
952
|
+
(typeof device?.client_id === "string" && device.client_id) ||
|
|
953
|
+
undefined;
|
|
954
|
+
const clientSecret = (typeof parsed.clientSecret === "string" && parsed.clientSecret) ||
|
|
955
|
+
(typeof parsed.client_secret === "string" && parsed.client_secret) ||
|
|
956
|
+
(typeof device?.clientSecret === "string" && device.clientSecret) ||
|
|
957
|
+
(typeof device?.client_secret === "string" && device.client_secret) ||
|
|
958
|
+
undefined;
|
|
959
|
+
const provider = typeof parsed.provider === "string"
|
|
960
|
+
? parsed.provider.toLowerCase()
|
|
961
|
+
: undefined;
|
|
962
|
+
const authMethod = provider === "google" || provider === "github"
|
|
963
|
+
? provider
|
|
964
|
+
: clientId && clientSecret
|
|
965
|
+
? "builder-id"
|
|
966
|
+
: "imported";
|
|
967
|
+
return {
|
|
968
|
+
accessToken,
|
|
969
|
+
refreshToken,
|
|
970
|
+
expiresAt,
|
|
971
|
+
profileArn,
|
|
972
|
+
authMethod,
|
|
973
|
+
region: KIRO_DEFAULT_REGION,
|
|
974
|
+
clientId,
|
|
975
|
+
clientSecret,
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
catch { }
|
|
979
|
+
}
|
|
980
|
+
finally {
|
|
981
|
+
db.close();
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
catch { }
|
|
985
|
+
}
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
export async function importExistingKiroAuth() {
|
|
989
|
+
const stored = await readKiroStoredAuth();
|
|
990
|
+
if (!stored)
|
|
991
|
+
return undefined;
|
|
992
|
+
if (stored.refreshToken &&
|
|
993
|
+
stored.expiresAt !== undefined &&
|
|
994
|
+
stored.expiresAt < Date.now()) {
|
|
995
|
+
try {
|
|
996
|
+
return await refreshKiroToken(stored.refreshToken, stored);
|
|
997
|
+
}
|
|
998
|
+
catch {
|
|
999
|
+
return stored;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return stored;
|
|
1003
|
+
}
|
|
1004
|
+
export async function maybeRefreshKiroCredential(key, storedRefreshToken) {
|
|
1005
|
+
const decoded = decodeKiroKey(key);
|
|
1006
|
+
const refreshToken = decoded?.refreshToken || storedRefreshToken;
|
|
1007
|
+
if (!refreshToken)
|
|
1008
|
+
return undefined;
|
|
1009
|
+
try {
|
|
1010
|
+
const refreshed = await refreshKiroToken(refreshToken, decoded);
|
|
1011
|
+
return encodeKiroKey(refreshed);
|
|
1012
|
+
}
|
|
1013
|
+
catch {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
//# sourceMappingURL=kiro-auth.js.map
|